1.导读
分布式锁是一种用于分布式系统中的并发控制机制,它用于确保在多个节点或多个进程之间的并发操作中,某些关键资源或代码块只能被一个节点或进程同时访问。分布式锁的目的是避免多个节点同时修改共享资源而导致的数据不一致或冲突的问题。通俗的来说,分布式锁的出现是为了解决分布式系统的问题,如果是单系统可以使用synchronized来完成资源的锁定,而如果是多系统环境,这个关键字只能控制本地的资源,由此诞生了分布式锁。
2.实现
初级版本1.1
使用sentx对系统进行上锁,setnx是redis中提供的命令,主要作用是set一个key,但是区别set的操作是,nx代表的是not exit(不存在)即不存在才能set成功,存在会失败报nil。上锁之后就是正常的执行业务代码,执行完毕过后释放锁。而如果线程setnx失败的话,线程应该睡眠1s然后自旋,也就是重新递归调用自己,重复以上操作,睡眠是为了防止压力过大。具体setnx命令为set lock 123 NX,可以自己试试。
升级版本1.2
思考:上面逻辑有什么问题?如果设置成功后,去执行代码,出现了异常或者业务闪断,是不是锁没有正常得到释放,这样B或者其他人都会一直无法使用,形成死锁,这个时候是不是就有聪明的人想到了使用过期时间expire。命令
EXPIRE key seconds
升级版版本1.3
同样上面设置同样会有问题,问题就出在设置过期时间这个环节可能也会出错,无法正常执行,这同上也会出现释放锁受阻,因此设置过期时间应该和setnx同属于原子操作,这样就算失败了也并没有设置上锁。具体操作是采用命令
set lock 123 Ex 30 NX
升级版本2.1
关于删除lock释放锁的操作的问题
思考:如果你的业务时间非常长,A执行代码过程需要40s,但是你设置的过期时间是30s,然后B因为你的key过期,会马上获取到锁的资源,然后这个时候A又到了30s,会执行释放锁的操作,肯定会释放到B的线程的锁,释放了其他线程也会马上进入,就会造成多个线程在执行同一个锁的操作,完全没有实现锁的特性。
实现:考虑实现使用UUID去设置值,在释放锁的时候先去获取锁的值,如果能够匹配上UUID就执行删除锁的操作。
升级版2.2
注意:if(get(lock) == UUID){del(lock)} 这段代码一般是由Java客户端在执行,这个时候就会出现非常致命的问题,get(lock)的过程中是Java端在像Redis过程中请求,假如你现在能够获取到lock的UUID,是属于A线程(本线程)的UUID的,但是在返回过程中,恰好又出现了过期时间过期了,这个时候B线程就会马上进入锁,执行后续操作!!!!问题来了,这个时候A以为自己还是成功获取到了,所以会删除lock,但是这个时候就会删除到B的lock,会出现上述现象。本质的产生就是释放锁(删除锁)的操作不是原子性的。
解决:lua脚本 官方的代码 KEYS[1] 就是lock 而 ARGV[1] 是UUID
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
这个本质你可以看成在redis内执行了获取key和删除的操作,所以并不存在Java客户端到Redis有往返造成的判断和删除非原子的情况。