点击上方 果汁简历 ,选择“置顶公众号”

优质文章,第一时间送达

作者:杨高超

https://urlify.cn/INZjAf

在现代的编程语言中,接触过多线程编程的程序员多多少少对锁有一定的了解。简单的说,多线程中的锁就是在多线程环境下,多个线程对共享资源进行修改的时候,保证共享资源一致性的机制。这里不展开说。在分布式环境下,原来的多线程的锁就不管用了,也就出现了分布式锁的需求。所谓分布式锁服务也就是在分布式环境下,保证多个分布式的服务共享的资源一致性的服务。

在分布式环境下实现一个分布式锁服务并不太容易,需要考虑很多在单进程下的锁服务不需要考虑的问题。分布式锁锁的实现也有很多。这里我们讨论在 Java 中通过 redis 来实现。

在 GitHub 中的 redisson 项目中已经有开源的实现。但是那个太复杂了。现在我们来基于单机的 redis 实现一个简单的分布式锁服务。这个服务必须满足下面的要求

  • 支持立即获取锁方式,如果获取到返回true,获取不到则返回false;

  • 支持等待获取锁方式,如果获取到,直接返回true,获取不到在等待一小段时间,在这一小段时间内反复尝试,如果尝试成功,则返回true,等待时间过后还获取不到则返回false;

  • 不能产生死锁的情况;

  • 不能释放非自己加的锁;

下面我们用实例来演示在 Java 中利用 redis 实现分布式锁服务

加锁

通过 redis 来实现分布式锁的加锁逻辑如下所示:

根据这个逻辑,实现上锁的核心代码如下所示:

jedis.select(dbIndex);
String key = KEY_PRE + key;
String value = fetchLockValue();
if(jedis.exists(key)){jedis.set(key,value);jedis.expire(key,lockExpirseTime);return value;
}

表面上看这段代码好像没有什么问题,实际上并不能在分布式环境中正确的实现加锁的操作。要能够正确的实现加锁操作,“判断 key 是否存在”、“保存 key-value”、“设置 key 的过期时间”这三步操作必须是原子操作。如果不是原子操作,那么可能会出现下面两种情况:

  • “判断 key 是否存在”得出 key 不存在的结果步骤后,“保存 key-value”步骤前,另一个客户端执行同样的逻辑,并且执行到了“判断 key 是否存在”步骤,同样得出了 key 不存在的结果。这样回导致多个客户端获得了同一把锁;

  • 在客户端执行完“保存 key-value” 步骤后,需要设置一个 key 的过期时间,防止客户端因为代码质量未解锁,在或者进程崩溃未解锁导致的死锁情况。在“保存 key-value”步骤之后,“设置 key 的过期时间”步骤之前,可能进程崩溃,导致“设置 key 的过期时间”步骤失败;

redis 在2.6.12版本之后,对 set 命令进行了扩充,能够规避上面的两个问题。新版的 redis set 命令的参数如下

SET key value [EX seconds] [PX milliseconds] [NX|XX]

新版的 set 命令增加了 EX 、 PX 、 NX|XX 参数选项。他们的含义如下

  • EX seconds – 设置键 key 的过期时间,单位时秒

  • PX milliseconds – 设置键 key 的过期时间,单位时毫秒

  • NX – 只有键 key 不存在的时候才会设置 key 的值

  • XX – 只有键 key 存在的时候才会设置 key 的值

这样,原来的三步操作就可以在一个 set 的原子操作里面来完成,规避了上面我们提到的两个问题。

新版的 redis 加锁核心代码修改如下所示:

jedis = redisConnection.getJedis();
jedis.select(dbIndex);
String key = KEY_PRE + key;
String value = fetchLockValue();
if ("OK".equals(jedis.set(key, value, "NX", "EX", lockExpirseTime))) {return value;
}

解锁

解锁的基本流程如下:

根据这个逻辑,在 Java 中解锁的核心代码如下所示:

