Python 数据共享四种方案完整实例(同进程 / JSON / Redis / HTTP)

摘要: 本文针对「a.py 常驻运行、定时刷新数据,多个 Python 文件需要读取最新值」这一常见需求,先讲清进程内存不共享的本质,再按「同进程 → 跨进程」递进给出四种可直接复制的完整方案:同进程 import 加守护线程刷新、独立进程加 JSON 文件原子写、独立进程加 Redis、独立进程加 HTTP 服务。每种方案都配有目录结构、完整代码、启动方式和优缺点分析,最后提供四种方案的对比选型表、七个高频踩坑清单和可直接套用的项目模板,帮助读者根据是否同进程、数据规模、实时性和跨机器需求快速做出选择。

适用场景:a.py 常驻运行、定时刷新数据,多个 py 文件需要读取最新的值。

本文给出 4 种可运行的完整方案,按「同进程 → 跨进程」逐步递进,直接复制即可用。


目录

    1. 先搞清楚问题本质
  • 方案一:同进程 import + 守护线程刷新

  • 方案二:独立进程 + JSON 文件原子写

  • 方案三:独立进程 + Redis

  • 方案四:独立进程 + HTTP 服务

  • 四种方案对比与选型

  • 踩坑清单


0. 先搞清楚问题本质

Python 的两个进程,内存是不共享的。 这句话决定了你只能用哪类方案。

关系 能否直接读变量 说明
同一进程,import 模块 ✅ 能 用 模块名.变量 访问,实时
两个独立的 python xxx.py 进程 ❌ 不能 必须走文件 / Redis / HTTP / socket

「a.py 定时更新」意味着它大概率是常驻进程 ,所以如果其他脚本是单独 python b.py 跑的,方案一无效,只能走方案二/三/四。

⚠️ 特别注意:python a.py 之后,另一个文件里 import a,不会 读到那个进程的内存,而是把 a.py 的代码又执行了一遍,得到的是另一个全新进程里的全新变量。这是新手最大的误解。


方案一:同进程 import + 守护线程刷新

适用场景

所有读取者都从同一个入口(比如 main.py)启动,a.py 只是被 import 的模块,不单独 python a.py。这是最简单、最快、零依赖的方案。

目录结构

text

复制代码
project/
├── a.py          # 数据源模块(后台线程定时刷新)
├── b.py          # 读取者 1
└── c.py          # 读取者 2

a.py 完整代码

python

复制代码
# a.py
import threading
import time
import random
---------- 内部状态 ----------
_lock = threading.Lock()        # 读写锁
_data = {}                      # 当前数据快照
_ready = threading.Event()      # 首次加载完成的信号
def _fetch():
"""模拟:从数据库 / 接口 / 串口 取数。替换成你自己的逻辑。"""
return {
"ts": time.time(),
"symbol": "AAPL",
"value": random.randint(1, 100),
}
def _refresh_loop(interval=5):
"""后台线程:每 interval 秒刷新一次"""
while True:
try:
new = _fetch()
with _lock:                 # 写锁
_data.clear()
_data.update(new)       # 原地更新,保持引用
_ready.set()                # 通知 get_data 可以读了
except Exception as e:
print(f"[a] 刷新失败: {e}")
time.sleep(interval)
模块被 import 时立即启动一次,且只启动一次(sys.modules 缓存保证)
_thread = threading.Thread(
target=_refresh_loop,
kwargs={"interval": 5},
daemon=True,                    # 主程序退出时自动结束
name="a-refresher",
)
_thread.start()
def get_data(wait=True, timeout=10):
"""对外唯一接口:返回最新数据的副本"""
if wait:
_ready.wait(timeout)        # 可选:等首次加载完成
with _lock:
return dict(_data)          # 返回副本,防止外部改到内部

b.py 完整代码(读取者)

python

复制代码
# b.py
import time
import a                          # 触发 a.py 顶层代码 → 启动刷新线程
for i in range(3):
print(f"[b] 第{i+1}次:", a.get_data())
time.sleep(6)                 # 6 > 5,能看到数据变化

c.py 完整代码(另一个读取者)

python

复制代码
# c.py
import time
import a
while True:
d = a.get_data()
if d:
print(f"[c] value={d['value']}  ts={d['ts']:.1f}")
time.sleep(5)

启动方式

bash

复制代码
python b.py          # 单独跑可以
python c.py          # 单独跑也可以(每个进程各自有一份 a 的线程)

如果你想让 b、c 共享同一份 a 的数据,必须让它们在同一个进程里跑,例如:

python

