1、为什么会出现分布式锁

  • 效率:使用分布式锁可以避免不同节点重复相同的工作,这些工作会浪费资源。比如用户付了钱之后有可能不同节点会发出多封短信。
  • 正确性:加分布式锁同样可以避免破坏正确性的发生,如果两个节点在同一条数据上面操作,比如多个节点机器对同一个订单操作不同的流程有可能会导致该笔订单最后状态出现错误,造成损失。

2、分布式锁一般有三种实现方式

  • 基于 MySQL中的锁MySQL 本身有自带的悲观锁 for update 关键字,也可以自己实现悲观/乐观锁来达到目的;
  • 基于 Zookeeper 有序节点Zookeeper允许临时创建有序的子节点,这样客户端获取节点列表时,就能够当前子节点列表中的序号判断是否能够获得锁;
  • 基于 Redis的单线程:由于 Redis 是单线程,所以命令会以串行的方式执行,并且本身提供了像 SETNX(set if not exists) 这样的指令,本身具有互斥性;
  • 自研分布式锁:如谷歌的 Chubby

每个方案都有各自的优缺点,例如 MySQL 虽然直观理解容易,但是实现起来却需要额外考虑 锁超时加事务 等,并且性能局限于数据库,诸如此类我们在此不作讨论,重点关注 Redis

使用Redis作为分布式锁的优点:对于Redis实现简单,性能对比 ZookeeperMysql较好。如果不需要特别复杂的要求,那么自己就可以利用setNx进行实现,如果自己需要复杂的需求的话那么可以利用或者借鉴Redission。对于一些要求比较严格的场景来说的话可以使用RedLock

使用redis作为分布式锁的缺点:需要维护Redis集群,如果要实现RedLock那么需要维护更多的集群。

3、分布式锁的条件

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件

  • 互斥性:在任意时刻,只有一个客户端能持有锁;
  • 不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁;
  • 具有容错性:只要大部分的Redis节点正常运行,客户端就可以加锁和解锁;
  • 解铃还须系铃人:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

3.1加锁的代码实现

setNx resourceName value

若加锁了之后如果机器宕机那么这个锁就不会得到释放所以会加入过期时间,加入过期时间需要和setNx同一个原子操作,在Redis2.8之前我们需要使用Lua脚本达到我们的目的,之前的setnxexpire无法保证原子性,但是Redis2.8之后Redis支持nxex操作是同一原子操作。

这是Redis2.8之后扩展了set方法的参数:

public class RedisTool {
 
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
 /**  * 尝试获取分布式锁  * @param jedis Redis客户端  * @param lockKey 锁  * @param requestId 请求标识  * @param expireTime 超期时间  * @return 是否获取成功  */  public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {   String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);   if (LOCK_SUCCESS.equals(result)) {  return true;  }  return false;   }  }

其实加锁就一行代码:

jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

  • 第一个为 key,我们使用 key来当锁,因为 key是唯一的。
  • 第二个为 value,我们传的是 requestId,很多童鞋可能不明白,有 key作为锁不就够了吗,为什么还要用到 value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件 解铃还须系铃人,通过给 value赋值为 requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。 requestId可以使用 UUID.randomUUID().toString()方法生成。
  • 第三个为 nxxx,这个参数我们填的是 NX,意思是 SET IF NOT EXIST,即当 key不存在时,我们进行 set操作;若 key已经存在,则不做任何操作;
  • 第四个为 expx,这个参数我们传的是 PX,意思是我们要给这个 key加一个过期的设置,具体时间由第五个参数决定。
  • 第五个为 time,与第四个参数相呼应,代表 key的过期时间。

3.2、解锁的代码实现:

public class RedisTool {
 
    private static final Long RELEASE_SUCCESS = 1L;
 
    /**  * 释放分布式锁  * @param jedis Redis客户端  * @param lockKey 锁  * @param requestId 请求标识  * @return 是否释放成功  */  public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {   String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";  Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));  if (RELEASE_SUCCESS.equals(result)) {  return true;  }  return false;  } }

4、Redission

4.1、应用背景

Jedis出现的时间比较长了,接触Redis比较早的人可能使用的都是Jedis,但是随着现代系统的多核和异步,为了不断提高的吞吐量,异步非阻塞线程模型大行其道,这里面非常热门的框架就是NettyNetty因其设计优秀,应用面广,实际使用的场景广泛,很多大型框架比如hadoop,dubbo等许多的底层都是通过Netty来实现的通信。Redission就是Redis基于Netty封装的通信客户端。

Redission,官网地址是: https://redisson.org/,中文文档地址是 https://github.com/redisson/redisson/wiki/目录。

4.2、使用

<!--Maven-->
<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.10.4</version>
</dependency>
// 1. Create config object
Config = ...
// 2. Create Redisson instance
RedissonClient redisson = Redisson.create(config);
// 3. Get Redis based object or service you need
RMap<MyKey, MyValue> map = redisson.getMap("myMap");  RLock lock = redisson.getLock("myLock") lock.lock(); //业务代码 lock.unlock();

4.3、优缺点

  • 优点 支持Redis单实例、Redis哨兵、Redis clusterRedis master-slave等各种部署架构,基于Redis所以具有Redis 功能使用的封装,功能齐全。许多公司试用后可以用到企业级项目中,社区活跃度高。
  • 缺点 最大的问题,就是如果你对某个Redis master实例,写入了myLock这种锁keyvalue,此时会异步复制给对应的master slave实例。但是这个过程中一旦发生Redis master宕机,主备切换,Redis slave变为了Redis master,接着就会导致,客户端2来尝试加锁的时候,在新的Redis master上完成了加锁,而客户端1也以为自己成功加了锁。
    此时就会导致多个客户端对一个分布式锁完成了加锁。这时系统在业务语义上一定会出现问题,导致脏数据的产生。所以这个就是Redis cluster,或者是Redis master-slave架构的主从异步复制导致的Redis分布式锁的最大缺陷:在Redis master实例宕机的时候,可能导致多个客户端同时完成加锁。