水煮Redisson(九)-分布式锁的各种实现方式

295 阅读4分钟

前言

在许多场景下,多个节点需要保持数据的一致性,为了保证这一特性,就诞生了分布式锁这样特殊的产物。
一个分布式锁要产生效果,需要存在三种特性:互斥性,超时机制,可重入。

  • 互斥:任何时候,只有一个客户端可以持有锁;
  • 超时:客户端拿到锁之后,如果程序出现问题不能释放,则自动超时释放,以免出现死锁;
  • 可重入:对同一个线程来说,可以多次获取到锁,不用再与其他客户端竞争;

数据库的实现

数据库中一般有两种锁,乐观锁和悲观锁,根据上面提到的分布式锁三种特性,可以判定乐观锁是不能用于分布式锁的,互斥和可冲入都不能满足。
乐观锁采用version,读取数据时,将version字段一并取出,更新时将version加一;提交更新时,判断是否有做过更改,如果与自身持有的version不一致,则表示其他线程已经对数据做了修改,更新失败。例如:
update t_goods set status=2,version=version+1 where id=111 and version=2;

悲观锁

悲观锁采用行级锁,在更新语句后面跟上 【for update】,当前操作执行完成之后会进行锁释放,才允许其他线程执行更新。

分布式锁表

这里锁表不是一个动词,而是一个名词,表示在这个表中实现分布式锁;里面每个分布式资源只有一条记录,用来表示谁占用了这个资源的锁。
下面是这个表的简单示意:

CREATE TABLE `sourceLock` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `source` varchar(64) NOT NULL DEFAULT '' COMMENT '资源',
  `client_id` varchar(1024) NOT NULL DEFAULT '占用资源的客户端id',
  `expire_time` timestamp NOT NULL  '超时时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_source` (`source `) USING BTREE
)
 ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='分布式锁表';

从这个表中可以看出,可重入和互斥性是能满足的,超时机制需要一个独立的服务去处理,扫描到期的锁,并执行释放操作【删除到期的记录行】。

zookeeper实现

原理是通过创建临时顺序节点的方式来实现。多个客户端同时申请锁的时候,会各自创建一个临时节点,然后获取父节点下所有的临时节点,判断自身的节点序号是否最小,如果最小则获取到锁,否则进入等待。当获取锁的节点释放锁,临时节自动删除,产生删除节点的事件,这个事件会被其他等待中的客户端监听,再开始新一轮的最小节点判断。

redis的实现

单机模式

redis原始命令,set key vaue [EX seconds] [PX milliseconds] [NX|XX]
指令介绍:
EX seconds : 将键的过期时间设置为 seconds 秒。 执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value 。
PX milliseconds : 将键的过期时间设置为 milliseconds 毫秒。 执行 SET key value PX milliseconds 的效果等同于执行 PSETEX key milliseconds value 。
NX : 只在键不存在时, 才对键进行设置操作。
XX : 只在键已经存在时, 才对键进行设置操作。

集群模式

Redis作者antirez提出了一个算法,RedLock,也就是红锁。红锁的原理大概是这样:假设集群中有5个节点,客户端请求锁时,会同时向所有节点发出请求,当1+n/2个节点返回成功之后,客户端计算当前时间是否已经越过设定的超时时间;比如申请一个锁,超时时间为3秒,15:20:00发起申请,等15:20:10才返回成功,那么这个锁是无效的,只有在15:20:03分之前获取到锁,才算是成功。

其他方式

上面三种分布式锁比较常见,另外还有一些非主流的方式,比如利用consul的Key/Value存储API中的acquire和release接口来实现;还有与redis类似的缓存服务memcache,也有分布式锁的实现。