【问题标题】:Share a dict with multiple Python scripts与多个 Python 脚本共享一个 dict
【发布时间】:2018-07-02 17:43:39
【问题描述】:

我想要一个独特的dict(键/值)数据库,可以从同时运行的多个 Python 脚本中访问。

如果script1.py 更新d[2839],那么script2.py 在几秒后查询d[2839] 时应该会看到修改后的值

  • 我考虑过使用SQLite,但似乎从多个进程并发写入/读取并不是SQLite的强项(假设script1.py刚刚修改了d[2839]script2.py的SQLite连接怎么知道@ 987654321@)

  • 我也想过锁定文件,当我想刷新修改(但它是rather tricky to do),并使用json.dump序列化,然后尝试检测修改,如果有任何修改等,请使用json.load 重新加载...哦不,我正在重新发明轮子,并重新发明效率特别低的键/值数据库!

  • redis 看起来像是一个解决方案,但 it does not officially support Windows,同样适用于 leveldb

  • 多个脚本可能想要同时写入(即使这是一个非常罕见的事件),有没有办法让数据库系统处理这个问题(感谢锁定参数?似乎通过默认 SQLite 不能这样做,因为"SQLite supports an unlimited number of simultaneous readers, but it will only allow one writer at any instant in time.")

什么是 Pythonic 解决方案?

注意:我在 Windows 上,dict 最多应该有 1M 项(键和值都是整数)。

【问题讨论】:

  • 好问题。我也期待一个合适的解决方案。同时,查看 multiprocessing Manager(仅适用于您可以从单个入口点启动两个脚本的情况)。作为最后的选择,你可以在 Docker 中运行 redis。
  • @9dogs 是的,解决方案在很多情况下都非常有用:) 注意:我不会从同一个入口点/不使用multiprocessing 启动所有脚本。我正在寻找比在 Docker 中运行 redis 更轻量级的解决方案,但我会在万不得已时记住这个选项!
  • 等等,为什么 SQLite 不能工作?
  • @juanpa.arrivillaga 我读到 SQLite 不是为同时从多个程序并发写入/读取而设计的。现在不是真的了吗?
  • @Basj。请参阅:sqlite3 concurrent access。它应该足以满足您的要求。

标签: python sqlite dictionary key-value-store


【解决方案1】:

您可以使用基于文档的数据库管理器。也许对于您的系统来说太重了,但并发访问通常是数据库管理系统和连接到它们的 API 到位的原因之一。

我在 Python 中使用过 MongoDB,它运行良好。 Python API 文档非常好,每个文档(数据库的元素)都是一个字典,可以像这样加载到 python 中。

