Fork me on GitHub

Redis - 分布式锁

注意:所有文章除特别说明外,转载请注明出处.

Redis - 分布式

[TOC]

分布式锁(多JVM)

控制分布式系统有序的去对共享资源进行操作,通过互斥来保持一致性

实现原理

  • 互斥性:保证同一时间只有一个客户端可以拿到锁,也就是可以对共享资源进行操作

  • 安全性:只有加锁的服务才能有解锁权限,也就是不能让a加的锁,bcd都可以解锁,如果都能解锁那分布式锁就没啥意义了

    可能出现的情况就是a去查询发现持有锁,就在准备解锁,这时候忽然a持有的锁过期了,然后b去获得锁,因为a锁过期,b拿到锁,这时候a继续执行第二步进行解锁如果不加校验,就将b持有的锁就给删除了

  • 避免死锁:出现死锁就会导致后续的任何服务都拿不到锁,不能再对共享资源进行任何操作了

  • 保证加锁与解锁操作是原子性操作

    这个其实属于是实现分布式锁的问题,假设a用redis实现分布式锁,假设加锁操作,操作步骤分为两步:1. 设置key set(key,value)。2. 给key设置过期时间,假设现在a刚实现set后,程序崩了就导致了没给key设置过期时间就导致key一直存在就发生了死锁。

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

  • 互斥性:在任意时刻,只有一个客户端能持有锁

  • 不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁

  • 具有容错性:只要大部分的Redis节点正常运行,客户端就可以加锁和解锁

  • 解铃还须系铃人:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解

如何实现分布式锁?

使用redis实现分布式锁
set key value ex time nx
set()多参数,高版本jedis使用
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;
      }
}
  • 第一个为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的过期时间

set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端

1
2
3
4
5
6
7
8
9
 public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {
Long result = jedis.setnx(lockKey, requestId);
if (result == 1) {
// 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
jedis.expire(lockKey, expireTime);
}
}

setnx()方法作用就是SET IF NOT EXIST,expire()方法就是给锁加一个过期时间。乍一看好像和前面的set()方法结果一样,然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁。网上之所以有人这样实现,是因为低版本的jedis并不支持多参数的set()方法

解锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  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) {
//首先获取锁对应的value值,检查是否与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;
}
}
//就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。
错误解锁
1
2
3
4
5
6
7
public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {       
// 判断加锁与解锁是不是同一个客户端
if (requestId.equals(jedis.get(lockKey))) {
// 若在此时,这把锁突然不是这个客户端的,则会误解锁
jedis.del(lockKey);
}
}

存在问题:问题在于如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了

setnx + 过期时间依旧存在问题

在高并发场景下,会出现误删不是自己的锁,删除掉别人的锁,出现一个锁永久性失效的问题

使用uuid解决,判断clientId跟redis中那把锁的value值是否一致,一致的话,才能进行删除锁。但还会出现有多个线程执行同一个代码块,对超时的锁进行续命,那么可以尝试使用==Redisson==实现分布式锁,这是Redis官方提供的Java组件。

  • 引入依赖

  • 在启动类中注入 redisson 的bean,可以在配置文件中添加redis的连接地址、参数,然后在代码中引入配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @SpringBootApplication
    public class RedissonApplication {
    public static void main(String[] args) {
    SpringApplication.run(RedissonApplication.class, args);
    }
    @Bean
    Redisson redissonSentinel() {
    //支持单机,主从,哨兵,集群等模式
    //此为哨兵模式
    Config config = new Config();
    config.useSentinelServers()
    .setMasterName("mymaster")
    .addSentinelAddress("redis://192.168.1.1:26379")
    .setPassword("123456");
    return (Redisson)Redisson.create(config);
    }
    }
  • 在redis中,设置一个库存数量为100

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @Test
    public void test() throws Exception {
    //设置一个key,aaa商品的库存数量为100
    stringRedisTemplate.opsForValue().set("aaa","100");
    Assert.assertEquals("100", stringRedisTemplate.opsForValue().get("aaa"));
    }

    String lockKey = "testRedisson";//分布式锁的key
    @Test
    public void testDistributed(){
    //执行的业务代码
    for(int i=0; i < 55; i++){
    RLock lock = redisson.getLock(lockKey);
    lock.lock(60, TimeUnit.SECONDS); //设置60秒自动释放锁 (默认是30秒自动过期)
    int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("aaa").toString());
    if(stock > 0){
    stringRedisTemplate.opsForValue().set("aaa",(stock-1)+"");
    System.out.println("test2_:lockkey:"+lockKey+",stock:"+(stock-1)+"");
    }
    lock.unlock(); //释放锁
    }
    }

本文标题:Redis - 分布式锁

文章作者:Bangjin-Hu

发布时间:2019年10月15日 - 09:22:26

最后更新:2020年03月30日 - 08:03:13

原始链接:http://bangjinhu.github.io/undefined/Redis - 分布式锁/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

Bangjin-Hu wechat
欢迎扫码关注微信公众号,订阅我的微信公众号.
坚持原创技术分享,您的支持是我创作的动力.