服务器之家:专注于服务器技术及软件下载分享
分类导航

Mysql|Sql Server|Oracle|Redis|MongoDB|PostgreSQL|Sqlite|DB2|mariadb|Access|数据库技术|

服务器之家 - 数据库 - Redis - Redis分布式锁的实现方式(redis面试题)

Redis分布式锁的实现方式(redis面试题)

2020-03-01 14:24程序员之道 Redis

这篇文章主要介绍了Redis分布式锁的实现方式(面试常见),需要的朋友可以参考下

 

什么是分布式锁?

要介绍分布式锁,首先要提到与分布式锁相对应的是线程锁、进程锁。

线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。

进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。

分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。

 

前言

现在的业务场景越来越复杂,使用的架构也就越来越复杂,分布式、高并发已经是业务要求的常态。像腾讯系的不少服务,还有CDN优化、异地多备份等处理。

说到分布式,就必然涉及到分布式锁的概念,如何保证不同机器不同线程的分布式锁同步呢?

 

实现要点

  1. 互斥性,同一时刻,智能有一个客户端持有锁。
  2. 防止死锁发生,如果持有锁的客户端崩溃没有主动释放锁,也要保证锁可以正常释放及其他客户端可以正常加锁。
  3. 加锁和释放锁必须是同一个客户端。
  4. 容错性,只有redis还有节点存活,就可以进行正常的加锁解锁操作。
 

正确的redis分布式锁实现

 

错误加锁方式

错误方式一

保证互斥和防止死锁,首先想到的使用redis的setnx命令保证互斥,为了防止死锁,锁需要设置一个超时时间。

?
1
2
3
4
5
6
7
public static void wrongLock(Jedis jedis, String key, String uniqueId, int expireTime) {
 Long result = jedis.setnx(key, uniqueId);
 if (1 == result) {
  //如果该redis实例崩溃,那就无法设置过期时间了
  jedis.expire(key, expireTime);
 }
}

在多线程并发环境下,任何非原子性的操作,都可能导致问题。这段代码中,如果设置过期时间时,redis实例崩溃,就无法设置过期时间。如果客户端没有正确的释放锁,那么该锁(永远不会过期),就永远不会被释放。

错误方式二

比较容易想到的就是设置值和超时时间为原子原子操作就可以解决问题。那使用setnx命令,将value设置为过期时间不就ok了吗?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static boolean wrongLock(Jedis jedis, String key, int expireTime) {
  long expireTs = System.currentTimeMillis() + expireTime;
  // 锁不存在,当前线程加锁成果
  if (jedis.setnx(key, String.valueOf(expireTs)) == 1) {
   return true;
  }
 
  String value = jedis.get(key);
  //如果当前锁存在,且锁已过期
  if (value != null && NumberUtils.toLong(value) < System.currentTimeMillis()) {
   //锁过期,设置新的过期时间
   String oldValue = jedis.getSet(key, String.valueOf(expireTs));
   if (oldValue != null && oldValue.equals(value)) {
    // 多线程并发下,只有一个线程会设置成功
    // 设置成功的这个线程,key的旧值一定和设置之前的key的值一致
    return true;
   }
  }
  // 其他情况,加锁失败
  return true;
 }

乍看之下,没有什么问题。但仔细分析,有如下问题:

value设置为过期时间,就要求各个客户端严格的时钟同步,这就需要使用到同步时钟。即使有同步时钟,分布式的服务器一般来说时间肯定是存在少许误差的。

锁过期时,使用 jedis.getSet虽然可以保证只有一个线程设置成功,但是不能保证加锁和解锁为同一个客户端,因为没有标志锁是哪个客户端设置的嘛。

 

错误解锁方式

解锁错误方式一

直接删除key

?
1
2
3
4
public static void wrongReleaseLock(Jedis jedis, String key) {
  //不是自己加锁的key,也会被释放
  jedis.del(key);
 }

简单粗暴,直接解锁,但是不是自己加锁的,也会被删除,这好像有点太随意了吧!

解锁错误方式二

判断自己是不是锁的持有者,如果是,则只有持有者才可以释放锁。

?
1
2
3
4
5
6
public static void wrongReleaseLock(Jedis jedis, String key, String uniqueId) {
 if (uniqueId.equals(jedis.get(key))) {
  // 如果这时锁过期自动释放,又被其他线程加锁,该线程就会释放不属于自己的锁
  jedis.del(key);
 }
}

看起来很完美啊,但是如果你判断的时候锁是自己持有的,这时锁超时自动释放了。然后又被其他客户端重新上锁,然后当前线程执行到jedis.del(key),这样这个线程不就删除了其他线程上的锁嘛,好像有点乱套了哦!

 

正确加锁释放锁方式

基本上避免了以上几种错误方式之外,就是正确的方式了。要满足以下几个条件:

命令必须保证互斥

设置的key必须要有过期时间,防止崩溃时锁无法释放

value使用唯一id标志每个客户端,保证只有锁的持有者才可以释放锁