jedis.select(dbIndex);
String key = KEY_PRE + key;
if(jedis.exists(key) && value.equals(jedis.get(key))){jedis.del(key);return true;
}
return false;

和加锁的时候一样,key 是否存在、判断是否自己持有锁、**删除 key-value **这三步操作需要是原子操作,否则当一个客户端执行完“判断是否自己持有锁”步骤后,得出自己持有锁的结论,此时锁的过期时间到了,自动被 redis 释放了,同时另一个客户端又基于这个 key 加锁成功,如果第一个客户端还继续执行删除 key-value的操作,就将不属于自己的锁给释放了。

这显然是不运行的。在这里我们利用 redis 执行 Lua 脚本的能力来解决原子操作的问题。修改后的解锁核心代码如下所示:

jedis.select(dbIndex);
String key = KEY_PRE + key;
String command = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
if (1L.equals(jedis.eval(command, Collections.singletonList(key), Collections.singletonList(value)))) {return true;
}

另外,判断是否自己持有锁的机制是用加锁的时候的 key-value 来判断当前的 key 的值是否等于自己持有锁时获得的值。所以加锁的时候的 value 必须是一个全局唯一的字符串。扩展:分布式全局唯一ID生成策略

完整的代码如下所示

package com.x9710.common.redis.impl;import com.x9710.common.redis.LockService;
import com.x9710.common.redis.RedisConnection;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import redis.clients.jedis.Jedis;import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.UUID;/*** 分布式锁 redis 实现** @author 杨高超* @since 2017-12-14*/
public class LockServiceRedisImpl implements LockService {private static Log log = LogFactory.getLog(LockServiceRedisImpl.class);private static String SET_SUCCESS = "OK";private static String KEY_PRE = "REDIS_LOCK_";private DateFormat df = new SimpleDateFormat("yyyyMMddHHmmssSSS");private RedisConnection redisConnection;private Integer dbIndex;private Integer lockExpirseTime;private Integer tryExpirseTime;public void setRedisConnection(RedisConnection redisConnection) {this.redisConnection = redisConnection;
}public void setDbIndex(Integer dbIndex) {this.dbIndex = dbIndex;
}public void setLockExpirseTime(Integer lockExpirseTime) {this.lockExpirseTime = lockExpirseTime;
}public void setTryExpirseTime(Integer tryExpirseTime) {this.tryExpirseTime = tryExpirseTime;
}public String lock(String key) {Jedis jedis = null;try {jedis = redisConnection.getJedis();jedis.select(dbIndex);key = KEY_PRE + key;String value = fetchLockValue();if (SET_SUCCESS.equals(jedis.set(key, value, "NX", "EX", lockExpirseTime))) {log.debug("Reids Lock key : " + key + ",value : " + value);return value;}} catch (Exception e) {e.printStackTrace();} finally {if (jedis != null) {jedis.close();}}return null;
}public String tryLock(String key) {Jedis jedis = null;try {jedis = redisConnection.getJedis();jedis.select(dbIndex);key = KEY_PRE + key;String value = fetchLockValue();Long firstTryTime = new Date().getTime();do {if (SET_SUCCESS.equals(jedis.set(key, value, "NX", "EX", lockExpirseTime))) {log.debug("Reids Lock key : " + key + ",value : " + value);return value;}log.info("Redis lock failure,waiting try next");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}} while ((new Date().getTime() - tryExpirseTime * 1000) < firstTryTime);} catch (Exception e) {e.printStackTrace();} finally {if (jedis != null) {jedis.close();}}return null;
}public boolean unLock(String key, String value) {Long RELEASE_SUCCESS = 1L;Jedis jedis = null;try {jedis = redisConnection.getJedis();jedis.select(dbIndex);key = KEY_PRE + key;String command = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";if (RELEASE_SUCCESS.equals(jedis.eval(command, Collections.singletonList(key), Collections.singletonList(value)))) {return true;}} catch (Exception e) {e.printStackTrace();} finally {if (jedis != null) {jedis.close();}}return false;
}/*** 生成加锁的唯一字符串** @return 唯一字符串*/
private String fetchLockValue() {return UUID.randomUUID().toString() + "_" + df.format(new Date());
}
}

