作者:杨高超

juejin.im/post/5a4984af6fb9a0450b66bc57

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

在分布式环境下实现一个分布式锁服务并不太容易,需要考虑很多在单进程下的锁服务不需要考虑的问题。分布式锁锁的实现也有很多。这里我们讨论在 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 的接口。这个主要是客户端无法获取到锁的时候会在一小段时间内反复尝试是否能够获取到锁。

Redis其他文章扩展:

从入门到入土,Redis简明教程

来讨论一下这些常见的 Redis 面试题

Springboot + redis + 注解 + 拦截器来实现接口幂等性校验

文中代码

https://github.com/gaochao2000/redis_util

END

Java面试题专栏

【61期】MySQL行锁和表锁的含义及区别(MySQL面试第四弹)

【62期】解释一下MySQL中内连接,外连接等的区别(MySQL面试第五弹)

【63期】谈谈MySQL 索引,B+树原理,以及建索引的几大原则(MySQL面试第六弹)

【64期】MySQL 服务占用cpu 100%,如何排查问题? (MySQL面试第七弹)

【65期】Spring的IOC是啥?有什么好处?

【66期】Java容器面试题:谈谈你对 HashMap 的理解

【67期】谈谈ConcurrentHashMap是如何保证线程安全的?

【68期】面试官:对并发熟悉吗?说说Synchronized及实现原理

【69期】面试官:对并发熟悉吗?谈谈线程间的协作(wait/notify/sleep/yield/join)

【70期】面试官:对并发熟悉吗?谈谈对volatile的使用及其原理

我知道你 “在看”

小王,在 Java 中如何利用 redis 实现一个分布式锁服务呢???相关推荐

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

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

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

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

  3. 基于Redis实现一个分布式锁

    与分布式锁相对应的是「单机锁」,我们在写多线程程序时,避免同时操作一个共享变量产生数据问题,通常会使用一把锁来「互斥」,以保证共享变量的正确性,其使用范围是在「同一个进程」中. 一.为什么需要分布式锁 ...

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

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

  5. java定时任务中使用多线程_java项目中如何利用多线程实现一个定时器任务

    java项目中如何利用多线程实现一个定时器任务 发布时间:2020-11-10 16:04:03 来源:亿速云 阅读:86 作者:Leah 今天就跟大家聊聊有关java项目中如何利用多线程实现一个定时 ...

  6. java正则表达式匹配数字范围_在java中怎么利用正则表达式匹配数字

    在java中怎么利用正则表达式匹配数字 发布时间:2020-12-03 17:47:12 来源:亿速云 阅读:58 作者:Leah 在java中怎么利用正则表达式匹配数字?针对这个问题,这篇文章详细介 ...

  7. 浅谈JAVA中如何利用socket进行网络编程(二)

    转自:http://developer.51cto.com/art/201106/268386.htm Socket是网络上运行的两个程序间双向通讯的一端,它既可以接受请求,也可以发送请求,利用它可以 ...

  8. 阿里JAVA面试题剖析:一般实现分布式锁都有哪些方式?使用 Redis 如何设计分布式锁?...

    面试原题 一般实现分布式锁都有哪些方式?使用 redis 如何设计分布式锁?使用 zk 来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高? 面试官心理分析 其实一般问问题,都是这么问的,先 ...

  9. [转载] java中对象作为参数传递给一个方法,到底是值传递,还是引用传递

    参考链接: 用Java传递和返回对象 看完绝对清晰~ java中对象作为参数传递给一个方法,到底是值传递,还是引用传递? pdd:所谓java只有按值传递:基本类型  值传递:引用类型,地址值传递,所 ...

最新文章

  1. DOS批处理高级教程:第三章 FOR命令中的变量(转)
  2. piwik mysql_piwik流量统计系统搭建(apache2.4+piwik+mysql5.6+php5.6.14)
  3. hdu 6106 Classes
  4. 【ArcGIS风暴】ArcGIS快捷键大全
  5. 「野性消费」也不怕!打造供应链数据平台,业务逻辑模板都在这了
  6. 微服务技术栈:常见注册中心组件,对比分析
  7. iframe 与frameset
  8. 数据科学和人工智能技术笔记 五、文本预处理
  9. 我的世界服务器不显示浮空字,我的世界服务器浮空字怎么做 | 手游网游页游攻略大全...
  10. CY7C68013 USB接口相机开发记录 - 第一天:资料下载
  11. 【Shiro第一篇】 Shiro权限框架简介
  12. Python条件分支语法
  13. Android插件GsonFormat
  14. wps桌面图标不显示问题
  15. 今日头条信息流 - 橙子建站
  16. Java Reference Objects or How I Learned to Stop Worrying and Love OutOfMemoryError
  17. Pubwin数据备份专家官方版
  18. 微信小程序:修复图片音频全新升级带特效喝酒神器源码
  19. 【送豪礼】死了都要爱!不告白不痛快!
  20. 计算机快捷方式在哪儿,Windows电脑计算器快捷键在哪里打开及敬业签云便签在线计算器怎么使用...

热门文章

  1. 如何看待快手领投知乎4.34亿美元融资?创始人周源亲自下场回答
  2. 继安卓市场下架后 探探App也在苹果商店下架
  3. 自考计算机系统结构知识点,2019自考计算机系统结构复习精讲资料一
  4. mvc 项目 webconfig 打开错误_Spring体系常用项目一览
  5. c 语言 字符 查找,C 语言实例 - 查找字符在字符串中出现的次数
  6. 双向链表list.h升序排序
  7. ajxs跨域 php_PHP Ajax 跨域问题最佳解决方案
  8. html背景图适应div_CSS实现背景图片屏幕自适应
  9. 【Flink】Flink Checkpoint 问题排查实用指南
  10. 【Java】Java 反射机制浅析