如何评价Redis实现的分布式锁?
前言
分布式锁我认为是分布式架构中非常重要的一环,是所有分布式架构和微服务实现的基础。即使是单体的JAVA WEB架构集群后也要引入分布式锁,因为集群之后节点本地的代码锁已不能锁着并发的资源。
举个例子我有个统一生成唯一订单号的方法,每次进去时用synchronized锁上方法,以免出现并发生成重复订单号问题。但一旦集群后,synchronized只对本地节点的JVM生效。因此并有了并发问题。
前两年我负责将单体架构往微服务架构改造时,当时研究分布式锁在网上能搜到的基本是基于redis实现的。我们当时生产环境也确实上了redis实现的分布式锁,并运行了一段时间。下面我们一起探讨下redis能不能实现分布式锁,是否安全?以免大家踩坑导致公司损失。
redis分布式锁的原理
redis有个命令叫setnx,如果已经用了setnx保存了某个key,再想调用setnx保存该key,redis会返回错误。利用这个特性,上锁的时候用setnx保存一个锁A并设置过期时间(根据实际业务处理时间决定这个key的保存时间),使用完成后再删除锁A即可。
redis实现分布式锁例子
我们先来解读下用redis实现分布式代码
/***
* 尝试获取分布式锁,可设置失效时间,单位为秒
* @param lockKey 锁的名称
* @param lockValue 解锁的密码(思考下为何需要解锁的密码?)
* @param time 锁的时间
* @return 是否上锁成功
*/
public boolean lock(String lockKey, String lockValue, int time) {
//我这用Jedis连接redis,因为我们系统中用了reids多个数据库,用springDataRedis不好做,
//根据实际情况调整部分代码即可
Jedis jedis = null;
try{
jedis = this.pool.getResource();
jedis.select(this.database);
boolean result = false;
for(int i=0;i<1000;i++){//循环1000次,每次休眠100毫秒,time钞内拿不到放弃
//调用LUA脚本保证代码的原子性(如果上锁步骤是多行代码必然是错误的)
if("OK".equals(jedis.set(lockKey, lockValue, SetParams.setParams().nx().ex(time)))){
result = true;
break;
}else {
Thread.sleep(100);
}
}
return result;
}catch(Exception e){
return false;
}finally{ if(jedis!=null) jedis.close(); }
}
/**
* 释放分布式锁
* @param lockKey 锁的名称
* @param lockValue 锁的密码
* @return 释放解锁成功
*/
public boolean unLock(String lockKey, String lockValue) {
Jedis jedis = null;
try{
jedis = this.pool.getResource();
jedis.select(this.database);
//执行lua脚本,如果执行成功会返回1
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
return new Long(1).equals(jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockValue)));
}catch(Exception e){
return false;
}finally{ if(jedis!=null) jedis.close(); }
}
代码中我问了一句“思考下为何需要解锁的密码?”,很简单,解铃人还须系铃人。方法A调用的,必须是方法A来解锁(根据redis key-value结构,密码可以用UUID),不是方法A解锁就不能保证方法A是否已完成并引起并发问题。
redis实现的锁是否安全?
回到文章的重点,redis实现的分布式锁是否安全?要回答这个问题,我们需要回到实际的生产环境中去,在大系统的生产环境中,中间件为了避免单点问题往往需要集群部署。redis作为重要的中间件,一直是集群部署。
但实际上redis并没有像etcd或zookeeper那样有一致性算法。
假设现在有两个进程t1和t2同时去获取锁A,t1获取成功了,就在此时redis master挂了,还未来得及把锁A同步到从节点上。由于redis是主从哨兵模式,从节点会马上成为主节点继续提供服务,但这时新的master并没有锁A记录。t2再去获取锁A是能获取成功的,并发问题由此诞生。
结论:在redis集群情况下,redis实现的分布式锁是不安全的。在非集群redis中是安全的,但实际生产中,单个redis容易产生单点问题,是不推荐的!
回到分布式的CAP理论中,一个分布式锁场景实际是CP模型(保证分区容错性和一致性),但redis实现的分布式锁实际是个AP模型(不保证一致性)。因为redis实现的分布式锁是不安全的!
怎样实现分布式锁是安全的?
zookeeper是在业界中最早解决分布式锁的组件之一。当前雅虎是用zookeeper来做分布式协调工作,采用的一致性协议是PAXOS协议。但zookeeper毕竟不是主要为了解决分布式锁而诞生的,普遍认为zookeeper实现的分布式锁性能并不出色。
etcd的诞生更多是为了解决一致性问题,采用的是raft协议,raft协议在区块链技术中和分布式关系型数据库TIBD采用raft协议解决一致性问题。性能较好。
Redisson是redis作者根据分布式锁需求而制定的组件,在redis官网文档有。redisson实现分布式锁的大概原理是加锁时保证每个节点都同步完成数据才算完成,保证了一致性。个人比较推荐大多数企业使用redisson即可,因为redisson实际就是调用了redis,对开发人员更友好。
并且大多数企业并发并有没那么高。
最后留个思考题:不重要的资源是否需要加锁?
在引入分布式锁的早期,很多同事担心锁竞争问题或是怕组件挂了,打算对不重要的资源不加锁了,只对重要的资源或并发率高的情况加锁,并发率低,没那么重要的资源不加锁。你们怎样看待这个问题?
下篇文章我们一起探讨这个问题。