【讨论】:

    【解决方案2】:

    我会使用一个 pub/sub websocket-framework,比如Autobahn/Python,用一个脚本作为“服务器”,它处理所有文件通信,但它取决于规模,也许这可能是矫枉过正。

    【讨论】:

      【解决方案3】:

      除了SQLite之外的嵌入式数据存储大部分都没有并发访问的优化,我也对SQLite的并发性能很好奇,所以我做了一个benchmark:

      import time
      import sqlite3
      import os
      import random
      import sys
      import multiprocessing
      
      
      class Store():
      
          def __init__(self, filename='kv.db'):
              self.conn = sqlite3.connect(filename, timeout=60)
              self.conn.execute('pragma journal_mode=wal')
              self.conn.execute('create table if not exists "kv" (key integer primary key, value integer) without rowid')
              self.conn.commit()
      
          def get(self, key):
              item = self.conn.execute('select value from "kv" where key=?', (key,))
              if item:
                  return next(item)[0]
      
          def set(self, key, value):
              self.conn.execute('replace into "kv" (key, value) values (?,?)', (key, value))
              self.conn.commit()
      
      
      def worker(n):
          d = [random.randint(0, 1<<31) for _ in range(n)]
          s = Store()
          for i in d:
              s.set(i, i)
          random.shuffle(d)
          for i in d:
              s.get(i)
      
      
      def test(c):
          n = 5000
          start = time.time()
          ps = []
          for _ in range(c):
              p = multiprocessing.Process(target=worker, args=(n,))
              p.start()
              ps.append(p)
          while any(p.is_alive() for p in ps):
              time.sleep(0.01)
          cost = time.time() - start
          print(f'{c:<10d}\t{cost:<7.2f}\t{n/cost:<20.2f}\t{n*c/cost:<14.2f}')
      
      
      def main():
          print(f'concurrency\ttime(s)\tpre process TPS(r/s)\ttotal TPS(r/s)')
          for c in range(1, 9):
              test(c)
      
      
      if __name__ == '__main__':
          main()
      

      我的 4 核 macOS 机器上的结果,SSD 卷:

      concurrency time(s) pre process TPS(r/s)    total TPS(r/s)
      1           0.65    7638.43                 7638.43
      2           1.30    3854.69                 7709.38
      3           1.83    2729.32                 8187.97
      4           2.43    2055.25                 8221.01
      5           3.07    1629.35                 8146.74
      6           3.87    1290.63                 7743.78
      7           4.80    1041.73                 7292.13
      8           5.37    931.27                  7450.15
      

      8 核 windows server 2012 云服务器上的结果,SSD 卷:

      concurrency     time(s) pre process TPS(r/s)    total TPS(r/s)
      1               4.12    1212.14                 1212.14
      2               7.87    634.93                  1269.87
      3               14.06   355.56                  1066.69
      4               15.84   315.59                  1262.35
      5               20.19   247.68                  1238.41
      6               24.52   203.96                  1223.73
      7               29.94   167.02                  1169.12
      8               34.98   142.92                  1143.39
      

      事实证明,无论并发性如何,总体吞吐量都是一致的,而且 SQLite 在 Windows 上的速度比 macOS 慢,希望这会有所帮助。


      由于 SQLite 写锁是数据库方面的,为了获得更高的 TPS,您可以将数据分区到多数据库文件:

      class MultiDBStore():
      
          def __init__(self, buckets=5):
              self.buckets = buckets
              self.conns = []
              for n in range(buckets):
                  conn = sqlite3.connect(f'kv_{n}.db', timeout=60)
                  conn.execute('pragma journal_mode=wal')
                  conn.execute('create table if not exists "kv" (key integer primary key, value integer) without rowid')
                  conn.commit()
                  self.conns.append(conn)
      
          def _get_conn(self, key):
              assert isinstance(key, int)
              return self.conns[key % self.buckets]
      
          def get(self, key):
              item = self._get_conn(key).execute('select value from "kv" where key=?', (key,))
              if item:
                  return next(item)[0]
      
          def set(self, key, value):
              conn = self._get_conn(key)
              conn.execute('replace into "kv" (key, value) values (?,?)', (key, value))
              conn.commit()
      

      我的 Mac 上的结果有 20 个分区:

      concurrency time(s) pre process TPS(r/s)    total TPS(r/s)
      1           2.07    4837.17                 4837.17
      2           2.51    3980.58                 7961.17
      3           3.28    3047.68                 9143.03
      4           4.02    2486.76                 9947.04
      5           4.44    2249.94                 11249.71
      6           4.76    2101.26                 12607.58
      7           5.25    1903.69                 13325.82
      8           5.71    1752.46                 14019.70
      

      总 TPS 高于单个数据库文件。

      【讨论】:

      • 非常感谢。因为SQLite supports an unlimited number of simultaneous readers, but it will only allow one writer at any instant in time. (reference here),是否需要在代码中添加一些内容以允许多个写入者并确保同时发生的写入操作不会相互覆盖?
      • @Basj 我添加了一个数据库分区实现。
      • @Basj ACID-compliant 是 SQLite 所做的,你最不应该担心的事情......无论如何,你可以在 wal.c 中查看 sqlite3WalBeginWriteTransaction(),作者将等待 WAL_WRITE_LOCK在一个事务开始的时候,我猜是你想看到的“锁定”部分,但完整的机制比这更复杂。
      • @Basj - “任何时候只允许一个作者”是因为 sqlite 会为你处理并发。如果一个进程正在写入,另一个尝试写入的进程不会失败,它会阻塞。您不需要为并发写入做任何事情就可以了。 This document 描述内部结构; this one about the python library 应该让你放心。
      • @Basj 是的,sqlite 会在内部为您完成。
      【解决方案4】:

      在有 redis 之前,有 Memcached(在 Windows 上工作)。 这是一个教程。 https://realpython.com/blog/python/python-memcache-efficient-caching/

      【讨论】:

      • 标题给了我一个关于作者意图的提示,“与其他 python 脚本共享一个字典”,它不需要数据存储来做到这一点。虽然多次提到数据库,但存储数据的行为可能是一个可能不需要的附带好处。
      【解决方案5】:

      我会考虑 2 个选项,都是嵌入式数据库

      SQLite

      herehere 的回答应该没问题

      伯克利数据库

      link

      Berkeley DB (BDB) 是一个软件库,旨在为键/值数据提供高性能嵌入式数据库

      它专为您的目的而设计

      BDB 可以同时支持数千个控制线程或 处理多达 256 个数据库的并发进程 TB,3 在各种操作系统上,包括大多数 类 Unix 和 Windows 系统,以及实时操作系统。

      它很强大,并且已经存在多年甚至几十年了

      提出redis/memcached/ 其他任何需要管理员参与的成熟的基于套接字的服务器 IMO 是在位于同一个盒子上的 2 个脚本之间交换数据的任务的开销

      【讨论】:

        【解决方案6】:

        CodernintyDB 可能值得探索,使用服务器版本。

        http://labs.codernity.com/codernitydb/

        服务器版本: http://labs.codernity.com/codernitydb/server.html

        【讨论】:

          【解决方案7】:

          您可以为此目的使用 python 字典。

          创建一个名为 G 的通用类或脚本,用于在其中初始化一个字典。 G 将运行 script1.py 和 script2.py 并将字典传递给两个脚本文件,在 python 中字典默认通过引用传递。这样,将使用单个字典来存储数据,并且两个脚本都可以修改字典值,可以在两个脚本中看到更改。我希望 script1.py 和 script2.py 是基于类的。它不保证数据的持久性。为了持久化,您可以在 x 间隔后将数据存储在数据库中。

          示例

          script1.py

          class SCRIPT1:
          
              def __init__(self, dictionary):
                  self.dictionary = dictionary
                  self.dictionary.update({"a":"a"})
                  print("SCRIPT1 : ", self.dictionary)
          
              def update(self):
                  self.dictionary.update({"c":"c"})          
          

          script2.py

          class SCRIPT2:
              def __init__(self, dictionary):
                  self.dictionary = dictionary
                  self.dictionary.update({"b":"b"})
                  print("SCRIPT 2 : " , self.dictionary)
          

          main_script.py

          import script1
          import script2
          
          x = {}
          
          obj1 = script1.SCRIPT1(x) # output: SCRIPT1 :  {'a': 'a'}
          obj2 = script2.SCRIPT2(x) # output: SCRIPT 2 :  {'a': 'a', 'b': 'b'}
          obj1.update()
          print("SCRIPT 1 dict: ", obj1.dictionary) # output: SCRIPT 1 dict:  {'c': 'c', 'a': 'a', 'b': 'b'}
          
          print("SCRIPT 2 dict: ", obj2.dictionary) # output: SCRIPT 2 dict:  {'c': 'c', 'a': 'a', 'b': 'b'}
          

          还要在您将运行脚本的目录中创建一个空的 _ init _.py 文件。

          另一种选择是:

          Redis

          【讨论】:

          • 感谢您的回答,请贴出代码示例,以便于理解。
          • OP 所说的“多个 Python 脚本同时运行”的意思是“有多个解释器实例”。
          • @Darkonaut 我不明白你的问题,请多解释一下
          • 这不是一个问题,但您的回答不是 OP 所要求的。代码中的所有内容都在一个解释器实例(一个进程)上按顺序运行。它只是从其他两个脚本中导入的代码,不运行其他进程,更不用说在进程之间共享一些东西了。
          【解决方案8】:

          听起来你真的需要某种数据库。

          如果 redis 不能在 windows 上工作,那么我会考虑 MongoDB。

          https://docs.mongodb.com/manual/tutorial/install-mongodb-on-windows/

          MongoDB 与 python 配合得很好,功能类似于 redis。以下是 PyMongo 的安装文档: http://api.mongodb.com/python/current/installation.html?_ga=2.78008212.1422709185.1517530606-587126476.1517530605

          另外,很多人都提到了 SQlite。我想你担心它一次只允许一个作家,但这并不是你担心的问题。我认为它的意思是,如果有两个作家,第二个将被阻止,直到第一个完成。这可能适合您的情况。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2017-08-16
            • 1970-01-01
            • 2014-04-10
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多