网站栏目怎么做,erp沙盘模拟实训报告,网站检索功能怎么做,找工程项目在redis集群模式下#xff0c;我们会启动多个tomcat实例#xff0c;每个tomcat实例都有一个JVM#xff0c;且不共享。而synchronize锁的作用范围仅仅是当前JVM#xff0c;所以我们需要一个作用于集群下的锁#xff0c;也就是分布式锁。#xff08;就是不能用JVM自带的锁了…在redis集群模式下我们会启动多个tomcat实例每个tomcat实例都有一个JVM且不共享。而synchronize锁的作用范围仅仅是当前JVM所以我们需要一个作用于集群下的锁也就是分布式锁。就是不能用JVM自带的锁了需要一个第三方应用实现锁
在redis集群中我们是用setnx实现互斥锁来实现分布式锁。
setnx key value NX EX 时间 是往redis中创建一个key-value键值对如果redis中没有该key则创建成功返回true如果redis已经有了该key则创建失败返回null。NX是互斥EX是超时时间后跟具体时间单位sEX用于过期时间过了过期时间自动删除该key-value键值对。
我们用setnx命令实现分布式锁时key和value都是String类型key一般是特定前缀该锁的名字
value为线程id。
为什么value要存线程id呢
因为存在这样情况线程一获取锁去执行逻辑过程中遇到网络动荡线程一卡住了然后一段时间后线程一获取的锁的过期时间到了线程一的锁自动释放。然后线程二来获取锁线程二获取成功去执行逻辑而在这个过程中线程一的网络动荡恢复了线程一继续执行线程一先于线程二执行完线程一去释放锁但此时线程一创建的锁已经因超时而自动释放了所以此时线程一会去错误的释放线程二的锁所以我们要把线程id存入加以判断防止误删其他线程的id。
代码实现获取锁和释放锁
package com.hmdp.utils;import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;import java.util.Collections;
import java.util.concurrent.TimeUnit;public class SimpleRedisLock implements ILock {private String name;private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name name;this.stringRedisTemplate stringRedisTemplate;}private static final String KEY_PREFIX lock:;//randomUUID生成的数字会带一个横线toStringtrue方法就是去掉横线//因为不同JVM中可能存在线程号相同的情况所以需要用UUID来区分不同的JVMprivate static final String ID_PREFIX UUID.randomUUID().toString(true) -;Overridepublic boolean tryLock(long timeoutSec) {// 获取线程标示String threadId ID_PREFIX Thread.currentThread().getId();// 获取锁Boolean success stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX name, threadId, timeoutSec, TimeUnit.SECONDS);//不能直接返回success因为会有自动拆箱的风险如果success是null就会返回true返回错误数据return Boolean.TRUE.equals(success);}Overridepublic void unlock() {// 获取线程标示String threadId ID_PREFIX Thread.currentThread().getId();// 获取锁中的标示String id stringRedisTemplate.opsForValue().get(KEY_PREFIX name);// 判断标示是否一致if(threadId.equals(id)) {// 释放锁stringRedisTemplate.delete(KEY_PREFIX name);}}
}我们需要新建一个类实现ILock接口然后去重写其中的tryLock获取锁、unlock释放锁方法。
往SimpleRedisLock的构造方法传入name变量方便我们在实例化SimpleRedisLock类中设置该锁的名字。我们后面用setnx命令创建锁时的key值就是特定前缀KEY_PREFIX加上这个namevalue值是randomUUID生成的唯一一串数字加上该线程id因为不同JVM中可能存在线程号相同的情况所以需要用UUID来区分不同的JVM。 问题
当本线程1在进入这个if判断后释放锁之前突然阻塞比如full GC该JVM上全服务堵塞阻塞时间过长锁超时释放这时另一个jvm的线程2获取到锁然后线程1继续执行释放锁 这样就又会出现同一个用户会有俩个线程在同时运行,所以需要保证判断和释放锁这俩步为一个原子性同成功或同失败 这样很容易想到事务redis也有事务但redis的事务可以保证原子性但不能保证一致性而且redis的事务是最后事务中的步骤同时完成。并不是一步一步的执行所以只能用乐观锁但不建议用乐观锁推荐用lua脚本
一个lua脚本中可以编写多条redis命令确保多条命令的原子性。即实现判断和释放锁一起执行的原子性。
我们需要把这个脚本写在resources目录下
释放锁的lua脚本
local keyKEYS[1] -- 锁的key
local threadIdARGV[1] --线程唯一标识
-- 比较线程标示与锁中的标示是否一致
if(redis.call(get, key) threadId) then-- 释放锁 del keyreturn redis.call(del, key)
end
return 0;
if() then end 相当于Java命令中的 if( ) { } 。
KEYS[1]:为需要传入的key值当需要传入多个key值时声明KEYS[2]、KEYS[3]...等就行。
ARGV[1]:为需要传入的其他非key的变量声明方法与key一样。例如
local key KEYS[1]
local key2 KEYS[2]
local threadIdARGV[1]
local releaseTimeARGV[2] redis.call( , ....)是执行的redis命令命令中单引号 中写要执行的redis操作后面的参数为执行该redis命令所需的参数例如get命令需要知道key值。
然后改写我们的分布式锁
我们需要先加载我们的lua脚本
private static final DefaultRedisScriptLong UNLOCK_SCRIPT;static {UNLOCK_SCRIPT new DefaultRedisScript();UNLOCK_SCRIPT.setLocation(new ClassPathResource(unlock.lua)); //加载的脚本的位置如果脚本文件在resources目录下则只需写脚本名称即可。UNLOCK_SCRIPT.setResultType(Long.class); //返回值的类型}
Overridepublic void unlock() {// 调用lua脚本 ,原来的多行代码变成了现在的单行代码就保证了原子性stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX name), //生成单元素的集合即脚本中的需要的KETS[1]参数ID_PREFIX Thread.currentThread().getId()); //即脚本中需要的ARVG[1]参数} 完整代码
package com.hmdp.utils;import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;import java.util.Collections;
import java.util.concurrent.TimeUnit;public class SimpleRedisLock implements ILock {private String name;private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name name;this.stringRedisTemplate stringRedisTemplate;}private static final String KEY_PREFIX lock:;//randomUUID生成的数字会带一个横线toStringtrue方法就是去掉横线//因为不同JVM中可能存在线程号相同的情况所以需要用UUID来区分不同的JVMprivate static final String ID_PREFIX UUID.randomUUID().toString(true) -;private static final DefaultRedisScriptLong UNLOCK_SCRIPT;static {UNLOCK_SCRIPT new DefaultRedisScript();UNLOCK_SCRIPT.setLocation(new ClassPathResource(unlock.lua)); //加载的脚本的位置UNLOCK_SCRIPT.setResultType(Long.class); //返回值的类型}Overridepublic boolean tryLock(long timeoutSec) {// 获取线程标示String threadId ID_PREFIX Thread.currentThread().getId();// 获取锁Boolean success stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX name, threadId, timeoutSec, TimeUnit.SECONDS);//不能直接返回success因为会有自动拆箱的风险如果success是null就会返回true返回错误数据return Boolean.TRUE.equals(success);}Overridepublic void unlock() {// 调用lua脚本 ,原来的多行代码变成了现在的单行代码就保证了原子性stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX name), //生成单元素的集合即脚本中的需要的KETS[1]参数ID_PREFIX Thread.currentThread().getId()); //即脚本中需要的ARVG[1]参数}}