比如,我们现在有这么多商品服务,现在都要查数据库,现在我们约定只有一个人能查数据库,查完以后放到缓存里面。
这样呢,所有服务都要进来,需要抢占一个锁,本地情况下,我们可以使用语法比如synchronize(this)锁住当前对象,只要大家用的是一个对象,就能锁住了。
在分布式情况下也一样,我们this在分布式情况下,肯定没得用。
但是,我们可以考虑现实生活中的一个例子,比如,我们几千个人都去上公共厕所,里面只有一个坑位,那如何保证他们有序的进行,就是说,只要我进来了,我就给这个厕所上把锁,那别人就不能进来了。然后,上完以后,再出门解锁,这样其他人又能进来了。
所以,我们分布式锁,也是这个思想原理,所有的商品服务,无论你是哪一个实例,大家都去一个公共的地方占锁,如果占到了锁,就去执行业务,如果没有站到锁的,那等待一下。
当我们业务执行完了以后,然后,释放锁,这样别人就可以占到这个锁了。
我们说的这个占坑,可以去任何一个地方去占,只要大家都去公共的一个地方,比如大家都去数据库,哪怕给数据库里面插入一条记录,如果这个人插入进去了,说明是成功的,如果插入的时候,看到已经有记录了,他就不能插入成功。
1.1 使用redis测试redis中文文档
分布式锁的基本原理,我们都尝试去一个地方占坑,能占成功就会返回ok,大家都用同一个key保存一个东西,看能不能保存成功就行了。
二、分布式锁演进-阶段一 2.1 思想大家想要获取锁,都去redis里面占坑,假设1000个请求全进来,都先去执行第一步,全部给redis发布setnx命令获取锁,也就是说没有lock的时候,我就占,这10000个肯定只有一个能占成功。
占成功了,就获取到锁,执行业务,执行完以后,删除锁,结束。
没占成功我们可以等一下,然后,重试。
2.2 问题假如,我们业务代码,在这执行期间,出现了异常,导致程序抛异常直接退出了,都没有执行删锁代码,别人想要再来获取锁,就没有这个坑位了。
所以,这就导致了我们的死锁问题。
假如,我把删锁代码放到finally里面,这样异常也能删除。但是还有另外一种情况,就是我们的这个代码不是由于异常崩了,而是,我们代码正好执行到这,准备执行删锁的时候,我们机器断电了,我们没有执行删锁这段代码,相当于redis又没有删除锁,别人又占不到这个坑位了。
这就是我们说的第一个最严重的问题,如果我们解锁失败就会导致死锁。
解决这个问题的办法是什么?
我们可以给这个锁,设置一个自动过期时间。
三、分布式锁演进-阶段二 3.1 思想为了解决死锁的问题,我们就可以给锁设置过期时间,比如30s,30s以后它会自动删除,即使我们执行业务代码,中间出现了问题,也没关系,redis会自动帮我们删除这个锁。
加锁我们在设置过期时间前,出现异常或断电了,相当于我们过期时间没设置上,同样也会造成死锁。
如果加锁,和设置过期时间,是一个原子操作,就是我给redis,让它占锁的同时,加上过期时间,只要能返回ok,redis里面就说明已经加上锁,并设置上过期时间了。
返回不了,那就是加锁失败。所以,这一块失败的原因,就是我们获取锁和设置过期时间不是原子的。
我们业务代码执行成功,就来删锁,但是,假设我们当时锁设置的过期时间为10s,由于我们业务代码很耗时,比如我执行了30s,等我将要去删锁的时候,我们之前设置的锁其实已经过期了,redis里面已经没有它了。我们想要去删,我们想要去删除一个已经没有的数据还算好的,最坏的情况是这样的,别的线程看到你的线程过期了,然后,占了这个锁,然后,当你执行删锁的时候,却把别人的锁给删除了。
解决:我们需要保证删锁的时候,删的不是别人的锁,怎么办?我们可以在占锁的时候,值指定为一个uuid,这个uuid每个人都不一样,保证每个人匹配是自己的锁才删除。
最下面的delete多余
五、分布式锁演进-阶段四 5.1 思想如果正好判断是当前值,正要删除锁的时候,锁已经过期,别人已经设置到了新值,那么我们删除的是别人的锁。
解决:
办法删除锁必须保证原子性,使用redis+Lua脚本完成。
六、分布式锁演进-阶段五 6.1 思想加锁保证原子性,解锁保证原子性
锁的自动续期,假设我们业务时间超长,业务还没执行完锁给过期了,所以,我们就要在执行业务期间,给锁自动续期,最简单的操作,就是我把锁的超时时间放长一些。
视频教程