复制代码
# main.py
import threading
import a, b, c
t1 = threading.Thread(target=b.run, daemon=True)
t2 = threading.Thread(target=c.run, daemon=True)
t1.start(); t2.start()
t1.join(); t2.join()

(b.py / c.py 需要把逻辑包进 def run())

逐行解释

代码 作用
threading.Lock() 防止读到「写了一半」的字典
_data.clear(); _data.update(new) 原地更新,外部如果持有 _data 引用也能看到变化
daemon=True 主线程退出时不被这个死循环卡住
模块顶层启动线程 import a 只执行一次,保证线程唯一
get_data() 返回 dict(_data) 防止调用方改了返回值影响内部状态

优点 / 缺点

  • ✅ 零依赖、纳秒级访问、无序列化开销

  • ❌ 只适合同进程,跨进程无效

  • ❌ 若 a.py 被 python a.py 直接跑,其他进程 import 到的是另一份内存


方案二:独立进程 + JSON 文件原子写

适用场景

a.py 是独立常驻进程(python a.py),其他脚本也是独立进程,同一台机器,数据不大(几 MB 以内),延迟几十毫秒可接受。

目录结构

text

复制代码
project/
├── shared_data.json     # 数据文件(自动生成)
├── a.py                 # 生产者
├── reader1.py           # 消费者 1
└── reader2.py           # 消费者 2

a.py 完整代码(生产者)

python

复制代码
# a.py
import json
import os
import tempfile
import time
import random
DATA_FILE = os.path.join(os.path.dirname(os.path.abspath(file)), "shared_data.json")
def write_atomic(path, obj):
"""
原子写:先写同目录临时文件,再 os.replace 替换。
os.replace 在同一文件系统内是原子操作,读端永远看到完整文件。
"""
d = os.path.dirname(path)
fd, tmp = tempfile.mkstemp(dir=d, suffix=".tmp")
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(obj, f, ensure_ascii=False)
os.replace(tmp, path)          # ★ 原子替换
except BaseException:
try:
os.unlink(tmp)             # 异常时清理临时文件
except FileNotFoundError:
pass
raise
def fetch():
"""模拟取数,替换成你的业务逻辑"""
return {
"ts": time.time(),
"symbol": "AAPL",
"value": random.randint(1, 100),
}
if name == "main":
print(f"[a] 开始写数据 → {DATA_FILE}")
while True:
try:
data = fetch()
write_atomic(DATA_FILE, data)
print(f"[a] {time.strftime('%H:%M:%S')} 写入 value={data['value']}")
except Exception as e:
print(f"[a] 写入失败: {e}")
time.sleep(5)

reader1.py 完整代码(消费者)

python

复制代码
# reader1.py
import json
import os
import time

DATA_FILE = os.path.join(os.path.dirname(os.path.abspath(file)), "shared_data.json")

def read_shared(max_age=None): """ 读共享文件。 max_age: 数据超过多少秒算过期,None 表示不检查 """ try: with open(DATA_FILE, encoding="utf-8") as f: data = json.load(f) except FileNotFoundError: return None # a.py 还没写第一次 except json.JSONDecodeError: return None # 极端情况兜底(理论上原子写不会发生)

复制代码
if max_age is not None and time.time() - data.get("ts", 0) > max_age:
    print("[reader1] 数据过期了")
    return None
return data

while True: d = read_shared(max_age=30) if d: age = time.time() - d"ts" print(f"reader1 value={d'value'} 数据年龄={age:.1f}s") else: print("reader1 暂无数据") time.sleep(2)

reader2.py 完整代码(另一个消费者,逻辑相同)

python

复制代码
# reader2.py
import json, os, time
DATA_FILE = os.path.join(os.path.dirname(os.path.abspath(file)), "shared_data.json")
while True:
try:
with open(DATA_FILE, encoding="utf-8") as f:
d = json.load(f)
print(f"[reader2] {d['symbol']} = {d['value']}")
except FileNotFoundError:
print("[reader2] 等 a.py 首次写入...")
time.sleep(3)

启动方式(开三个终端)

bash

复制代码
python a.py         # 终端1:常驻,每 5 秒写一次
python reader1.py   # 终端2
python reader2.py   # 终端3

关键解释

为什么必须「原子写」?

如果你这样写:

python

复制代码
with open("data.json", "w") as f:
    json.dump(data, f)      # ❌ 危险

在 json.dump 执行到一半时,读端去 json.load,会读到截断的 JSON ,直接 JSONDecodeError。数据越大越容易触发。

正确做法三步:

  1. 在同目录创建临时文件(tempfile.mkstemp(dir=d))

  2. 写入完整内容

  3. os.replace(tmp, path) ------ 这一步在 POSIX / Windows 上都是原子的

