type
status
date
slug
summary
tags
category
icon
password
上节说到,使用锁可以解决多线程并发的问题,但如果遇到服务集群的问题,那么锁就不好使了。由此产生的分布式锁的问题也就来了。
对于集群的情况下,每个jvm 都是单独的锁监视器,要想实现集群下的并发问题,需要一个所有服务共享的锁监视器。
这个跟 session 共享的问题就很像了,不用服务自带的,用第三方的,所有服务都从 redis里获取数据,就实现了数据共享。

分布式锁的实现
使用 setnx 的特性可以得到互斥锁,如果不存在才能设置 key,存在了不能 set 成功。

image-20230726155431071
获取锁:setnx,释放锁:del。但是如果设置锁之后,没来得及执行 del,redis 就宕机了,那么这个锁就无法释放了。所以为了以防万一,可以给锁设置过期时间。
实现 redis 锁
修改一人一单业务实现
使用 redis 获取锁,解决分布式锁遇到的问题。
误删其他线程的锁
如下图,线程一因为阻塞,redis超时释放锁了,线程二会获取锁进来执行业务,然后线程一醒来执行业务完成之后删除 key,删除的是线程二的锁。此时线程二正在运行,因为锁没了,线程三也可以拿到锁,去执行。以此类推,也造成了线程不安全。

解决方案
释放锁的时候要释放本线程的锁,key加入线程 id,线程 id 是 jvm 生成的,是逐渐递增的。如果是集群的情况下,可能会产生相同的线程 id,也会导致误删除。所以应该使用 redis 生成全局 id或者 UUID,以这个值作为唯一 id,加入 key 中。
分布式锁的原子性问题
上面的方案是不是就没问题了呢,还不是,还存在一些问题。
针对上面误删的问题。如果在释放锁的时候,已经判断完了,正在删除的时候发生了阻塞,比如 发生了GC 垃圾回收的STW。阻塞的时候发生了锁的超时释放,接着第二个线程拿到了锁,执行业务,阻塞结束了,去释放锁,这个时候第一个线程释放的就是第二个线程的锁了。
所以需要把 if 判断和释放锁这个动作设置成原子操作,保证 if 判断完之后,锁也一定释放。
lua 脚本
悲观锁效率太低了,业务中有很多都需要释放锁
使用乐观锁也可以解决。
这里使用 lua 脚本解决,使用 lua 写一些 redis 命令,这些命令可以形成原子性操作。