作者:奋怒的小超_656 | 来源:互联网 | 2023-10-13 09:59
之前的文章《分布式锁详解 - 分别利用Zookeeper和数据库实现分布式锁》,由于篇幅太长,又碰上加班时间不够充裕,所以没有把Redis
的实现也顺带进去,特此做一些利用Redis
实现分布式锁的分析。
PS:让我做选择的话,分布式锁的选择还是更愿意选择利用Zookeeper
去实现的。 原因的话可以参考下面这张图:
一、关于Redis分布式锁的基础知识 1、缓存有效期
: redis中的数据,不一定都是持久化的;为存储元素设置生存时间,当元素过期时,会被自动删除。(主要是为了防止死锁,一般生存时间设定为业务操作时间的两倍基本就可以了)
2、SETNX命令
: SETNX key value
,将key的值设为value,当且仅当key不存在。若给定的key已经存在,则 SETNX
不做任何动作。SETNX
是 【 SET if Not eXists 】的简写。(SETNX 命令就是用来保证设置失效时间和插入值是原子操作,如果不是原子执行就可能设置完key,在设置过期时间的时候系统挂机,就会导致死锁问题)
3、lua脚本
轻量小巧的脚本语言,用于支持redis操作序列的原子性。
PS:如果对Redis不是很熟悉的话,建议还是要去学习一下的,现在基本大部分的互联网公司都离不开Redis的掌握。此处顺便分享一个Redis的学习路线图给大家。
二、Redis实现分布式锁 还是和上一篇文章一样,Redis实现抽象锁AbstractLock
,然后拓展其抽象方法。 AbstractLock.java
import java. util. concurrent. locks. Lock; public abstract class AbstractLock implements Lock { public void getLock ( ) { if ( tryLock ( ) ) { System. out. println ( "获取Lock锁的资源 #####" ) ; } else { waitLock ( ) ; getLock ( ) ; } } public abstract boolean tryLock ( ) ; public abstract void waitLock ( ) ; public abstract void unLock ( ) ; }
1、项目引入Redis相关依赖信息:spring-data-redis
(Spring整合Redis一些信息)和jedis
(Redis客户端)。
2、Jedis连接池的配置: 3、实现:
class RedisLock extends AbstractLock { ThreadLocal< String> local &#61; new ThreadLocal < > ( ) ; public boolean tryLock ( ) { String uuid &#61; UUID. randomUUID. toString ( ) ; local. set ( uuid) ; Jedis jedis &#61; ( Jedis) factory. getConnection ( ) . getNativeConnection ( ) ; String ret &#61; jedis. set ( KEY, uuid, "NX" , "PX" , 1000 ) ; if ( "OK" . equals ( ret) ) { return true } return false ; } public void waitLock ( ) throw Exception{ Thread. sleep ( 3000 ) ; } public void unLock ( ) throw Exception{ String script &#61; FileUtils. readFileByLines ( "C://unlock.lua" ) ; Jedis jedis &#61; ( Jedis) factory. getConnection ( ) . getNativeConnection ( ) ; jedis. eval ( script, Array. asList ( KEY) , Arrays. asList ( local. get ( ) ) ) ; } }
其中上面的lua
脚本如下即可&#xff1a;
if redis.call("get", KEYS[1]) &#61;&#61; ARGV[1] thenreturn redis.call("del", KEYS[1]) elsereturn 0 end
Redisson分布式锁 上面实现的Redis分布式存在一个大问题&#xff1a;假设设置失效时间10秒&#xff0c;如果由于某些原因导致10秒还没执行完任务&#xff0c;这时候锁自动失效&#xff0c;导致其他线程也会拿到分布式锁。
面对这种情况&#xff0c;就需要通过一个守护线程&#xff0c;定时检测持有锁的线程是否还在正常做业务&#xff0c;如果是就给分布式锁的过期时间进行续命。实现起来相当复杂。这个时候&#xff0c;就可以借助Redisson
。
Redisson是一个在Redis的基础上实现的&#xff0c;基于Netty封装了一些利用了Redis特性的工具&#xff0c;让我们可以更高效、更简便的使用Redis。
业界大部分基于Redis的分布式锁都是由Redisson实现的&#xff0c;看看他的使用吧&#xff1a;
RLock lock &#61; redisson. getLock ( "lock-xxxx" ) ; try { 加锁方式一&#xff1a;最常见的使用方法&#xff0c;默认30 秒过期&#xff0c;watch dog ( 看门狗) 每过10 秒检测一次线程情况&#xff0c;进行续锁lock. lock ( ) ; 加锁方式二&#xff1a;支持过期解锁功能, 10 秒钟以后自动解锁, 无需调用&#96;unlock ( ) &#96;方法手动解锁lock. lock ( 10 , TimeUnit. SECONDS) ; 加锁方式三&#xff1a;尝试加锁&#xff0c;最多等待3 秒&#xff0c;上锁以后10 秒自动解锁。boolean res &#61; lock. tryLock ( 3 , 10 , TimeUnit. SECONDS) ; if ( res) { } finally { 解锁方式&#xff1a;lock. unlock ( ) ; }
是不是非常简单&#xff0c;直接通过引入Redisson&#xff0c;获取到Rlock&#xff0c;剩下的操作就跟我们Java单机锁的使用方式基本相差无几了。
Redisson的两个特性&#xff1a; 1、加锁机制
线程去获取锁&#xff0c;获取成功: 执行lua脚本&#xff0c;保存数据到redis数据库。 线程去获取锁&#xff0c;获取失败: 一直通过while循环尝试获取锁&#xff0c;获取成功后&#xff0c;执行lua脚本&#xff0c;保存数据到redis数据库。 2、watch dog自动延期机制 当我们使用lock.lock();
方法进行加锁时&#xff0c;默认锁的过期时间是30秒。Redisson会启动一个watch dog(看门狗)
后台线程&#xff0c;以锁超时时间 / 3 &#61; 30/3&#61;10s
作为周期&#xff08;每过10秒检测一次&#xff09;&#xff0c;检测持有锁的线程是否还在执行业务&#xff0c;还在执行&#xff0c;就进行锁的续期。也就是说&#xff0c;如果一个拿到锁的线程一直没有完成业务逻辑&#xff0c;那么看门狗会帮助线程不断的延长锁超时时间&#xff0c;锁不会因为超时而被释放。默认情况下&#xff0c;看门狗的续期时间是30s&#xff0c;也可以通过修改Config.lockWatchdogTimeout来另行指定。