读端要么读到旧文件,要么读到新文件,永远不会读到半个文件。

为什么要同目录?

os.replace 跨文件系统不是原子的(会退化成复制)。所以临时文件必须和目标文件在同一目录。

读取延迟

写端每 5 秒写一次,读端每 2 秒读一次,最坏延迟 ≈ 7 秒。如果要更快,就缩短两边的间隔,但会占用更多 CPU / IO。

优点 / 缺点

  • ✅ 零依赖、单文件、可人肉排查(直接 cat shared_data.json)

  • ✅ 进程崩溃不影响历史数据

  • ❌ 有轮询延迟

  • ❌ 数据大了(> 几十 MB)序列化开销明显 → 换 pickle 或 msgpack

  • ❌ 只适合同机,不能跨机器

如果要存 DataFrame / numpy

把 json.dump 换成 pickle.dump,读端 pickle.load,原子写逻辑完全一样:

python

复制代码
import pickle
def write_atomic_pkl(path, obj):
    d = os.path.dirname(path)
    fd, tmp = tempfile.mkstemp(dir=d, suffix=".tmp")
    with os.fdopen(fd, "wb") as f:
        pickle.dump(obj, f, protocol=pickle.HIGHEST_PROTOCOL)
    os.replace(tmp, path)

方案三:独立进程 + Redis

适用场景

  • 多个消费进程 / 多台机器都要读

  • 需要实时性(毫秒级)

  • 需要「最新值」语义(永远读到最后一次写的)

前置准备

bash

复制代码
pip install redis
# 本地装 Redis:
#   Docker:  docker run -d -p 6379:6379 redis
#   Ubuntu:  sudo apt install redis-server
#   Mac:     brew install redis && brew services start redis

目录结构

text

复制代码
project/
├── a.py          # 生产者:定时 SET
├── reader1.py    # 消费者 1
└── reader2.py    # 消费者 2

a.py 完整代码(生产者)

python

复制代码
# a.py
import json
import time
import random
import redis
r = redis.Redis(host="127.0.0.1", port=6379, db=0, decode_responses=True)
KEY = "app:data"
def fetch():
return {
"ts": time.time(),
"symbol": "AAPL",
"value": random.randint(1, 100),
}
if name == "main":
while True:
data = fetch()
# SET 本身是原子的,读端要么读到旧值,要么读到新值
r.set(KEY, json.dumps(data, ensure_ascii=False))
# 可选:设个过期时间,防止 a.py 挂了以后读到「僵尸数据」
# r.expire(KEY, 60)
print(f"[a] 写入 {data}")
time.sleep(5)

reader1.py 完整代码(消费者)

python

复制代码
# reader1.py
import json
import time
import redis
r = redis.Redis(host="127.0.0.1", port=6379, db=0, decode_responses=True)
KEY = "app:data"
while True:
raw = r.get(KEY)
if raw is None:
print("[reader1] 暂无数据")
else:
d = json.loads(raw)
age = time.time() - d["ts"]
print(f"[reader1] value={d['value']}  年龄={age:.1f}s")
time.sleep(2)

reader2.py 完整代码(用 pub/sub 实时推送,不是轮询)

如果你希望「a.py 一更新,reader 立刻收到」,用 Redis Pub/Sub:

python

复制代码
# a.py(发布者,追加在 set 之后)
r.publish("app:data:channel", json.dumps(data, ensure_ascii=False))

python

复制代码
# reader2.py(订阅者)
import json
import redis
r = redis.Redis(host="127.0.0.1", port=6379, db=0, decode_responses=True)
p = r.pubsub()
p.subscribe("app:data:channel")
print("[reader2] 已订阅,等待推送...")
for msg in p.listen():
if msg["type"] != "message":
continue
d = json.loads(msg["data"])
print(f"[reader2] 收到推送: {d}")

Pub/Sub vs GET:

  • GET 是「拉」模式,读端主动查,有轮询延迟,但能读到最新值(即使刚启动)

  • Pub/Sub 是「推」模式,零延迟,但离线期间的消息会丢,刚启动时读不到历史值

  • 生产上常见做法:启动时先 GET 一次拿当前值,再 SUB 接收增量

启动方式

bash

复制代码
# 先确保 Redis 在跑
redis-cli ping     # 返回 PONG 就 OK
python a.py
python reader1.py
python reader2.py

关键解释

