分布式锁是解决协调分布式系统之间,同步访问共享资源的一种方式。
举个例子,火车票保证不超买?
这类问题,有很多种解决的办法。大概的思路有三种。
-
第一种是基于数据库实现
-
第二种是基于缓存系统去实现 (典型代表是使用Redis实现的锁服务)
-
第三种是基于分布式一致性算法实现 (典型代表为Zookeeper)
-
提供个其他思路,解决需要加锁的问题,也许更好的方式是绕过加锁的环节。比如说两个请求要同时访问一个资源,如果可以把他们串行化(比如放到同一个队列来处理)。当然,只是提供一个思路,这个要看业务具体的特点。
基于数据库的实现,大概也是两个思路。第一种是悲观锁(select xxx for update),第二种是乐观锁。
-
悲观锁
通过数据库行锁 select for update 锁住这行数据,然后将查询和插入的 SQL 在同一个事务中提交。
select id from order where order_id = xxx for update不过,这种方案的问题是:
-
基于 MySQL 行锁的方式会出现交叉死锁,比如事务 1 和事务 2 分别取得了记录 1 和记录 2 的排它锁,然后事务 1 又要取得记录 2 的排它锁,事务 2 也要获取记录 1 的排它锁,那这两个事务就会因为相互锁等待,产生死锁。
-
当然,你可以通过“超时控制”解决交叉死锁的问题,但在高并发情况下,出现的大部分请求都会排队等待,所以“基于关系型数据库实现分布式锁”的方式在性能上存在缺陷。
-
-
乐观锁
带个版本号 (具体细节先不展开了)
-
在锁的目录下面,创建临时有序节点
-
每个节点需要申请锁的时候,在这个目录下面创建临时节点
-
在锁的目录下面,锁的序号最小的一个节点认为拿到了锁
-
后面的节点 依次watch前面的节点,当前面的节点变更的时候,后面的节点被唤醒,进行step3的判断
举个例子:
-
比如某进程获得到的节点序号为 /zk_lock/003,然后所有的节点列表为[/zk_lock/001,/zk_lock/002,/zk_lock/003],则对 /zk_lock/002 这个节点添加一个事件监听器。
-
如果锁释放了,会唤醒下一个序号的节点,判断是否自己的节点序号是最小。
-
比如 /zk_lock/001 释放了,/lock/002 监听到时间,此时节点集合为[/zk_lock/002,/zk_lock/003],则 /lock/002 为最小序号节点,获取到锁。
优点:
-
临时有序的zk节点,可以保证当进程挂掉的时候,锁还是可以被释放的。
-
每个节点watch前面的节点,相当于一个订阅回调的机制,这样业务层使用起来更舒服。
在redis上面设置一个key作为锁
-
使用set key value 作为锁的标记位,然后expire key 10s 设置key的有效期
问题:这个方案的问题是,是通过两条语句执行的,不能保证原子性.
-
SET key value NX PX millisecondskey: 资源的key,标识一种资源。 value: 此值必须在所有进程和所有获取锁的请求中都是唯一的。 NX: 该命令仅在key不存在时才设置。 PX milliseconds: 过期时间,单位为毫秒。这个方案是通过一条命令设置key的value,以及key的有效期,这样可以保证是一个原子的操作。
释放 :
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end这种方案解决不了的问题是:
如果在一个redis的集群中,当在master的节点完成了这个操作,但是由于redis是异步复制的,slave的节点不指定会马上收到通知的数据。如果master立刻宕机,会导致上锁失败。
-
redlock 方案
对于在集群中的枷锁redis给的方案是redlock,它的基本思路就是为锁准备多个独立的节点,在锁过期时间内只要超过半数获取到锁,就算成功,避免redis主从切换的时候,数据丢失。
总结一下,分布式锁是解决多个进程同时访问临界资源的常用方法,在分布式系统中非常普遍,常见的实现方式是基于数据库,基于 Redis。
-
redis方案注重性能,但是不能保证安全性。
-
redlock方案,资源成本太高,不能公用redis cluster集群,还不能绝对保证安全性,有点鸡肋。
-
zookeeper方案,比较简单,没有时钟跳跃的问题。可靠性要比redis的高,但是性能比redis的要差
参考: