前言

现在的业务场景越来越复杂,使用的架构也就越来越复杂,分布式、高并发已经是业务要求的常态。像腾讯系的不少服务,还有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的分布式锁就有这么多道道,不知道你是否看明白了?留言讨论下吧!

程序员的小伙伴们,觉得自己孤单么,那就加入公众号[程序员之道],一起交流沟通,走出我们的程序员之道!

redis分布式锁,面试官请随便问,我都会的更多相关文章

  1. 学习任务,阅读一下Redis分布式锁的官方文档

    地址: https://redis.io/topics/distlock 这是一篇质疑RedLock的论文:https://martin.kleppmann.com/2016/02/08/how-to ...

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

    前言 上一篇[面试官问我,使用Dubbo有没有遇到一些坑?我笑了.]之后,又有一位粉丝和我说在面试过程中被虐了.鉴于这位粉丝是之前肥朝的粉丝,而且周一又要开启新一轮的面试,为了回馈他长期以来的支持,所 ...

  3. 面试官再问Redis分布式锁如何续期?这篇文章甩 他一脸

    一.真实案例 二.Redis分布式锁的正确姿势 据肥朝了解,很多同学在用分布式锁时,都是直接百度搜索找一个Redis分布式锁工具类就直接用了.关键是该工具类中还充斥着很多System.out.prin ...

  4. 面试官:你真的了解Redis分布式锁吗?

    什么是分布式锁 说到Redis,我们第一想到的功能就是可以缓存数据,除此之外,Redis因为单进程.性能高的特点,它还经常被用于做分布式锁. 锁我们都知道,在程序中的作用就是同步工具,保证共享资源在同 ...

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

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

  6. Redis分布式锁实现Redisson 15问

    大家好,我是三友. 在一个分布式系统中,由于涉及到多个实例同时对同一个资源加锁的问题,像传统的synchronized.ReentrantLock等单进程情况加锁的api就不再适用,需要使用分布式锁来 ...

  7. 如何正确使用redis分布式锁

    前言   笔者在公司担任技术面试官,在笔者面试过程中,如果面试候选人提到了reids分布式锁,笔者都会问一下redis分布式锁的知识点,但是令笔者遗憾的是,该知识点十个人中有九个人都答得不清楚,或者回 ...

  8. Redis分布式锁 (图解-秒懂-史上最全)

    文章很长,而且持续更新,建议收藏起来,慢慢读! 高并发 发烧友社群:疯狂创客圈(总入口) 奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : 极致经典 + 社群大片好评 < Java 高并发 三 ...

  9. 关于分布式锁原理的一些学习与思考-redis分布式锁,zookeeper分布式锁

    首先分布式锁和我们平常讲到的锁原理基本一样,目的就是确保,在多个线程并发时,只有一个线程在同一刻操作这个业务或者说方法.变量. 在一个进程中,也就是一个jvm 或者说应用中,我们很容易去处理控制,在j ...

随机推荐

  1. duilib学习领悟(3)

    世上本无窗口,窗口只是人的眼睛和电脑屏幕及鼠标键盘相互操作后的视觉效果! 下面我们来看看我们之前讲过的代码: class CDuiFrameWnd : public CWindowWnd, publi ...

  2. gRPC 到 JSON 代理生成器 grpc-gateway

    grpc-gateway是protoc的插件,它读取protobuf服务定义并生成反向代理服务器,该服务将RESTful HTTP API转换为gRPC. 这个服务是根据你的服务定义中的google. ...

  3. VS 项目清理小工具 ClearSolution

    简介 VS项目清理小工具,通过INI配置文件快速清理项目,清除无用的数据库等文件与文件夹,简单实用. 支持平台 Windows 开源许可 ClearSolution 遵循 [Apache 协议],使用 ...

  4. mongodb中find $ne null 与$exists的区别

    $ne null 会把空列表也算入,即使不存在. $exists 的识别效果就比较好 1.插入样例数据 db.nullexistsdemo.insertMany( [{ "name" ...

  5. vs调试 iis发布之后的项目

    方法一 启动vs  访问iis地址 即可调试 方法二 点击调试, 选择附加到进程 选择所有用户进程,  选择w3wp.exe  ,附加 , 即可调试

  6. linux下解决安装jdk后‘环境变量’不生效的问题

    1.是否需要配置环境变量,主要看java -version 显示的版本是否为你期望的版本: (1)不需要配置环境变量的情况 使用java -version查看,版本显示正好是你刚刚安装的版本,这一般为 ...

  7. Asp.Net跨平台 Jexus 5.8.1 独立版

    在Linux上运行ASP.NET网站或WebApi的传统步骤是,先安装libgdiplus,再安装mono,然后安装Jexus.在这个过程中,虽然安装Jexus是挺简便的一件事,但是安装mono就相对 ...

  8. 第一章:Python数据分析前的基础铺垫

    本节概要 - 数据类型 - 数据结构 - 数据的常用操作方法 数据类型 基础铺垫 定义 我们搞数据时,首先要告诉Python我们的数据类型是什么 数值型:直接写一个数字即可 逻辑型:True,Fals ...

  9. scrapy框架之进阶

    五大核心组件 - 引擎(Scrapy) 用来处理整个系统的数据流处理, 触发事务(框架核心) - 调度器(Scheduler) 用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. ...

  10. codeforces#1234F. Yet Another Substring Reverse(子集dp)

    题目链接: https://codeforces.com/contest/1234/problem/F 题意: 给出一个只包含前20个小写字母的字符串,一次操作可以让一段字符颠倒顺序 最多一次这样的操 ...