点 说明
decode_responses=True 自动把 bytes 解码成 str,省得手动 .decode()
r.set() 原子性 Redis 单命令原子,不会有「读到半截 JSON」的问题
r.expire(KEY, 60) 防止 a.py 挂了以后消费者读到很久以前的旧值。加上后,读端拿到 None 就知道数据源挂了
Pub/Sub 无持久化 订阅者掉线期间的消息会丢,不适合「必须拿到每一条」的场景(那要用 Redis Stream / Kafka)

优点 / 缺点

  • ✅ 实时、原子、可跨机器、多消费者天然支持

  • ✅ 支持过期、数据结构丰富

  • ❌ 需要额外部署 Redis 服务

  • ❌ 引入了外部依赖(挂了就全挂)


方案四:独立进程 + HTTP 服务

适用场景

  • 跨机器 / 跨语言(前端、Go、Java 都能调)

  • 需要鉴权、限流、监控

  • 想把 a.py 变成一个「数据服务」

前置准备

bash

复制代码
pip install flask requests

目录结构

text

复制代码
project/
├── a.py          # 数据服务:Flask 提供 GET /data
└── reader.py     # 客户端:requests 请求

a.py 完整代码(Flask 服务 + 后台刷新线程)

python

复制代码
# a.py
import threading
import time
import random
from flask import Flask, jsonify

app = Flask(name)

---------- 后台刷新的数据 ----------

_lock = threading.Lock() _data = {}

def _fetch(): return { "ts": time.time(), "symbol": "AAPL", "value": random.randint(1, 100), }

def _refresh_loop(interval=5): while True: try: new = _fetch() with _lock: _data.clear() _data.update(new) except Exception as e: print(f"a 刷新失败: {e}") time.sleep(interval)

---------- HTTP 接口 ----------

@app.route("/data", methods="GET") def get_data(): with _lock: return jsonify(dict(_data))

@app.route("/health", methods="GET") def health(): return {"status": "ok"}

if name == "main": # 后台刷新线程(daemon,Flask 退出时自动结束) threading.Thread(target=_refresh_loop, kwargs={"interval": 5}, daemon=True, name="refresher").start()

复制代码
# 开发用 app.run;生产用 gunicorn / uvicorn
app.run(host="0.0.0.0", port=8000, threaded=True)</pre>

reader.py 完整代码(HTTP 客户端)

python

复制代码
# reader.py
import time
import requests
URL = "http://127.0.0.1:8000/data"
while True:
try:
resp = requests.get(URL, timeout=2)
resp.raise_for_status()
d = resp.json()
age = time.time() - d["ts"]
print(f"[reader] value={d['value']}  年龄={age:.1f}s")
except requests.RequestException as e:
print(f"[reader] 请求失败: {e}")
time.sleep(2)

启动方式

bash

复制代码
python a.py         # 启动服务,监听 8000
python reader.py    # 另一个终端

用 curl 也能测

bash

复制代码
curl http://127.0.0.1:8000/data
# {"symbol":"AAPL","ts":1729900000.123,"value":42}

生产部署(不要用 app.run)

app.run() 是开发服务器,单进程、无并发保护。生产环境用 gunicorn:

bash

复制代码
pip install gunicorn
注意:gunicorn 多 worker 时,每个 worker 会各自跑一个刷新线程 → 数据不一致!
解决:把刷新线程放到单独进程,或改用 --workers 1
gunicorn -w 1 -b 0.0.0.0:8000 a:app

⚠️ 重要坑 :gunicorn -w 4 会启动 4 个 worker 进程,a.py 的顶层刷新线程会跑 4 份,_data 也各有各的。此时应该:

  • 用 -w 1(单 worker,多线程处理请求),或

  • 把「刷新 + 存储」独立成单独进程,Flask 只做读取(数据从 Redis / 文件读)

优点 / 缺点

  • ✅ 语言无关、跨机器、天然支持鉴权 / 限流 / 监控

  • ✅ 客户端极简单(requests.get)

  • ❌ 需要跑一个 Web 服务,部署稍重

  • ❌ 有网络开销(本机 ~1ms,跨机器看网络)

  • ❌ gunicorn 多 worker 时需要额外处理数据一致性


四种方案对比与选型

维度 方案一 import 方案二 JSON 文件 方案三 Redis 方案四 HTTP
进程关系 必须同进程 跨进程、同机 跨进程、可跨机 跨进程、可跨机
依赖 无 无 Redis 服务 Flask / gunicorn
延迟 纳秒 秒级(轮询) 毫秒 毫秒~几十毫秒
实时性 最高 低 高 高
多消费者 天然支持 天然支持 天然支持 天然支持
跨语言 ❌ 半(读 JSON) ✅ ✅
可调试性 差 好(看文件) 中(redis-cli) 好(curl)
代码复杂度 低 低 中 中