测试代码

package com.x9710.common.redis.test;import com.x9710.common.redis.RedisConnection;
import com.x9710.common.redis.impl.LockServiceRedisImpl;public class RedisLockTest {public static void main(String[] args) {for (int i = 0; i < 9; i++) {new Thread(new Runnable() {public void run() {RedisConnection redisConnection = RedisConnectionUtil.create();LockServiceRedisImpl lockServiceRedis = new LockServiceRedisImpl();lockServiceRedis.setRedisConnection(redisConnection);lockServiceRedis.setDbIndex(15);lockServiceRedis.setLockExpirseTime(20);String key = "20171228";String value = lockServiceRedis.lock(key);try {if (value != null) {System.out.println(Thread.currentThread().getName() + " lock key = " + key + " success! ");Thread.sleep(25 * 1000);}else{System.out.println(Thread.currentThread().getName() + " lock key = " + key + " failure! ");}} catch (Exception e) {e.printStackTrace();} finally {if (value == null) {value = "";}System.out.println(Thread.currentThread().getName() + " unlock key = " + key + " " + lockServiceRedis.unLock(key, value));}}}).start();}
}
}

测试结果

Thread-1 lock key = 20171228 failure!
Thread-2 lock key = 20171228 failure!
Thread-4 lock key = 20171228 failure!
Thread-8 lock key = 20171228 failure!
Thread-7 lock key = 20171228 failure!
Thread-3 lock key = 20171228 failure!
Thread-5 lock key = 20171228 failure!
Thread-0 lock key = 20171228 failure!
Thread-6 lock key = 20171228 success!
Thread-1 unlock key = 20171228 false
Thread-2 unlock key = 20171228 false
Thread-4 unlock key = 20171228 false
Thread-8 unlock key = 20171228 false
Thread-3 unlock key = 20171228 false
Thread-5 unlock key = 20171228 false
Thread-0 unlock key = 20171228 false
Thread-7 unlock key = 20171228 false
Thread-6 unlock key = 20171228 true

从测试结果来看可以看到,9个线程同时给一个 key 加锁,只有一个能够成功获取到锁,其余的客户端都无法获取到锁。

后记

这个代码里面还实现了一个 tryLock 的接口。这个主要是客户端无法获取到锁的时候会在一小段时间内反复尝试是否能够获取到锁。

文中代码

https://github.com/gaochao2000/redis_util

END

往期精彩回顾

美团技术大佬写给工程师的十条精进原则

为什么优秀的程序员都写博客?

29 岁成为阿里巴巴 P8,工作前 5 年完成晋升 3 连跳,他如何做到?

点个赞呗

