关于Redis事务(WATCH&MULTI)模拟原子INCR命令的并发正确性问题咨询
关于Redis事务(WATCH&MULTI)模拟原子INCR命令的并发正确性问题咨询
最近我尝试用Redis的WATCH和MULTI命令来模拟INCR的原子递增操作,本以为能实现同样的效果,但在并发场景下发现了问题——最终的数值总是不符合预期,想跟大家拆解下这个情况。
问题核心
我原本的思路是这样的:通过WATCH监控目标key,获取当前值后加1,再用MULTI开启事务执行SET操作,以此模拟原子递增。但实际测试后发现,GET和MULTI之间并不是原子执行的,并发请求进来时,多个客户端可能同时拿到同一个旧值,导致后续的事务执行出现覆盖,最终结果失效。
对应的核心命令流程如下:
WATCH mykey val = GET mykey val = val + 1 MULTI SET mykey $val EXEC
对比测试代码与结果
我用Python asyncio写了两组并发测试代码,分别用原生INCR和WATCH&MULTI方案,依赖python3.8和asyncio-redis==0.16.0。
1. 原生原子INCR方案
这个方案下,10个并发worker执行递增,最终结果正确为10:
import asyncio import asyncio_redis async def worker(_id): global cache_pool r = await cache_pool.incr('some_key') print(f"worker_id: {_id} - {r}") async def connect(): global cache_pool cache_pool = await asyncio_redis.Pool.create(host='localhost', port=6379, poolsize=10) if __name__ == '__main__': loop = asyncio.get_event_loop() loop.run_until_complete(connect()) for i in range(10): loop.create_task(worker(i)) loop.run_forever()
2. WATCH&MULTI模拟方案
这个方案运行后,会出现部分worker的事务执行覆盖了刚递增的值,导致最终结果不符合预期:
import asyncio import asyncio_redis async def worker(_id): global cache_pool while True: try: await cache_pool.watch(['some_key']) value = await cache_pool.get('some_key') value = int(value or '0') value += 1 tr = await cache_pool.multi() await tr.set('some_key', str(value)) await tr.exec() except asyncio_redis.exceptions.TransactionError: pass else: print(f"worker: {_id} got optimistic lock - {value}") break async def connect(): global cache_pool cache_pool = await asyncio_redis.Pool.create(host='localhost', port=6379, poolsize=10) if __name__ == '__main__': loop = asyncio.get_event_loop() loop.run_until_complete(connect()) for i in range(10): loop.create_task(worker(i)) loop.run_forever()
出现问题的原因在于:当多个worker同时对some_key执行WATCH后,会同时获取到同一个初始值;第一个worker完成事务执行修改key后,其他worker的事务本应因为WATCH的key被修改而触发TransactionError并重试,但实际测试中还是会有部分worker串行进入事务块,覆盖掉刚更新的数值,导致最终结果错误。
备注:内容来源于stack exchange,提问作者Ali Ebrahimi
相关产品推荐
相关产品推荐

