结果正确,线程 2 在 crash 后,其他线程在等待,直到锁过期。(这里不好演示,感兴趣的同学可以自己试试)
进行到这里,似乎已经可以解决数据不一致的问题了,但在欢喜之余,不妨多想想会不会出现其他问题。比如假设 A 进程的逻辑还没处理完,但是锁由于过期时间到了,导致锁自动释放掉,这时 B 线程获得了锁,开始处理 B 的逻辑,然后 A 进程的逻辑处理完了,就把 B 进程的锁给删除了呢?这也是下面要讲的问题。
方式三:锁的生成和删除必须是同一个线程
我们先把测试用例改成下面这样:
在以上的例子,我们让线程的执行时间大于锁的过期时间,导致锁到期自动释放。
测试结果:
从以上结果可以看出,由于每个线程的执行时间大于锁的过期时间,当线程的任务还没执行完时,锁已经自动释放,使得下一个线程获得了锁,而后下一个线程的锁被上一个执行完了的线程删掉或者也是自动释放(具体要看线程的执行时间和锁的释放时间),于是又产生了同一个数据被两个或多个线程同时修改的问题,导致数据出现不一致。
我们用四个线程,按照时间顺序画的流程图如下:
可以看到,在 2.5s 和 5s 的时刻,都产生了误删锁的情况。
既然这个现象是由于锁过期导致误删别人家的锁引发的,那我们就顺着这个思路,强制线程只能删除自己设置的锁。如果是这样,就得被每个线程的锁添加一个唯一标识了。看看上面的锁机制,我们每次添加锁的时候,都是给lock_key 设为 1,无论是 key 还是 value,都不具备唯一性,如果把 key 设为每个线程唯一的,那在分布式系统中,得产生 N (等于总线程数)个 key 了 ,从直观性和维护性上来说,这都是不可取的,于是乎只能从 value 入手了。我们看到每个线程都可以取到一个唯一标识,即线程 ID,如果加上进程的 PID,以及机器的 IP,就可以构成一个线程锁的唯一标识了,如果还担心不够唯一,再打上一个时间戳了,于是乎,我们的分布式锁最终版就变成了以下这样:
以上可以看出,问题没有得到解决。因为什么原因呢?以上我们设置值的唯一性只能确保线程不会误删其他线程产生的锁,进而出现连串的误删锁的情况,比如 A 删了 B 的锁,B 执行完删了 C 的锁 。使用 redis 的过期机制,只要业务的处理时间大于锁的过期时间,就没有一个很好的方式来避免由于锁过期导致其他线程同时占有锁的问题,所以需要熟悉业务的执行时间,来合理地设置锁的过期时间。
还需注意的一点是,以上的实现方式中,删除锁(del_lock)的操作不是原子性的,先是拿到锁,再判断锁的值是否相等,相等的话最后再删除锁,既然不是原子性的,就有可能存在这样一种极端情况:在判断的那一时刻,锁正好过期了,被其他线程占有了锁,那最后一步的删除,就可能会造成误删锁了。可以使用官方推荐的 Lua 脚本来确保原子性:
是只要锁的过期时间设置的足够合理,这个问题其实是可以忽略的,也可以说出现这种极端情况的概率是及其小的。毕竟,在我们优雅的 python 代码中,突然插入一段脚本,显得不是那么 pythonic 了。
总结
以上我们使用 redis 来实现一个分布式的同步锁,来保证数据的一致性,其特点是:
满足互斥性,同一个时刻只能有一个线程可以获取锁
利用 redis 的 ttl 来确保不会出现死锁,但同时也会带来由于锁过期引发的多线程同时占有锁的问题,需要我们合理设置锁的过期时间来避免
利用锁的唯一性来确保不会出现误删锁的情况
以上的方案中,我们是假设 redis 服务端是单集群且高可用的,忽视了以下的问题:如果某一时刻 redis master 节点发生了故障,集群中的某个 slave 节点变成 master 节点,这时候就可能出现原 master 节点上的锁没有及时同步到 slave 节点,导致其他线程同时获得锁。对于这个问题,可以参考 redis 官方推出的 redlock 算法,但是比较遗憾的是,该算法也没有很好地解决锁过期的问题。
进行到这里,似乎已经可以解决数据不一致的问题了,但在欢喜之余,不妨多想想会不会出现其他问题。比如假设 A 进程的逻辑还没处理完,但是锁由于过期时间到了,导致锁自动释放掉,这时 B 线程获得了锁,开始处理 B 的逻辑,然后 A 进程的逻辑处理完了,就把 B 进程的锁给删除了呢?这也是下面要讲的问题。
方式三:锁的生成和删除必须是同一个线程
我们先把测试用例改成下面这样:
在以上的例子,我们让线程的执行时间大于锁的过期时间,导致锁到期自动释放。
测试结果:
从以上结果可以看出,由于每个线程的执行时间大于锁的过期时间,当线程的任务还没执行完时,锁已经自动释放,使得下一个线程获得了锁,而后下一个线程的锁被上一个执行完了的线程删掉或者也是自动释放(具体要看线程的执行时间和锁的释放时间),于是又产生了同一个数据被两个或多个线程同时修改的问题,导致数据出现不一致。
我们用四个线程,按照时间顺序画的流程图如下:
可以看到,在 2.5s 和 5s 的时刻,都产生了误删锁的情况。
既然这个现象是由于锁过期导致误删别人家的锁引发的,那我们就顺着这个思路,强制线程只能删除自己设置的锁。如果是这样,就得被每个线程的锁添加一个唯一标识了。看看上面的锁机制,我们每次添加锁的时候,都是给lock_key 设为 1,无论是 key 还是 value,都不具备唯一性,如果把 key 设为每个线程唯一的,那在分布式系统中,得产生 N (等于总线程数)个 key 了 ,从直观性和维护性上来说,这都是不可取的,于是乎只能从 value 入手了。我们看到每个线程都可以取到一个唯一标识,即线程 ID,如果加上进程的 PID,以及机器的 IP,就可以构成一个线程锁的唯一标识了,如果还担心不够唯一,再打上一个时间戳了,于是乎,我们的分布式锁最终版就变成了以下这样:
以上可以看出,问题没有得到解决。因为什么原因呢?以上我们设置值的唯一性只能确保线程不会误删其他线程产生的锁,进而出现连串的误删锁的情况,比如 A 删了 B 的锁,B 执行完删了 C 的锁 。使用 redis 的过期机制,只要业务的处理时间大于锁的过期时间,就没有一个很好的方式来避免由于锁过期导致其他线程同时占有锁的问题,所以需要熟悉业务的执行时间,来合理地设置锁的过期时间。
还需注意的一点是,以上的实现方式中,删除锁(del_lock)的操作不是原子性的,先是拿到锁,再判断锁的值是否相等,相等的话最后再删除锁,既然不是原子性的,就有可能存在这样一种极端情况:在判断的那一时刻,锁正好过期了,被其他线程占有了锁,那最后一步的删除,就可能会造成误删锁了。可以使用官方推荐的 Lua 脚本来确保原子性:
是只要锁的过期时间设置的足够合理,这个问题其实是可以忽略的,也可以说出现这种极端情况的概率是及其小的。毕竟,在我们优雅的 python 代码中,突然插入一段脚本,显得不是那么 pythonic 了。
总结
以上我们使用 redis 来实现一个分布式的同步锁,来保证数据的一致性,其特点是:
满足互斥性,同一个时刻只能有一个线程可以获取锁
利用 redis 的 ttl 来确保不会出现死锁,但同时也会带来由于锁过期引发的多线程同时占有锁的问题,需要我们合理设置锁的过期时间来避免
利用锁的唯一性来确保不会出现误删锁的情况
以上的方案中,我们是假设 redis 服务端是单集群且高可用的,忽视了以下的问题:如果某一时刻 redis master 节点发生了故障,集群中的某个 slave 节点变成 master 节点,这时候就可能出现原 master 节点上的锁没有及时同步到 slave 节点,导致其他线程同时获得锁。对于这个问题,可以参考 redis 官方推出的 redlock 算法,但是比较遗憾的是,该算法也没有很好地解决锁过期的问题。