Python实现Redis三种CAS事务操作及性能对比

月亮慢半拍
月亮慢半拍
发布时间:2026-10-13| 1人浏览过
原创

本文详解Python中实现Redis CAS(Compare and Set)的三种方案:基于Pipeline的乐观锁、基于同步装饰器的悲观锁以及Lua脚本原子操作。通过多线程并发测试对比发现,高竞争场景下乐观锁性能急剧下降,而Lua脚本因原子执行特性,在50线程1000次操作下表现最优,显著优于其他两种锁机制。

CAS(Compare and Set,比较并设置)是一种典型的事务操作机制。

简而言之,事务的核心目的是在存取数据库中同一数据时,确保操作的隔离性与原子性,从而维持数据的一致性。

传统关系型数据库如MySQL主要依靠加锁机制来保证数据一致性,典型代表是悲观锁。例如,在执行SELECT FOR UPDATE时,MySQL会对指定数据行加锁,直到事务提交(COMMIT)或回滚(ROLLBACK)。若在此期间有其他事务尝试写入该数据,后续事务将被阻塞,直至前一个事务结束。这种机制的弊端在于:持有锁的事务执行越慢,等待解锁的事务阻塞时间越长;此外,还容易引发死锁问题(此前文章已详细讲解死锁成因)。

本文将介绍三种在Redis中实现CAS事务的方法,并解决以下虚拟场景问题:维护一个数值,若该值小于当前时间,则将其更新为当前时间;若该值大于当前时间,则将其更新为当前时间加上30。在单线程环境下,基础代码如下:

# 初始化
r = redis.Redis()
if not r.exists("key_test"):
  r.set("key_test", 0)

def inc():
  count = int(r.get('key_test')) + 30 #1
  # 如果值比当前时间小,则设置为当前时间
  count = max(count, int(time.time())) #2
  r.set('key_test', count) #3
  return count

这段代码在单线程环境中运行良好,但显然无法直接移植到多线程或多进程环境。例如,当进程A和进程B同时执行到#1处时,两者获取了相同的count值,随后执行#2和#3,会导致count值总共只增加了30,而非预期的60。为了在多进程环境下正确运行,我们需要引入其他机制。

py-redis库本身提供了事务操作支持。

Redis提供了几条与事务相关的命令:MULTI、EXEC和WATCH。利用这些命令,可以将多个命令打包,确保它们一次性、按顺序执行,且中间不会被其他命令插入打断。事务从MULTI命令开始,在调用EXEC后触发执行。此外,WATCH命令用于监视任意数量的键;如果在调用EXEC执行事务期间,被监视的任一键被修改,整个事务将不会执行。

以下是使用Redis原生事务解决CAS问题的代码示例。

class CasNormal(object):
  def __init__(self, host, key):
    self.r = redis.Redis(host)
    self.key = key
    if not self.r.exists(self.key):
      self.r.set(self.key, 0)

  def inc(self):
    with self.r.pipeline() as pipe:
      while True:
        try:
          #监视一个key,如果在执行期间被修改了,会抛出WatchError
          pipe.watch(self.key)
          next_count = 30 + int(pipe.get(self.key))
          pipe.multi()
          if next_count < int(time.time()):
            next_count = int(time.time())
          pipe.set(self.key, next_count)
          pipe.execute()
          return next_count
        except WatchError:
          continue
        finally:
          pipe.reset()

代码逻辑并不复杂,引入了前文提到的MULTI、EXEC和WATCH命令。熟悉事务操作的读者可以很容易看出,这是一种乐观锁的实现方式(我们假设没有竞争发生,每次获取数据时不加锁,只有在真正有人修改时才处理冲突)。然而,乐观锁在高并发场景下往往显得力不从心,文末的性能对比数据将验证这一点。

接下来介绍基于Redis实现的悲观锁方案。

悲观锁是一种保守策略,每次获取数据时都假设他人也会同时获取,因此先加锁,使用完毕后再释放。Redis本身未直接实现悲观锁,但我们可以利用Redis特性构建一个悲观锁机制。

有了悲观锁的支持,我们的操作便有了保障。基于上述代码,只需添加@synchronized装饰器即可保证同一时间只有一个进程执行。以下是基于悲观锁的解决方案代码。

lock_conn = redis.Redis("localhost")

class CasLock(object):
  def __init__(self, host, key):
    self.r = redis.Redis(host)
    self.key = key
    if not self.r.exists(self.key):
      self.r.set(self.key, 0)

  @synchronized(lock_conn, "lock", 10)
  def inc(self):
    next_count = 30 + int(self.r.get(self.key))
    if next_count < int(time.time()):
      next_count = int(time.time())
    self.r.set(self.key, next_count)
    return next_count

代码量明显减少(得益于引入了synchronized装饰器)。

第三种方案是基于Lua脚本实现。

前两种方法均依赖锁机制,而锁的实现必然涉及竞争处理,区别仅在于竞争发生时的处理策略不同。使用Redis Lua脚本实现CAS操作,可以直接将该操作视为一个<b>原子操作</b>。

我们知道,Redis的一系列操作本身是原子的,且Redis会按顺序执行所有接收到的命令。具体代码如下:

class CasLua(object):
  def __init__(self, host, key):
    self.r = redis.Redis(host)
    self.key = key
    if not self.r.exists(self.key):
      self.r.set(self.key, 0)
    self._lua = self.r.register_script("""
    local next_count = redis.call('get',KEYS[1]) + ARGV[1]
    ARGV[2] = tonumber(ARGV[2])
    if next_count < ARGV[2] then
      next_count = ARGV[2]
    end
    redis.call('set',KEYS[1],next_count)
    return tostring(next_count)
        """)

  def inc(self):
    return int(self._lua([self.key], [30, int(time.time())]))

此处先注册了该脚本,后续可直接调用。关于Redis Lua脚本的详细介绍文章众多,感兴趣者可自行搜索,此处不再赘述。

以下是三种方案的性能对比测试。

本次测试较为简单(但足以体现效果差异),测试环境为本地开发机,数据仅供参考数量级。

分别测试了三种操作在单线程、5线程、10线程和50线程情况下,执行1000次操作的耗时,结果如下:

     optimistic Lock pessimistic lock  lua
1thread       0.43       0.71 0.35
5thread       5.80       3.10 0.62
10thread      17.80       5.60 1.30
50thread      245.00       29.60 6.50

依次对应的是:Redis原生事务实现的乐观锁、基于Redis实现的悲观锁以及Lua脚本实现。

在比较悲观锁和乐观锁之前,需说明一点:本次测试对乐观锁并不完全公平,因为乐观锁的设计初衷是假设并发冲突较少。在单线程环境下,悲观锁表现较差,原因在于多了一次Redis网络交互。随着线程数增加,悲观锁性能逐渐提升,毕竟其设计初衷即为解决高并发竞争环境。在50线程场景下,乐观锁单次操作耗时高达0.245秒,性能极差,若应用于生产环境,几乎不可用。

至于Lua脚本的性能,表现优异,耗时几乎呈线性增长(在50线程场景下,完成1000次操作的总耗时为6.5秒,即在6.5秒内完成了50 * 1000次CAS操作)。

以上测试均在本地Redis环境下进行。若Redis部署在远程服务器,网络交互时间将增加,此时Lua脚本的优势将更加显著。希望对大家的学习有所帮助,也欢迎大家支持本站。

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。