面试必考:在 Java 中如何利用 redis 实现一个分布式锁服务相关推荐

  1. 小王,在 Java 中如何利用 redis 实现一个分布式锁服务呢???

    作者:杨高超 juejin.im/post/5a4984af6fb9a0450b66bc57 在现代的编程语言中,接触过多线程编程的程序员多多少少对锁有一定的了解.简单的说,多线程中的锁就是在多线程环 ...

  2. 【Redis笔记】一起学习Redis | 如何利用Redis实现一个分布式锁?

    一起学习Redis | 如何利用Redis实现一个分布式锁? 前提知识 什么是分布式锁? 为什么需要分布式锁? 分布式锁的5要素和三种实现方式 实现分布式锁 思考思考 基础方案 改进方案 保证setn ...

  3. 面试必考之Java中String是基础类型?是包装类型?

    我们都知道,Java中String不属于基础数据类型.基础类型只有8中基本数据类型:byte.short.int.long.float.double.char.boolean,而String是最常用到 ...

  4. 十九、面试必考,Java中的this关键字

    @Author:Runsen @Date:2019/11/18 文章目录 前言 this引用成员变量 类属性的调用 this参数类型的构造器 总结 前言 最近,朋友丢出来了一段Java代码,这个程序的 ...

  5. 使用Golang利用ectd实现一个分布式锁

    http://blog.codeg.cn/post/blog/2016-02-24-distrubute-lock-over-etcd/ By zieckey · 2016年02月24日 · 1205 ...

  6. java语言特点 字符串不变_面试必问:Java中String类型为什么设计成不可变的?

    这几天在各大平台上都看到过这样一些帖子,全都是关于String类型对象不可变的问题,当然现在也是找工作的准备时期,因此花了一部分时间对其进行整理一下. 想要完全了解String,在这里我们需要解决以下 ...

  7. java中数组的返回值是什么类型_面试必问:Java中String类型为什么设计成不可变的?...

    这几天在各大平台上都看到过这样一些帖子,全都是关于String类型对象不可变的问题,当然现在也是找工作的准备时期,因此花了一部分时间对其进行整理一下. 想要完全了解String,在这里我们需要解决以下 ...

  8. 面试必考之Java基础数据类型

    你有思考过编程语言为什么要有「数据类型」吗? 面试官其实是想让你告诉他:小讯同学是谁?她出生在哪儿? 目录 1.困扰小讯同学的一串神秘数字 2.场景技术实现 2.1 最终效果图 2.2 奉上源码

  9. 字节面试:如何用Redis实现一个分布式锁?

    在开始提到Redis分布式锁之前,我想跟大家聊点Redis的基础知识. 说一下Redis的两个命令: SETNX key value setnx 是SET if Not eXists(如果不存在,则 ...

  10. 最强Java面试题全部合集,涵盖BAT大厂面试必考的9大技术!-强烈建议收藏

    过去2年我持续分享了BAT TMD为代表的大厂最新面试题目,特别是蚂蚁金服.天猫.淘宝.头条.拼多多等Java面试题目. 过去2年,我已经成功的帮助了部分同学进入了大厂. 2020开始,我依然会为大家 ...

最新文章

  1. QIIME 2用户文档. 3老司机上路指南(2018.11)
  2. linux-xargs
  3. Struts2.3+Spring4.0
  4. What Is Text Mining?
  5. 性能远超AtomicLong,LongAdder原理完全解读
  6. C++ Function语意学
  7. python的self
  8. RuoYi-Cloud 登陆 /code 获取验证码出错
  9. 28.Qt获取路径问题
  10. java 图片生成器_Java实现的图片生成器
  11. jboss linux环境部署,linux下部署jboss
  12. RoboWare的安装与使用
  13. 解决Ubuntu环境搜狗拼音候选区乱码问题
  14. 六脉连环大总结,教你干趴Docker
  15. Ubuntu18.04打开关闭DNS服务
  16. 记账想要简单又安全,使用电脑记账是最佳的选择
  17. 攻防世界xctfweb题leaking题解
  18. 如何利用eclipse的WTD自动部署一个webservice
  19. Chrome无头模式获取直播间弹幕
  20. 红旗 linux 8,中科红旗Asianux Server Linux 8有何突出之处,附新功能介绍

热门文章

  1. oms系统应用服务器,OMS、WMS、TMS、ERP之间的关系
  2. js网页进度条等待特效
  3. C:\Users\用户名\Documents不可用
  4. linux var mqm权限,MQ7.5通道权限问题
  5. 数据库服务器的安装与配置
  6. 只安装XCode’s Com­mand line tools不安装XCode
  7. Windows自定义域名跳转指定网址
  8. ES 矩阵查询(Adjacency matrix aggregation)
  9. C++之常指针和指向常量的指针
  10. 虎牙直播怎么换html5,虎牙直播助手怎么改名字 昵称更换方法