分布式锁主流有三种模式:
实现方式 功能要求 实现难度 学习成本 运维成本
MySQL 的方案借助表锁/行锁实现 满足基本要求 不难 熟悉 小量OK、大量影响现有业务、1主多从架构,不方便扩容
通过 ZK 创建数据节点的方式实现 满足要求 熟悉 ZK API 即可 需要学习 重,需要堆机器,有跨机房请求
Redis 使用 setnxex 基本要求 不难 熟悉 扩容方便、现有服务
MySQL 单主架构,写都会到 master,有瓶颈。ZK 的方式需要自己搭建、运维,而且需要堆机器,利用率不高。最终采用了 Redis 来实现,流量/存储都可以扩容,运维也不需要自己。
一、基于Mysql实现分布式锁 (乐观锁)
Mysql实现分布式锁 主要是基于数据库的排他锁(也叫行级排他锁), 采用乐观锁的方式去做。
我们可以通过一个update语句是否成功来判断线程抢占锁是否成功,比如如下sql语句:
CREATE TABLE `t_schedule_cluster` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '@cname:主键',
`execute` int(1) NOT NULL COMMENT '@cname:执行状态',
`version` int(11) NOT NULL COMMENT '@cname:版本号 ',
`task_name` varchar(128) NOT NULL COMMENT '@cname:任务名称',
`execute_ip` varchar(32) DEFAULT NULL COMMENT '@cname:执行ip ',
`update_time` datetime DEFAULT NULL COMMENT '@cname:修改时间',
PRIMARY KEY (`id`),
KEY `Index_series_id` (`execute`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COMMENT='@cname:多机定时任务调度';
争抢锁的sql语句:
update t_schedule_cluster set execute = 1 version = ?, execute_ip = ?, update_time = ? where task_name = ? and version = ?
实现原理入下图:
但是数据库的性能有限,如果在高并发的情况下会频发的访问数据库,对数据库会造成较大的压力。
二,基于redis的分布式锁实现
基于Redis实现的分布式锁其实很简单,底层就是使用redis的setnx
指令来实现的加锁,我们来看看官方对setnx的定义:
SETNX key value
将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
返回值:
设置成功,返回 1 。
设置失败,返回 0 。
redis> EXISTS job # job 不存在
(integer) 0
redis> SETNX job "programmer" # job 设置成功
(integer) 1
redis> SETNX job "code-farmer" # 尝试覆盖 job ,失败
(integer) 0
redis> GET job # 没有被覆盖
"programmer"
以上内容来自于:http://redisdoc.com/string/setnx.html
既然setnx这么强大,那么我们是不是可以高枕无忧直接使用了? 当然了,我们还要考虑一些极端场景。
2.1 死锁问题
既然设置了value值,那么我们肯定会想到过期时间,那么就需要再使用setnx指令后继续使用expire指令。但是这两部操作必定不是原子性的,如果执行expire失败怎么办?
其实Redis官方也考虑到了这个问题,在Redis2.8 之后,官方执行setnx 和 expire命令一起使用了。如下:
SET lock_key lock_value NX PX 30000
其中:
1.lock_key:即锁名称,这个名称应是公开的,在分布式环境中,对于某一确定的公共资源,所有争用方(客户端)都应该知道对应锁的名字。对于 Redis 而言,lock_name 就是 key-value 中的 key,具有唯一性。
2. lock_value:是由客户端生成的一个随机字符串,它要保证在足够长的一段时间内在所有客户端的所有获取锁的请求中都是唯一的,用于唯一标识锁的持有者。
3. NX 表示只有当 lock_key(key) 不存在的时候才能 SET 成功,从而保证只有一个客户端能获得锁,而其它客户端在锁被释放之前都无法获得锁。
4. PX 30000 表示这个锁节点有一个 30 秒的自动过期时间(目的是为了防止持有锁的客户端故障后,无法主动释放锁而导致死锁,因此要求锁的持有者必须在过期时间之内执行完相关操作并释放锁)。
具体操作如下图:
2.2 锁自动过期存在的隐患
例如我们有两个线程A、B,此时线程A抢到了锁,且设置自动过期时间为10s钟,因为系统其他原因导致系统A发生阻塞。而此刻10s钟后锁自动过期,线程C获取到了同一个资源的锁,线程A从阻塞中恢复,认为自己仍然持有锁,继续操作同一资源。这样就使得加锁的互斥性失效了。
解决方案:
我们在上面set lock_key lock_value 时讲过,lock_value是一个随机生成的字符串,在每次获取锁的时候都会重新生成。那么我们在执行真正的业务逻辑(类似于和db进行交互的操作,同一时刻只能一个线程操作的情况)时,判断当前生成的随机字符串和lock_value是否一致,如果不一致则说明redis中的lock_value被修改过,也就说明此刻锁已经被其他线程所占有。
具体操作流程如下图:
主要使用的就是这两种方案,在这里只是做个简单总结,其实还有其他一些可以实现分布式锁,根据自己项目本身情况选择最合适的。
另外 已经Redis也有开源的框架可以很好地支持基于Redis的分布式锁,这里推荐一个:Redission https://github.com/redisson/redisson
PS:2019 继续努力加油学习更多知识,让自己在技术这条道路上越走越远!