Redis(八)-- Redis分布式锁实现
一、使用分布式锁要满足的几个条件
- 系统是一个分布式系统(关键是分布式,单机的可以使用ReentrantLock或者synchronized代码块来实现)
- 共享资源(各个系统访问同一个资源,资源的载体可能是传统关系型数据库或者NoSQL)
- 同步访问(即有很多个进程同事访问同一个共享资源。没有同步访问,谁管你资源竞争不竞争)
二、应用的场景例子
管理后台的部署架构(多台tomcat服务器+redis【多台tomcat服务器访问一台redis】+mysql【多台tomcat服务器访问一台服务器上的mysql】)就满足使用分布式锁的条件。多台服务器要访问redis全局缓存的资源,如果不使用分布式锁就会出现问题。 看如下伪代码:
- long N=0L;
- //N从redis获取值
- if(N<5){
- N++;
- //N写回redis
- }
上面的代码主要实现的功能:
从redis获取值N,对数值N进行边界检查,自加1,然后N写回redis中。 这种应用场景很常见,像秒杀,全局递增ID、IP访问限制等。以IP访问限制来说,恶意攻击者可能发起无限次访问,并发量比较大,分布式环境下对N的边界检查就不可靠,因为从redis读的N可能已经是脏数据。传统的加锁的做法(如java的synchronized和Lock)也没用,因为这是分布式环境,这个同步问题的救火队员也束手无策。在这危急存亡之秋,分布式锁终于有用武之地了。
分布式锁可以基于很多种方式实现,比如zookeeper、redis...。不管哪种方式,他的基本原理是不变的:用一个状态值表示锁,对锁的占用和释放通过状态值来标识。
三、使用redis的setNX命令实现分布式锁
1、实现的原理
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。redis的SETNX命令可以方便的实现分布式锁。
2、基本命令解析
1)setNX(SET if Not eXists)
语法:
- SETNX key value
将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写
返回值:
例子:
- redis> EXISTS job # job 不存在
- (integer) 0
- redis> SETNX job "programmer" # job 设置成功
- (integer) 1
- redis> SETNX job "code-farmer" # 尝试覆盖 job ,失败
- (integer) 0
- redis> GET job # 没有被覆盖
- "programmer"
所以我们使用执行下面的命令
- SETNX lock.foo <current Unix time + lock timeout + 1>
如返回1,则该客户端获得锁,把lock.foo的键值设置为时间值表示该键已被锁定,该客户端最后可以通过DEL lock.foo来释放该锁。
如返回0,表明该锁已被其他客户端取得,这时我们可以先返回或进行重试等对方完成或等待锁超时。
2)getSET
语法:
- GETSET key value
将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
当 key 存在但不是字符串类型时,返回一个错误。
返回值:
返回给定 key 的旧值。
当 key 没有旧值时,也即是, key 不存在时,返回 nil 。
3)get
语法:
- GET key
返回值:
当 key 不存在时,返回 nil ,否则,返回 key 的值。
如果 key 不是字符串类型,那么返回一个错误
四、解决死锁
上面的锁定逻辑有一个问题:如果一个持有锁的客户端失败或崩溃了不能释放锁,该怎么解决?
- 我们可以通过锁的键对应的时间戳来判断这种情况是否发生了,如果当前的时间已经大于lock.foo的值,说明该锁已失效,可以被重新使用。
发生这种情况时,可不能简单的通过DEL来删除锁,然后再SETNX一次(讲道理,删除锁的操作应该是锁拥有这执行的,这里只需要等它超时即可),当多个客户端检测到锁超时后都会尝试去释放它,这里就可能出现一个竞态条件,让我们模拟一下这个场景:
- C0操作超时了,但它还持有着锁,C1和C2读取lock.foo检查时间戳,先后发现超时了。
- C1 发送DEL lock.foo
- C1 发送SETNX lock.foo 并且成功了。
- C2 发送DEL lock.foo
- C2 发送SETNX lock.foo 并且成功了。
- 这样一来,C1,C2都拿到了锁!问题大了!
幸好这种问题是可以避免的,让我们来看看C3这个客户端是怎样做的:
- C3发送SETNX lock.foo 想要获得锁,由于C0还持有锁,所以Redis返回给C3一个0
- C3发送GET lock.foo 以检查锁是否超时了,如果没超时,则等待或重试。
- 反之,如果已超时,C3通过下面的操作来尝试获得锁:
- GETSET lock.foo <current Unix time + lock timeout + 1>
- 通过GETSET,C3拿到的时间戳如果仍然是超时的,那就说明,C3如愿以偿拿到锁了。
- 如果在C3之前,有个叫C4的客户端比C3快一步执行了上面的操作,那么C3拿到的时间戳是个未超时的值,这时,C3没有如期获得锁,需要再次等待或重试。留意一下,尽管C3没拿到锁,但它改写了C4设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。
注意:为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。
五、代码实现
expireTimeMsg 锁过期时间,防止线程在入锁之后 无限等待
waitTimeMsg 锁等待时间(或者 叫 尝试获得锁的时间),防止线程饥饿
1.Jedis工具类
- package com.xbq.redis;
- import redis.clients.jedis.Jedis;
- import redis.clients.jedis.JedisPool;
- import redis.clients.jedis.JedisPoolConfig;
- /**
- * Jedis工具类
- * @author xbq
- * @created:2017-4-19
- */
- public class JedisUtil {
- private JedisPool pool;
- private static String URL = "192.168.242.130";
- private static int PORT = 6379;
- private static String PASSWORD = "xbq123";
- // ThreadLocal,给每个线程 都弄一份 自己的资源
- private final static ThreadLocal<JedisPool> threadPool = new ThreadLocal<JedisPool>();
- private final static ThreadLocal<Jedis> threadJedis = new ThreadLocal<Jedis>();
- private final static int MAX_TOTAL = 100; // 最大分配实例
- private final static int MAX_IDLE = 50; // 最大空闲数
- private final static int MAX_WAIT_MILLIS = -1; // 最大等待数
- /**
- * 获取 jedis池
- * @return
- */
- public JedisPool getPool(){
- JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
- // 控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取,如果赋值为-1,则表示不限制;
- // 如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)
- jedisPoolConfig.setMaxTotal(MAX_TOTAL);
- // 控制一个pool最多有多少个状态为idle(空闲的)的jedis实例
- jedisPoolConfig.setMaxIdle(MAX_IDLE);
- // 表示当borrow(引入)一个jedis实例时,最大的等待时间,如果超过等待时间,则直接抛出JedisConnectionException
- jedisPoolConfig.setMaxWaitMillis(MAX_WAIT_MILLIS);
- final int timeout = 60 * 1000;
- pool = new JedisPool(jedisPoolConfig, URL, PORT, timeout);
- return pool;
- }
- /**
- * 在jedis池中 获取 jedis
- * @return
- */
- public Jedis common(){
- // 从 threadPool中取出 jedis连接池
- pool = threadPool.get();
- // 为空,则重新产生 jedis连接池
- if(pool == null){
- pool = this.getPool();
- // 将jedis连接池维护到threadPool中
- threadPool.set(pool);
- }
- // 在threadJedis中获取jedis实例
- Jedis jedis = threadJedis.get();
- // 为空,则在jedis连接池中取出一个
- if(jedis == null){
- jedis = pool.getResource();
- // 验证密码
- jedis.auth(PASSWORD);
- // 将jedis实例维护到threadJedis中
- threadJedis.set(jedis);
- }
- return jedis;
- }
- /**
- * 释放资源
- */
- public void closeAll(){
- Jedis jedis = threadJedis.get();
- if(jedis != null){
- threadJedis.set(null);
- JedisPool pool = threadPool.get();
- if(pool != null){
- // 释放连接,归还给连接池
- pool.returnResource(jedis);
- }
- }
- }
- }
2.分布式锁实现
- package com.xbq.redis;
- import org.apache.log4j.Logger;
- import redis.clients.jedis.Jedis;
- /**
- * Redis实现分布式锁
- * @author xbq
- */
- public class RedisLock {
- private static final Logger logger = Logger.getLogger(RedisLock.class);
- // 获取jedis实例
- private Jedis jedis;
- // 锁 的key
- private String lockKey;
- // 锁过期时间,防止线程在入锁之后 无限等待
- private int expireTimeMsg = 60 * 1000;
- // 锁等待时间(或者 叫 尝试获得锁的时间),防止线程饥饿
- private int waitTimeMsg = 10 * 1000;
- // 系统时间偏移量5秒,服务器间的系统时间差不可以超过5秒,避免由于时间差造成错误的解锁
- private final static int offsetTime = 5 * 1000; // 用毫秒表示
- // 默认减去的时间
- private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;
- // 锁状态
- private volatile boolean lock = false;
- public RedisLock(Jedis jedis, String lockKey){
- this.jedis = jedis;
- this.lockKey = lockKey + "_lock";
- }
- public RedisLock(Jedis jedis, String lockKey, int waitTimeMsg){
- this(jedis, lockKey);
- this.waitTimeMsg = waitTimeMsg;
- }
- public RedisLock(Jedis jedis, String lockKey, int waitTimeMsg, int expireTimeMsg){
- this(jedis, lockKey, waitTimeMsg);
- this.expireTimeMsg = expireTimeMsg;
- }
- /**
- * 获取 锁 的key
- * @return
- */
- public String getLockKey(){
- return lockKey;
- }
- /**
- * 获取 key 对应的value
- * @param key
- * @return
- */
- private String get(String key){
- return jedis.get(key);
- }
- /**
- * 设置 key value,不存在 key,设置值 成功,返回1;存在key,设置值 失败,返回0
- * @param key
- * @param value
- * @return
- */
- private long setNx(String key, String value){
- return jedis.setnx(key, value);
- }
- /**
- * 获取旧值,设置 新的 值
- * @param key
- * @param value
- * @return
- */
- private String getSet(String key, String value){
- return jedis.getSet(key, value);
- }
- /**
- * 获取锁
- * 实现思路: 主要是使用了redis 的setnx命令,缓存了锁
- * reids缓存的key是锁的key,所有的共享, value是锁的到期时间(注意:这里把过期时间放在value了,没有时间上设置其超时时间)
- * 执行过程:
- * 1.通过setnx尝试设置某个key的值,成功(当前没有这个锁)则返回,成功获得锁
- * 2.锁已经存在则获取锁的到期时间,和当前时间比较,超时的话,则设置新的值
- * @return
- * @throws InterruptedException
- */
- public boolean lock() throws InterruptedException{
- int waitTime = waitTimeMsg;
- // 循环为了多次争夺锁
- while (waitTime >= 0) {
- // 过期时间
- long exquires = System.currentTimeMillis() + expireTimeMsg + 1;
- logger.info(Thread.currentThread().getName() + "尝试获取锁!");
- // 得到了 锁
- if(this.setNx(lockKey, String.valueOf(exquires)) == 1){
- logger.info(Thread.currentThread().getName() + "获得了锁,锁 过期时间为:" + exquires);
- lock = true;
- return true;
- }
- // 存在原来的锁,就获取原来锁的过期时间
- String lastLockTime = this.get(lockKey);
- // 判断redis中的时间是否为空,获取出的 时间 过期了,则进行下面操作
- if(lastLockTime != null
- && System.currentTimeMillis() - Long.valueOf(lastLockTime) > (expireTimeMsg + offsetTime)){
- // 获取上一个锁的过期时间,并设置现在的锁的过期时间(只有一个线程才能获取上一个线程的设置时间,因为jedis.getSet是同步的)
- String oldValue = this.getSet(lockKey, String.valueOf(exquires));
- // 防止误删(覆盖,因为key是相同的)了他人的锁——这里达不到效果,这里值会被覆盖,但是因为相差了很少的时间,所以可以接受
- if(oldValue != null && oldValue.equals(lastLockTime)){
- // [分布式的情况下]:如果这个时候,多个线程恰好都到了这里,但是只有一个线程的设置值和当前值相同,他才有权利获取锁
- logger.info("------" + Thread.currentThread().getName() + "获得了锁!------");
- lock = true;
- return true;
- }
- }
- // 循环一次减去一次
- waitTime = waitTime - DEFAULT_ACQUIRY_RESOLUTION_MILLIS;
- // 使用随机的等待时间可以一定程度上保证公平性
- Thread.sleep((long)(Math.random() * 100));
- }
- logger.error("--------" + Thread.currentThread().getName() + "获取锁失败!!");
- return false;
- }
- /**
- * 释放锁
- */
- public void unLock(){
- // 判断加锁了,才进行删除操作
- if(lock){
- jedis.del(lockKey);
- System.out.println(Thread.currentThread().getName() + "解锁成功!--------------");
- // 恢复默认值
- lock = false;
- }
- }
- }
3.模拟并发测试
- package com.xbq.redis;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.Semaphore;
- import redis.clients.jedis.Jedis;
- /**
- * 模拟并发环境下 获取锁
- * @author xbq
- */
- public class Main {
- public static void main(String[] args) {
- // 定义线程池
- ExecutorService service = Executors.newCachedThreadPool();
- // 只能有10个线程同时访问,用来模拟并发
- final Semaphore semaphore = new Semaphore(10);
- // 模拟20个客户端访问
- for (int i = 0; i < 20; i++) {
- Runnable runnable = new Runnable() {
- String lockKey = "TestLock33";
- @Override
- public void run() {
- try {
- // 获取许可
- semaphore.acquire();
- // 获取jedis实例
- Jedis jedis = new JedisUtil().common();
- RedisLock redisLock = new RedisLock(jedis, lockKey, 10000);
- if(redisLock.lock()){ // 获取到了锁,然后进行 业务处理
- // 业务代码
- Thread.sleep(3000);
- }
- // 释放锁
- redisLock.unLock();
- // 访问完后,释放 ,如果屏蔽下面的语句,则在控制台只能打印5条记录,之后线程一直阻塞
- semaphore.release();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- };
- // 执行线程
- service.execute(runnable);
- }
- // 退出线程池
- service.shutdown();
- }
- }
六、一些问题
1、为什么不直接使用expire设置超时时间,而将时间的毫秒数其作为value放在redis中?
如下面的方式,把超时的交给redis处理:
- lock(key, expireSec){
- isSuccess = setnx key
- if (isSuccess)
- expire key expireSec
- }
这种方式貌似没什么问题,但是假如在setnx后,redis崩溃了,expire就没有执行,结果就是死锁了。锁永远不会超时。
2、为什么前面的锁已经超时了,还要用getSet去设置新的时间戳的时间获取旧的值,然后和外面的判断超时时间的时间戳比较呢?
因为是分布式的环境下,可以在前一个锁失效的时候,有两个进程进入到锁超时的判断。如:
C0超时了,还持有锁,C1/C2同时请求进入了方法里面
C1/C2获取到了C0的超时时间
C1使用getSet方法
C2也执行了getSet方法
假如我们不加 oldValueStr.equals(currentValueStr) 的判断,将会C1/C2都将获得锁,加了之后,能保证C1和C2只能一个能获得锁,一个只能继续等待。
注意:这里可能导致超时时间不是其原本的超时时间,C1的超时时间可能被C2覆盖了,但是他们相差的毫秒及其小,这里忽略了。
七、源码下载
https://gitee.com/xbq168/DistributedLockByRedis
致谢:感谢您的阅读!转载请加原文链接,谢谢。转载请加上原文链接,谢谢!http://www.cnblogs.com/0201zcr/p/5942748.html
Redis(八)-- Redis分布式锁实现的更多相关文章
- 基于redis实现的分布式锁
基于redis实现的分布式锁 我们知道,在多线程环境中,锁是实现共享资源互斥访问的重要机制,以保证任何时刻只有一个线程在访问共享资源.锁的基本原理是:用一个状态值表示锁,对锁的占用和释放通过状态值来标 ...
- 一个Redis实现的分布式锁
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.redis.conne ...
- 基于Redis的简单分布式锁的原理
参考资料:https://redis.io/commands/setnx 加锁是为了解决多线程的资源共享问题.Java中,单机环境的锁可以用synchronized和Lock,其他语言也都应该有自己的 ...
- redis客户端、分布式锁及数据一致性
Redis Java客户端有很多的开源产品比如Redission.Jedis.lettuce等. Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持:Redis ...
- Redis系列(二)--分布式锁、分布式ID简单实现及思路
分布式锁: Redis可以实现分布式锁,只是讨论Redis的实现思路,而真的实现分布式锁,Zookeeper更加可靠 为什么使用分布式锁: 单机环境下只存在多线程,通过同步操作就可以实现对并发环境的安 ...
- 在redis上实现分布式锁
/** *在redis上实现分布式锁 */ class RedisLock { private $redisString; private $lockedNames = []; public func ...
- 如何用redis正确实现分布式锁?
先把结论抛出来:redis无法正确实现分布式锁!即使是redis单节点也不行!redis的所谓分布式锁无法用在对锁要求严格的场景下,比如:同一个时间点只能有一个客户端获取锁. 首先来看下单节点下一般r ...
- redis系列:分布式锁
redis系列:分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分 ...
- 一般实现分布式锁都有哪些方式?使用redis如何设计分布式锁?使用zk来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高?
#(1)redis分布式锁 官方叫做RedLock算法,是redis官方支持的分布式锁算法. 这个分布式锁有3个重要的考量点,互斥(只能有一个客户端获取锁),不能死锁,容错(大部分redis节点创建了 ...
- Redis如何实现分布式锁
今天我们来聊一聊分布式锁的那些事. 相信大家对锁已经不陌生了,我们在多线程环境中,如果需要对同一个资源进行操作,为了避免数据不一致,我们需要在操作共享资源之前进行加锁操作.在计算机科学中,锁(lock ...
随机推荐
- iOS播放系统声音和震动
在需要声音的类的.h文件中添加 #import <AudioToolbox/AudioToolbox.h>static SystemSoundID shake_sound_male ...
- 【WPF】设置ListBox容器Item的流式布局
需求:像下图那样显示把一组内容装入ListBox中显示.要求用WrapPanel横向布局,顺序如图中的数字. 问题:ListBox默认的布局是从上往下单列的,所以需要设置布局. <ListBox ...
- drupal 使用步骤
一.安装 二.汉化 ①.下载语言包文件:http://localize.drupal.org/translate/languages/zh-hans ②.将 .po 文件放置到 drupal7/pro ...
- mysql修改密码与password字段不存在mysqladmin connect to server at localhost failed
mysqladmin: connect to server at 'localhost' failed 停止mysql服务 systemctl stop mysql 安全模式启动 chown -R m ...
- jQuery 工具大搜集
jQuery 是一个非常棒的类库,但是为了保证代码的干净以及代码的精简,它只提供最核心的功能.所以就有了很多其他的工具来丰富jQuery的功能.我在使用这些工具的时候发现我常常重复的编写一些代码,所以 ...
- 移动H5功能设计反思 测试用例总结
一.线上页面滑动流畅性测试 1.减少长动画效果(影响流畅) 2.是否自动跳转或者还是让用户自己操作跳转需要推敲 二.buttom和页面滑动的选择(优劣) 部分手机本身就会滑动不灵敏,大部分时候其实用b ...
- (一) Qt Model/View 的简单说明
(一) Qt Model/View 的简单说明 .预定义模型 (二)使用预定义模型 QstringListModel例子 (三)使用预定义模型QDirModel的例子 (四)Qt实现自定义模型基于QA ...
- 《FPGA全程进阶---实战演练》第三章之PCB叠层
1.双面板 在双层板设计layout时,最好不要不成梳状结构,因为这样构成的电路,回路面积较大,但是只要对较重要的信号加以地保护,布线完成之后将空的地方敷上地铜皮,并在多个过孔将两个地连接起来,可以弥 ...
- Deep Reinforcement Learning from Self-Play in Imperfect-Information Games
Heinrich, Johannes, and David Silver. "Deep reinforcement learning from self-play in imperfect- ...
- python with妙用
class aa(): def bb(self): print("hhhh") return "hello world" def __enter__(self) ...