Redis之分布式锁实现原理
认识分布式系统
服务架构的大致发展流程和分类,说了大致啊不必细究,毕竟只是为了方便对分布式不了解的同学有个初步认知。
- MVC架构 :当业务规模很小时,将所有功能都部署在同一个进程中,通过双机或者前置负载均衡实现负载分流;此时,用于分离前后逻辑的Mvc架构是关键。
- RPC架构:当垂直应用越来越多,应用之间交互不可避免,将核心和公共业务抽取出来,作为独立的服务,实现前后台逻辑分离、此时,用于提高业务复用和拆分的RPC框架是关键。
- SOA架构:随着业务发展,服务数量越来越多,服务生命周期管控和运行态的治理成为瓶颈,此时用于提升服务质量的SOA服务治理是关键。
微服务架构:随着敏捷开发,持续交付,DEVOPS理论的发展和实践,以及基于docker等轻量级容器部署应用和服务的成熟,微服务架构开始流行,逐渐成为应用架构的未来演进方向。通过服务的原子拆分,以及微服务的独立打包,部署和升级,小团队敏捷交付,应用的交付周期将缩短,运维成本也将大幅下降。
到了现在火热的微服务是真正的分布式的、去中心化的。把所有的业务技术处理逻辑包括路由、消息解析等放在服务内部,去掉一个大一统的 ESB,服务间轻通信,比 SOA 更彻底的拆分。微服务架构强调的重点是业务系统需要彻底的组件化和服务化,原有的单个业务系统会拆分为多个可以独立开发,设计,运行和运维的小应用,这些小应用之间通过服务完成交互和集成。
分布式锁的背景意义
如果在一个分布式系统中,我们从数据库中读取一个数据,然后修改保存,这种情况很容易遇到并发问题。因为读取和更新保存不是一个原子操作,在并发时就会导致数据的不正确。这种场景其实并不少见,比如电商秒杀活动,库存数量的更新就会遇到。如果是单机应用,直接使用本地锁就可以避免。如果是分布式应用,本地锁派不上用场,这时就需要引入分布式锁来解决。由此可见分布式锁的目的其实很简单,就是为了保证多台服务器在执行某一段代码时保证只有一台服务器执行。为了保证分布式锁的可用性,至少要确保锁的实现要同时满足以下几点:
- 互斥性:在任何时刻,保证只有一个客户端持有锁。
- 不能出现死锁。如果在一个客户端持有锁的期间,这个客户端崩溃了,也要保证后续的其他客户端可以上锁。
保证上锁和解锁都是同一个客户端。
这就说到分布式架构下的分布式锁的实现,纵观各类公众号、博文、论坛,分布式锁的实现无非哪几种方式,当然我也不例外(主要是不够流弊~_~),1)数据库乐观锁;2)基于Redis的分布式锁;3)基于ZooKeeper的分布式锁。下面着重了解下redis的实现方案。
Redis实现分布式锁原理
Redis实现分布式锁主要利用Redis的`setnx`命令。`setnx`是`SET if not exists`(如果不存在,则 SET)的简写。
127.0.0.1:6379> setnx lock test1 #在key值lock不存在的情况下,将键key的值设置为test1
(integer) 1
127.0.0.1:6379> setnx lock test2 #试图覆盖lock的值,返回0表示失败
(integer) 0
127.0.0.1:6379> get lock #获取lock的值,验证没有被覆盖
"test1"
127.0.0.1:6379> del lock #删除lock的值,删除成功
(integer) 1
127.0.0.1:6379> setnx lock test2 #再使用setnx命令设置,返回1表示成功
(integer) 1
127.0.0.1:6379> get lock #获取lock的值,验证设置成功
"test2"
代码层面的表述就如下代码块
private static Jedis jedis = new Jedis("localhost");
private static final Long SUCCESS = 1L;
/**
* 加锁
*/
public boolean tryLock(String key, String requestId) {
// 使用setnx命令。
// 不存在则保存成功返回1-加锁成功;如果已经存在则返回0-加锁失败。
return SUCCESS.equals(jedis.setnx(key, requestId));
}
/**
* 加锁 并指定过期时间方便开发把控
*/
public boolean tryLock(String key, String requestId, int expireTime) {
// 使用jedis的api,保证原子性
// NX 不存在则操作 EX 设置有效期,单位是秒
Object result = jedis.set(key, requestId, "NX", "EX", expireTime);
//返回OK则表示加锁成功
return SUCCESS.equals(result);
}
/**
* 删除key的lua脚本,先比较requestId是否相等,是统一key则删除
*/
private static final String DEL_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
/**
* 解锁
*/
public boolean unLock(String key, String requestId) {
// 删除成功表示解锁成功
Long result = (Long) jedis.eval(DEL_SCRIPT, Collections.singletonList(key), Collections.singletonList(requestId));
return SUCCESS.equals(result);
}
但是我们知道上锁完成之后就开始我们的业务逻辑操作及一系列的保存更新操作,但是也会包括调用第三方接口情况,再加上本身网络开销,上锁的时间如何把控,如何能做到业务逻辑操作完成把锁及时释放掉,这是问题,也是避免不了的问题,如果业务没有处理完成过期时间到就把锁释放了,这样就会出去数据错误的问题,如果过期时间设置过长,那就会影响整体的性能指标。这时候又一种方案出现了,这种方式就是给锁续期。
Redisson实现分布式锁原理
在Redisson框架实现分布式锁的思路,就使用watchDog机制实现锁的续期。当加锁成功后,同时开启守护线程,默认有效期是30秒,每隔10秒就会给锁续期到30秒,只要持有锁的客户端没有宕机,就能保证一直持有锁,直到业务代码执行完毕由客户端自己解锁,如果宕机了自然就在有效期失效后自动解锁。这样我们只需要预估一个业务逻辑处理的大概时间,也不用考虑由于第三方调用或者网络波动引起的逻辑没有处理完成而过早把锁释放掉的情况。
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock、联锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)等,还提供了许多分布式服务。Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让我们能够将精力更集中地放在处理业务逻辑上。
/**
* 根据name对进行上锁操作,redisson tryLock 根据第一个参数,一定时间内为获取到锁,则不再等待直接返回boolean。交给上层处理
*/
public boolean tryLock(String key) throws InterruptedException {
if (key == null || "".equals(key)) {
return false;
}
//tryLock,第一个参数是等待时间,2秒内获取不到锁,则直接返回。 第二个参数 60是60秒后强制释放
return redissonClient.getLock(key + StringConstance.REDIS_LOCK).tryLock(WAIT_TIME, LEASE_TIME, TimeUnit.SECONDS);
}
/**
* 设置 公平锁
*/
public boolean tryFairLock(String key) throws InterruptedException {
if (key == null || "".equals(key)) {
return false;
}
return redissonClient.getFairLock(key + StringConstance.REDIS_LOCK).tryLock(WAIT_TIME, LEASE_TIME, TimeUnit.SECONDS);
}
/**
* 设置 公平锁
*/
public boolean tryFairLock(String key, Long waitTime, Long leaseTime) throws InterruptedException {
if (key == null || "".equals(key)) {
return false;
}
return redissonClient.getFairLock(key + StringConstance.REDIS_LOCK).tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
}
/**
* 创建 联锁
*/
public boolean multiLock(String... keys) throws InterruptedException {
//tryLock,第一个参数是等待时间,5秒内获取不到锁,则直接返回。 第二个参数 60是60秒后强制释放
RLock[] locks = new RLock[keys.length];
for (int i = 0; i < keys.length; i++) {
if (keys[i] == null || "".equals(keys[i])) {
throw new IllegalStateException("key must be not null");
}
locks[i] = redissonClient.getLock(keys[i]);
}
return new RedissonMultiLock(locks).tryLock(WAIT_TIME, LEASE_TIME, TimeUnit.SECONDS);
}
加锁: 解锁:
从上了解到RedissonLock是可重入的,并且考虑了失败重试,可以设置锁的最大等待时间, 在实现上也做了一些优化,减少了无效的锁申请,提升了资源的利用率。但是有一点需要特别注意,RedissonLock 同样没有解决节点挂掉的时候,存在丢失锁的风险的问题。
例如:只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况:
在Redis的master节点上拿到了锁,但是这个加锁的key还没有同步到slave节点,master故障,发生故障转移,slave节点升级为master节点,导致锁丢失。而现实情况是有一些场景无法容忍的,所以 Redisson 提供了实现了redlock算法的 RedissonRedLock,RedissonRedLock 真正解决了单点失败的问题,代价是需要额外的为 RedissonRedLock 搭建Redis环境。所以,如果业务场景可以容忍这种小概率的错误,则推荐使用 RedissonLock, 如果无法容忍,则推荐使用 RedissonRedLock。
如有披露或问题欢迎留言或者入群探讨
还没有评论,来说两句吧...