大家好,我是 Snow Hide,作为《左耳听风》这个专栏的学员之一,这是我打卡的第 22 天,也是我第 22 次进行打卡这种操作。
今天我温习了该专栏里一篇叫《管理设计篇之“分布式锁”》的文章。
关键词总结:分布式锁服务的特点(安全性、避免死锁、容错性)、Redis 的分布式死锁服务(避免死锁的问题、不区分 Client 会出现的一种问题)、分布式锁服务的一个问题(实际应用时的问题、解决问题的栅栏(fence)技术)、从乐观锁到 CAS、分布式锁设计的重点(设计初衷、设计考量)。
所学总结:
分布式锁服务的特点
一般通过数据库、缓存系统(Redis)或层级键值存储系统(Zookeeper)等技术来实现。
安全性
任何情况下都只能有一个客户端可以获得锁。
避免死锁
无论之前拿到锁的客户端最后结果如何,排队中的客户端始终可以拿到锁。
容错性
只要服务集群还有运行着的节点,客户端就可以进行加/解锁操作
Redis 的分布式死锁服务
避免死锁的问题
在某个键不存在的情况下才能成功配置指定的键,允许多个进程同时对键进行设置,只有一个进程能成功,这就避免了多个进程同时操作一个指定键可能产生的不可预知的问题。
区分客户端可以避免的问题
通过键来进行客户端的解锁操作可以避免其释放了其他客户端申请的锁。官方推荐从 /dev/urandom 中取 20 个字节作为随机数,或使用 RC4 加密算法在 /dev/urandom 中获得一个种子,然后生成一个伪随机流。官方不推荐使用时间戳+客户端编号的方式来生成随机数,因为安全性较差一些,但这种方式对一些场景来说是没有问题的。
分布式锁服务的一个问题
实际应用时的问题
由于某种延迟类问题,新的客户端获得锁之后对数据进行了改动,而在此之前的另一个客户端因为延迟的问题而导致它的锁时间过期并在新客户端申请了锁处理完毕之后才回过神来重新申请了锁来完成之前的任务,这里出现的问题是之前客户端覆盖了新客户端所提交的数据。
解决问题的栅栏(fence)技术
- 给锁服务分配一个单调递增的版本号;
- 写数据时通过进程的版本号来判断;
- 保存版本号至数据库中,对请求的进程做校验。
从乐观锁到 CAS
CAS 源自于汇编的原子操作 (Compare And Swap)指令,被广泛的应用在了无锁的数据结构中。
版本不变才更新
UPDATE table_name SET xxx = #{xxx}, version=version+1 where version =#{version};
库存不变才更新
SELECT stock FROM tb_product where product_id=#{product_id};
UPDATE tb_product SET stock=stock-#{num} WHERE product_id=#{product_id} AND stock=#{stock};
分布式锁设计的重点
保证在集群中相同服务的相同方法在同一时间内只能被外部的某一个线程所调用执行。
设计初衷
- 锁服务设置过期时间,过期后自动解锁;
- 改变数据之前通过数据的改前特征来判断其是否还未被更改;
- Redis 的分布式锁服务适合于同步或互斥来自不同机器的线程。
- CAS 无锁方式适用于对共享数据进行更改的场景。分布式锁服务适用于对不同进程进行同步或互斥的场景。
设计考量
- 借助锁持有时间超时或会话超时等方式来释放没有被回收的锁;
- 保证高可用以及持久化;
- 锁服务本身是非阻塞的;
- 锁的可重入性(函数中执行中断的地方是可以被作为起点来继续前行的,并且多个调用可以安全的并发执行)。
末了
重新总结了一下文中提到的内容:分布式锁、加锁和解锁、锁超时、乐观并发控制、CAS 源自操作、分布式锁服务同步。