乐观锁和Redis分布式锁怎么用来保证服务器之间的互斥访问,实际应用中有哪些坑和注意点
- 问答
- 2025-12-26 17:01:33
- 1
乐观锁和Redis分布式锁是两种常见的用来解决在分布式系统、也就是多台服务器环境下,对共享资源进行互斥访问的技术,它们的思想和实现方式完全不同,适用的场景也不同。
乐观锁

乐观锁,顾名思义,它的心态很“乐观”,它假设大部分情况下,多个服务器同时去修改同一条数据的情况不会发生,所以允许多个服务器同时读取数据,但在更新数据的时候,会去检查一下从读取数据到准备更新这段时间里,这个数据有没有被别的服务器修改过,如果没被修改过,就正常更新;如果已经被修改了,那么本次更新就放弃,通常会选择重试或者告诉用户操作失败。
它的核心实现很简单,通常是在数据库的数据表里加一个版本号字段(比如叫 version),工作流程是这样的:

- 服务器A从数据库读取某条数据,同时记下当前的版本号,
version=1。 - 服务器B也读取了同一条数据,也拿到了
version=1。 - 服务器A计算完新的数据后,准备更新回数据库,它执行的更新语句不会是简单的
UPDATE table SET data=new_data WHERE id=123,而是会带上版本号条件:UPDATE table SET data=new_data, version=version+1 WHERE id=123 AND version=1。 - 这个SQL语句在执行时,数据库会检查
id=123这条记录的当前版本号是否还是1,如果是,说明这期间没有别人动过,于是更新成功,同时版本号加1变成2。 - 紧接着,服务器B也计算完了,它执行同样的更新语句:
UPDATE ... WHERE id=123 AND version=1,但此时数据库中的版本号已经被服务器A更新为2了,所以这个语句找不到符合条件的记录,更新失败,服务器B就知道自己的数据已经过时了,需要重新读取最新的数据(现在版本是2,数据是A更新后的)再进行业务逻辑计算和更新尝试。
乐观锁在实际应用中的坑和注意点:
- 高并发下的冲突问题:如果真的是一个热点数据,比如秒杀场景下的商品库存,会有大量请求同时读、同时更新,这会导致大部分更新语句都会因为版本号对不上而失败,也就是“写冲突”很高,大量请求会不断重试,给数据库带来压力,用户体验也不好,所以乐观锁不适合这种写操作非常频繁的场景。
- 业务逻辑需要支持重试:使用乐观锁,更新失败是常态化的预期结果,你的业务代码必须能处理这种失败,比如要有重试机制,但重试次数不能无限,通常要设置一个上限,否则可能造成死循环。
- ABA问题:这是一个经典问题,假设服务器A读到版本号是V1,然后CPU时间片用完被挂起,在此期间,服务器B把数据从A改成了B(版本号变成V2),然后又有一个服务器C把数据从B改回了A(版本号变成V3),这时服务器A恢复执行,它一看数据还是A,版本号虽然变了,但数据内容和它读的时候一样,它可能就误以为数据没被改过,然后用自己的版本号条件(V1)去更新,居然可能会成功(取决于实现,如果只比较值不比较版本号就会成功),这就覆盖了服务器C的修改,解决ABA问题通常需要更复杂的机制,比如使用递增的版本号或时间戳,而不仅仅是比较数据值。
Redis分布式锁

Redis分布式锁的心态是“悲观”的,它认为冲突很可能会发生,所以在一开始就要阻止冲突,它的做法是:当一台服务器要操作共享资源前,必须先想办法在Redis里占一个“坑”,这个“坑”就是锁,只有成功占到坑的服务器才能继续执行后续操作,其他服务器只能等待这个锁被释放。
在Redis中,这个“坑”通常是用一个Key-Value对来表示,最基础的实现命令是 SETNX(SET if Not eXists),意思是只有当这个key不存在时,才能设置成功,比如服务器A执行 SETNX lock_key 1,如果返回成功,表示它拿到了锁,它操作完资源后,再执行 DEL lock_key 把锁删除(释放),这样其他服务器就能来抢了。
Redis分布式锁在实际应用中的坑和注意点非常多,远比乐观锁复杂:
- 死锁问题:如果服务器A拿到锁之后,还没来得及释放锁,自己突然宕机了怎么办?那么这个锁就永远存在于Redis中,其他服务器再也拿不到锁,系统就卡死了,这是最致命的问题,为了解决它,我们必须给锁设置一个过期时间,即使用
SET lock_key 1 EX 10 NX这样的命令(或组合命令),在设置锁的同时给它一个10秒后自动过期的属性,这样即使服务器宕机,锁也会在10秒后自动释放。 - 锁误删问题:光设置过期时间还不够,假设服务器A拿到锁,设置了30秒过期,但它的业务逻辑执行了40秒(可能因为GC停顿或网络延迟),在第35秒时,锁因为过期自动释放了,此时服务器B成功拿到了锁,到了第40秒,服务器A的逻辑执行完了,它依然会去执行删除锁的操作,结果就是把服务器B刚创建的锁给删掉了!解决方案是,在设置锁的时候,其Value不能是一个简单的固定值(如1),而应该是一个全局唯一的值,比如UUID,服务器在删除锁的时候,要先判断当前锁的Value是不是自己设置的那个UUID,只有是自己的才能删,这个“判断+删除”操作必须是原子的,需要用Lua脚本实现,因为Redis执行Lua脚本是原子性的。
- 锁续期问题:针对上面锁超时释放的问题,除了确保业务逻辑执行时间要短于锁过期时间外,更可靠的方案是使用“看门狗”机制进行锁续期,即,在持有锁的服务器上启动一个后台线程,定时(比如每隔过期时间的1/3)去检查锁是否还存在且还是自己的,如果是,就延长锁的过期时间,Java中常用的Redisson客户端就内置了这个功能。
- Redis本身的高可用问题:如果你用的是单机Redis,如果这台Redis宕机了,那么整个分布式锁服务就不可用了,所以生产环境一定要用Redis哨兵(Sentinel)模式或集群(Cluster)模式来保证高可用,但这又引入了新的问题,比如在主从切换的瞬间,可能会发生锁丢失:服务器A从旧主节点拿到了锁,但锁还没来得及同步到新的主节点,旧主节点就宕机了,新主节点上没有这个锁,服务器B又能成功获取锁,导致互斥失效,对于要求绝对严格的场景,可能需要使用RedLock算法(尝试在大多数Redis节点上都获取锁),但这个算法本身也存在争议,实现复杂且性能较低。
- 乐观锁 简单轻量,通过版本号控制,适合读多写少、冲突不那么剧烈的场景,比如CMS内容更新、用户信息修改等,它的核心代价是冲突后的处理(重试或报错)。
- Redis分布式锁 功能强大,能实现强互斥,适合写操作频繁、需要明确排队等待的场景,比如秒杀扣库存、重要订单处理,它的核心代价是维护锁的复杂性(过期时间、误删、续期、集群可靠性),并且引入了一个外部组件Redis,增加了系统复杂度。
选择哪种方案,完全取决于你的业务场景对数据一致性的要求程度以及并发压力的类型。
本文由寇乐童于2025-12-26发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://haoid.cn/wenda/68904.html