加锁直接使用set命令同时设置唯一id和过期时间;其中解锁稍微复杂些,加锁之后可以返回唯一id,标志此锁是该客户端锁拥有;释放锁时要先判断拥有者是否是自己,然后删除,这个需要redis的lua脚本保证两个命令的原子性执行。

下面是具体的加锁和释放锁的代码:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Slf4j
public class RedisDistributedLock {
 private static final String LOCK_SUCCESS = "OK";
 private static final Long RELEASE_SUCCESS = 1L;
 private static final String SET_IF_NOT_EXIST = "NX";
 private static final String SET_WITH_EXPIRE_TIME = "PX";
 // 锁的超时时间
 private static int EXPIRE_TIME = 5 * 1000;
 // 锁等待时间
 private static int WAIT_TIME = 1 * 1000;
 private Jedis jedis;
 private String key;
 public RedisDistributedLock(Jedis jedis, String key) {
  this.jedis = jedis;
  this.key = key;
 }
 // 不断尝试加锁
 public String lock() {
  try {
   // 超过等待时间,加锁失败
   long waitEnd = System.currentTimeMillis() + WAIT_TIME;
   String value = UUID.randomUUID().toString();
   while (System.currentTimeMillis() < waitEnd) {
    String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, EXPIRE_TIME);
    if (LOCK_SUCCESS.equals(result)) {
     return value;
    }
    try {
     Thread.sleep(10);
    } catch (InterruptedException e) {
     Thread.currentThread().interrupt();
    }
   }
  } catch (Exception ex) {
   log.error("lock error", ex);
  }
  return null;
 }
 public boolean release(String value) {
  if (value == null) {
   return false;
  }
  // 判断key存在并且删除key必须是一个原子操作
  // 且谁拥有锁,谁释放
  String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  Object result = new Object();
  try {
   result = jedis.eval(script, Collections.singletonList(key),
     Collections.singletonList(value));
   if (RELEASE_SUCCESS.equals(result)) {
    log.info("release lock success, value:{}", value);
    return true;
   }
  } catch (Exception e) {
   log.error("release lock error", e);
  } finally {
   if (jedis != null) {
    jedis.close();
   }
  }
  log.info("release lock failed, value:{}, result:{}", value, result);
  return false;
 }
}

单是一个redis的分布式锁就有这么多道道,不知道你是否看明白了?留言讨论下吧!

总结

以上所述是小编给大家介绍的Redis分布式锁的实现方式(redis面试题),希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对服务器之家网站的支持!如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

原文链接:https://blog.csdn.net/chanllenge/article/details/102983597

延伸 · 阅读

精彩推荐
  • RedisRedis存取序列化与反序列化性能问题详解

    Redis存取序列化与反序列化性能问题详解

    这篇文章主要给大家介绍了关于Redis存取序列化与反序列化性能问题的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参...

    这名字已经存在9742021-02-24
  • Redis聊一聊Redis与MySQL双写一致性如何保证

    聊一聊Redis与MySQL双写一致性如何保证

    一致性就是数据保持一致,在分布式系统中,可以理解为多个节点中数据的值是一致的。本文给大家分享Redis与MySQL双写一致性该如何保证,感兴趣的朋友一...

    mind_programmonkey6432021-08-12
  • RedisLinux Redis 的安装步骤详解

    Linux Redis 的安装步骤详解

    这篇文章主要介绍了 Linux Redis 的安装步骤详解的相关资料,希望大家通过本文能掌握如何安装Redis,需要的朋友可以参考下 ...

    carl-zhao3822019-11-08
  • Redis在ssm项目中使用redis缓存查询数据的方法

    在ssm项目中使用redis缓存查询数据的方法

    本文主要简单的使用Java代码进行redis缓存,即在查询的时候先在service层从redis缓存中获取数据。如果大家对在ssm项目中使用redis缓存查询数据的相关知识感...

    caychen8962019-11-12
  • Redis就这?Redis持久化策略——AOF

    就这?Redis持久化策略——AOF

    今天为大家介绍Redis的另一种持久化策略——AOF。注意:AOF文件只会记录Redis的写操作命令,因为读命令对数据的恢复没有任何意义...

    头发茂密的刘叔4052021-12-14
  • Redisredis启动,停止,及端口占用处理方法

    redis启动,停止,及端口占用处理方法

    今天小编就为大家分享一篇redis启动,停止,及端口占用处理方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧 ...

    澄海单挑狂5152019-11-14
  • RedisRedis数据结构之链表与字典的使用

    Redis数据结构之链表与字典的使用

    这篇文章主要介绍了Redis数据结构之链表与字典的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友...

    白泽来了4052021-08-03
  • RedisRedis分布式锁升级版RedLock及SpringBoot实现方法

    Redis分布式锁升级版RedLock及SpringBoot实现方法

    这篇文章主要介绍了Redis分布式锁升级版RedLock及SpringBoot实现,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以...

    等不到的口琴7802021-07-25