文章有点长并且绕,先来个图片缓冲下!

前言

现在的业务场景越来越复杂,使用的架构也就越来越复杂,分布式、高并发已经是业务要求的常态。像腾讯系的不少服务,还有CDN优化、异地多备份等处理。
说到分布式,就必然涉及到分布式锁的概念,如何保证不同机器不同线程的分布式锁同步呢?

实现要点

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

正确的redis分布式锁实现

错误加锁方式

错误方式一

保证互斥和防止死锁,首先想到的使用redis的setnx命令保证互斥,为了防止死锁,锁需要设置一个超时时间。
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了吗?
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;
}

乍看之下,没有什么问题。但仔细分析,有如下问题:
  1. value设置为过期时间,就要求各个客户端严格的时钟同步,这就需要使用到同步时钟。即使有同步时钟,分布式的服务器一般来说时间肯定是存在少许误差的。
  2. 锁过期时,使用 jedis.getSet虽然可以保证只有一个线程设置成功,但是不能保证加锁和解锁为同一个客户端,因为没有标志锁是哪个客户端设置的嘛。

错误解锁方式

解锁错误方式一

直接删除key
public static void wrongReleaseLock(Jedis jedis, String key) {
//不是自己加锁的key,也会被释放
jedis.del(key);
}

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

解锁错误方式二

判断自己是不是锁的持有者,如果是,则只有持有者才可以释放锁。
public static void wrongReleaseLock(Jedis jedis, String key, String uniqueId) {
if (uniqueId.equals(jedis.get(key))) {
// 如果这时锁过期自动释放,又被其他线程加锁,该线程就会释放不属于自己的锁
jedis.del(key);
}
}

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

正确加锁释放锁方式

基本上避免了以上几种错误方式之外,就是正确的方式了。要满足以下几个条件:
  1. 命令必须保证互斥
  2. 设置的key必须要有过期时间,防止崩溃时锁无法释放
  3. value使用唯一id标志每个客户端,保证只有锁的持有者才可以释放锁
加锁直接使用set命令同时设置唯一id和过期时间;其中解锁稍微复杂些,加锁之后可以返回唯一id,标志此锁是该客户端锁拥有;释放锁时要先判断拥有者是否是自己,然后删除,这个需要redis的lua脚本保证两个命令的原子性执行。
下面是具体的加锁和释放锁的代码:

@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的分布式锁就有这么多道道,不知道你是否看明白了?留言讨论下吧!

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

redis分布式锁,面试官请随便问,我都会相关推荐

  1. Redis分布式锁(Redlock官方文档的理解)

    Redis分布式锁(Redlock官方文档的理解) 我github博客原文 官网解释 分布式锁在许多不同进程下需要对共享资源进行互斥操作的环境下,十分需要 Redis作者提出了 Redlock 算法 ...

  2. redis反杀面试官之10问

    简言 1. 笔者近两年来一直使用redis,也对redis有过仔细的研究,不敢说精通,熟悉至少是有的 2. redis越来越火,网上相应的文章,总结,面试问题也有很多,但大多是应付简单面试用的,如果面 ...

  3. 面试连环问之Redis分布式锁

    面试连环问之Redis分布式锁 面试连环问之Redis分布式锁 核心 主要介绍 主要代码 1.新建RedisLock 2.使用Redis锁遇到的问题 面试连环问之Redis分布式锁 核心 Redis分 ...

  4. 面试官问我,Redis分布式锁如何续期?懵了。

    作者:肥朝,来自:肥朝(ID:feichao_java) 前言 上一篇[面试官问我,使用Dubbo有没有遇到一些坑?我笑了.]之后,又有一位粉丝和我说在面试过程中被虐了.鉴于这位粉丝是之前肥朝的老粉丝 ...

  5. redis desktop manager_面试官:Redis分布式锁如何解决锁超时问题?

    Java面试笔试面经.Java技术每天学习一点 Java面试 关注不迷路 作者:wangzaiplus 来源:https://www.jianshu.com/u/8cb4591440ca 一.前言 关 ...

  6. redis续期_面试官:Redis分布式锁如何解决锁超时问题的?

    一.前言 关于redis分布式锁, 查了很多资料, 发现很多只是实现了最基础的功能, 但是, 并没有解决当锁已超时而业务逻辑还未执行完的问题, 这样会导致: A线程超时时间设为10s(为了解决死锁问题 ...

  7. 面试必问:如何实现Redis分布式锁

    摘要:今天我们来聊聊分布式锁这块知识,具体的来看看Redis分布式锁的实现原理. 一.写在前面 现在面试,一般都会聊聊分布式系统这块的东西.通常面试官都会从服务框架(Spring Cloud.Dubb ...

  8. 面试官:Redis分布式锁解决方案是什么?

    今天博主在这片文章中主要给大家讲下Redis分布式锁的原理以及解决方案 学到三连呦 1.Redis分布式锁原理 1.1.简述 我们知道分布式锁的特性是排他.避免死锁.高可用.分布式锁的实现可以通过数据 ...

  9. 秒杀商品超卖事故:Redis分布式锁请慎用!

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:浪漫先生 来源:juejin.im/post/6854573 ...

最新文章

  1. Matlab编程与数据类型 -- 出错处理语句try/catch/end
  2. diff算法_React源码揭秘(三):Diff算法详解
  3. mysql面试关联查询语句_sql语句多表联查语句的练习(面试题)
  4. DataSource数据源定义
  5. Android中的IPC方式AIDL
  6. php 利用qrcode生成二维码
  7. 【Web前端】儿童摄影网——网页制作代码
  8. 联合阿里在职测开工程师耗时一个星期写的 【接口测试+自动化接口接口测试详解]
  9. coffeescript基本语法
  10. uiautomator xpath筛选父级通框架内子元素调用
  11. AI+医疗如何商业破冰?6位一线专家这样说
  12. Vue项目在页面添加水印功能
  13. 插入(希尔)排序时间、空间复杂度
  14. 静态URL和动态URL有什么区别呢?
  15. php 量化交易 开源,hikyuu开源量化交易研究框架 v1.0
  16. AndroidOTA升级流程
  17. vue项目适配 rem
  18. Exchange 2007 术语表
  19. Echarts主题和调色盘以及颜色渐变
  20. 大疆OSDK开发-在模拟器中画个一箭穿心

热门文章

  1. ai可以滚轮缩放吗_AI侵入艺术天堂!艺术也可以“量产”了吗?
  2. matlab调用c函数语言,MATLAB调用C/C++函数的方法
  3. 高温保护_【美的空调维修案例】P2压缩机高温保护 不定时出现P1过欠压保护...
  4. word图片填充之后图片颜色变灰了_一文读懂如何快速实现网页页面变灰?
  5. python怎么创建变量_Python之变量的创建过程
  6. python可变对象与不可变对象_python 可变对象与不可变对象
  7. 动态规划 - 最长递增子序列LIS
  8. (软件工程复习核心重点)第十章面向对象设计-第四节:设计人机交互子系统和设计任务管理子系统
  9. Jenkins-CI 远程代码执行漏洞复现(CVE-2017-1000353)
  10. deb下载路径、apt下载路径(deb路径、apt路径)、自定义打包deb文件、安装deb文件、解压deb文件