一句话选型

text

复制代码
a.py 和其他 py 在同一个进程里?
├─ 是 → 【方案一】import + 守护线程(最简单)
└─ 否 → 数据要共享给谁?
        ├─ 只有本机几个 py,数据不大   → 【方案二】JSON 原子写
        ├─ 本机/多机,要实时           → 【方案三】Redis
        └─ 跨语言 / 对外提供服务        → 【方案四】HTTP

踩坑清单

坑 1:from a import data 拿到的是快照

python

复制代码
from a import data       # ❌ 快照,之后 a.data 变了这里不会变
print(data)
import a                 # ✅ 用属性访问
print(a.data)

原理:from ... import ... 是在导入那一刻把对象绑定 到当前模块的全局命名空间,之后 a.data = ... 重新赋值不会同步。

坑 2:python a.py 之后别的进程 import a 拿不到数据

bash

复制代码
# 终端1
python a.py             # 进程 A,内存里有 data
终端2
python b.py             # import a → 又执行一遍 a.py → 进程 B,全新内存

两个进程内存独立,方案一在跨进程场景下完全无效。必须走方案二/三/四。

坑 3:写文件不用原子替换,读端随机报错

python

复制代码
open("data.json", "w").write(json.dumps(d))   # ❌ 读端可能读到半截

必须:

python

复制代码
tmp = "data.json.tmp"
with open(tmp, "w") as f:
    f.write(json.dumps(d))
os.replace(tmp, "data.json")                   # ✅ 原子

坑 4:daemon=True 的线程会随着主线程退出被杀掉

python

复制代码
t = threading.Thread(target=loop, daemon=True)

主程序 sys.exit() 时,daemon 线程不会执行 finally / 清理逻辑。如果刷新线程里有资源要释放(比如关数据库连接),用 daemon=False + 主线程里主动 join,或者注册 atexit。

坑 5:Redis 里存了数据但 a.py 挂了,读端一直拿到旧值

用 expire 兜底:

python

复制代码
r.set(KEY, json.dumps(data))
r.expire(KEY, 60)       # 60 秒不刷新就自动删除

读端拿到 None 就知道数据源挂了,而不是拿到三天前的数据还以为是真的。

坑 6:gunicorn 多 worker 导致刷新线程跑多份

bash

复制代码
gunicorn -w 4 a:app     # ❌ 4 个进程各自跑一份刷新线程,_data 各不一致
gunicorn -w 1 a:app     # ✅ 单进程多线程

或者把刷新逻辑放到独立进程,Web 服务只做读取(从 Redis / 文件读)。

坑 7:pickle / json 每次都全量序列化,数据大时 CPU 爆炸

数据量 > 10 MB 时考虑:

  • msgpack(比 JSON 小、快)

  • pyarrow / parquet(列式,适合 DataFrame)

  • multiprocessing.shared_memory(同一台机器、零拷贝)


完整项目模板

如果你想直接拿来用,推荐结构:

复制代码
project/
├── a.py                 # 数据生产者(常驻)
├── readers/
│   ├── reader1.py
│   └── reader2.py
├── shared_data.json     # 方案二产物
└── requirements.txt

requirements.txt(按需选择):

复制代码
# 方案二
# (无依赖)
方案三
redis>=5.0
方案四
flask>=3.0
requests>=2.31
gunicorn>=21.0      # 生产部署

一句话总结 :同进程用方案一,本机小数据用方案二,实时多机用方案三,对外服务用方案四。判断标准永远是「是不是同一个进程」,这一条决定了 90% 的选型。

相关推荐
略略略咯咯1 小时前
啊啊啊啊啊
java·服务器·数据库
奇牙coding1 小时前
GPT-5.5 报 401 invalid_api_key 怎么办?换密钥前按 4 层排查请求头、项目权限和端点
数据库·gpt·ios·ai
IvorySQL1 小时前
PostgreSQL 日报| WAL summarizer 断言失败(10 月 26 日)
数据库·postgresql·区块链
王仔小奶1 小时前
慢SQL治理与性能调优:DBA如何用Shell/Python提升运维效率?
python·sql·dba
用户0332126663671 小时前
使用 Python 快速在 PowerPoint 中生成表格【教程】
python
夜晚回家2 小时前
Python 身份运算符详解 | is 与 == 到底差在哪
python
fengye2071612 小时前
cpython框架结构
python
用户91294046848422 小时前
大模型返回 JSON 后,先过这道 Python 校验
python
这个DBA有点耶2 小时前
2026年国产数据库“实际用户增长最快”核验指南:口径优先级、可查数据与选型动作清单
数据库·mysql·架构