Redis Pub/Sub无法解决协作编辑的一致性与冲突问题,因其不保序、不持久、无ACK;需结合OT/CRDT、Redis Streams或乐观锁等方案。

Redis 的发布订阅(Pub/Sub)能实现实时状态广播,但不能单独解决协作编辑中的数据一致性与冲突问题。它只负责“通知变更”,不保证顺序、不存储历史、不处理并发写入冲突。真正在多端协同编辑中落地,必须搭配状态同步协议(如 OT 或 CRDT)、服务端协调逻辑,或改用 Redis Streams + 消费组 + 乐观锁组合。
为什么直接用 PUBLISH/SUBSCRIBE 同步编辑状态会出问题
Pub/Sub 是纯内存、即发即弃的广播通道:消息不持久、无 ACK、无重试、不保序。在协作编辑场景下,这会导致几个典型故障:
- 客户端断连重连后收不到离线期间的变更,状态直接脱节
- 两个用户同时修改同一字段(比如光标位置或某段文本),
PUBLISH只是把“我改了”喊出去,没人判断“谁改得对” -
pubsub.listen()是阻塞式长轮询,单线程 Python 客户端里一旦卡在监听,其他逻辑(如本地编辑响应)就全被堵住 - 频道名硬编码或动态拼接不当,容易出现订阅错频道、漏频道、重复订阅(导致重复处理)
redis-py 中 pubsub 实例必须为每个连接独占
一个 redis.Redis 连接不能复用于命令操作和 Pub/Sub 监听——因为 pubsub.listen() 会接管该连接的 socket,后续所有 r.set() r.get() 都会失败,报错 ConnectionError: Connection not ready。
正确做法是分开建连:
import redis
<h1>普通命令连接(读写 key)</h1><p>r = redis.Redis(host='localhost', port=6379, db=0)</p><h1>Pub/Sub 专用连接(只 listen)</h1><p>ps = redis.Redis(host='localhost', port=6379, db=0).pubsub()
ps.subscribe('doc:123:state')</p><h1>注意:不能再用 r.pubsub() 或 ps.set() 等混合调用</h1><p></p>
- 别在同一个
redis.Redis实例上调用pubsub()后还执行r.get(),必然报错 - 多线程环境下,每个线程应持有自己的
pubsub实例,不要共享 - 使用连接池时,
redis.ConnectionPool默认不支持 Pub/Sub 复用,需显式配置max_connections并预留专用连接
冲突必须在应用层收敛,Redis 不提供自动合并能力
Redis 本身没有“文档协同”语义。当 A 和 B 同时编辑同一份内容,仅靠 PUBLISH 'doc:123' '{"op":"insert","pos":10,"text":"x"}' 和 PUBLISH 'doc:123' '{"op":"delete","range":[8,12]}',接收方无法判断哪个先发生、是否可合并、是否已过期。
可行的收敛方式有:
- 引入版本号或时间戳字段(如
"v": 123456789),客户端收到消息后比对本地版本,丢弃旧版本更新 - 服务端维护操作日志(推荐用
Redis Streams替代 Pub/Sub),用XADD写入带 ID 的事件,消费者用XREADGROUP拉取并按 ID 严格排序处理 - 对关键状态(如用户在线状态、光标位置)采用 Hash 结构 +
HSET doc:123:cursor userA '{"x":120,"y":45}',再用PUBLISH doc:123:cursor_update userA通知变更,避免直接广播原始值 - 写操作前用
WATCH doc:123:version+MULTI/EXEC做乐观锁,失败则拉取最新状态重算差异再重试
真正适合协作编辑的替代方案:从 Pub/Sub 切到 Streams
如果业务需要可靠、有序、可追溯的协同状态同步,Redis Streams 是更合理的选择。它支持:
- 消息持久化(重启不丢)
- 消费者组(
XGROUP)实现多实例负载分担或广播模式 -
XREADGROUP支持未确认消息重发(XPENDING查看) - 天然支持事件溯源:每条操作都是带 ID 的原子记录,便于回放、调试、审计
示例结构:
# 写入编辑操作
r.xadd('stream:doc:123', {'op': 'insert', 'user': 'u1', 'pos': 5, 'text': 'hello'}, maxlen=1000)
<h1>订阅者从头开始读(或从 $ 读最新)</h1><p>r.xreadgroup('group1', 'consumer1', {'stream:doc:123': '>'}, count=1)
</p>
注意:Streams 不是 Pub/Sub 的升级版,而是不同定位——前者是消息队列,后者是广播信道。选错会踩深坑:用 Pub/Sub 做协同,就像用广播喇叭指挥手术室,声音大但没法确认谁听到了、谁执行了、谁执行错了。
最易被忽略的一点:Pub/Sub 的“实时”是以牺牲可靠性换来的。线上环境网络抖动、客户端 GC 暂停、Python GIL 阻塞都可能导致 listen() 卡住数秒甚至更久,此时变更消息已在服务端丢弃。要真正稳住协同体验,得接受“稍延迟但不错乱”的设计,而不是迷信“实时即正确”。











