目录
Redis实现分布式锁
原理
Redis中有这么个东西(key:k1 value:v1),k1这个key就是判断的锁
当线程进来访问时,判断redis中是否有k1,如果没有,就设置k1然后访问资源,操作,此时另一个线程过来再次获取是否存在k1,当然此时k1肯定是存在的,所以该线程不能访问锁的资源,需要等待或者重新访问。
当访问的线程访问完之后要释放k1也就是删除k1。
测试代码
package com.shao.seckill;
import com.shao.seckill.utils.UUIDUtil;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.RedisScript;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@SpringBootTest
class SeckillApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedisScript<Boolean> script;
@Test
void contextLoads() {
ValueOperations valueOperations = redisTemplate.opsForValue();
//占位,如果key不存在才可以设置成功返回true
Boolean isLock = valueOperations.setIfAbsent("k1", "v1");
//如果占位成功,进行正常操作
if(isLock){
//操作
valueOperations.set("name","xxxx");
String name = (String) valueOperations.get("name");
System.out.println(name);
//操作完之后删除锁,让其它线程能够使用
redisTemplate.delete("k1");
}else{
System.out.println("有线程在使用,请稍后尝试");
}
}
}
测试结果
可以正常锁和解锁资源
问题
看似没有问题其实有很大隐患,如果一个线程抢占资源之后没有释放资源就抛出异常,此时就出现死锁现象,别的线程再也等不到资源。
优化一
解决办法
给锁的资源添加过期时间,例如该部分执行的时间是3秒,我们就设置k1的过期时间是5秒,此时我们人为的设置个异常,就算第一个线程进入后抛出异常过五秒之后也k1也会自动失效。
测试代码
package com.shao.seckill;
import com.shao.seckill.utils.UUIDUtil;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.RedisScript;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@SpringBootTest
class SeckillApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedisScript<Boolean> script;
@Test
void contextLoads02(){
ValueOperations valueOperations = redisTemplate.opsForValue();
//比上次多设置一个时间,到时见自动解锁,解决出现死锁的情况
//但是此种方法并不安全
// 例如A线程获取锁,由于某些原因在执行操作时耗费时间特别长
// 但是设置锁的key已经到达自动销毁时间,下一个线程B就能获取锁进入该操作
// 此时线程A在删除锁,其实删除的是线程B的锁,至此连续错误删除加锁导致系统出错
Boolean isLock = valueOperations.setIfAbsent("k1", "v1",5, TimeUnit.SECONDS);
if(isLock){
valueOperations.set("newname","newxxxx");
String name = (String) valueOperations.get("newname");
System.out.println(name);
//人为制造异常
int x =5/0;
redisTemplate.delete("k1");
}else{
System.out.println("有线程在使用,请稍后尝试");
}
}
}
测试结果
解决死锁问题
问题
还是不安全
例如A线程获取锁,由于某些原因在执行操作时耗费时间特别长
但是设置锁的key已经到达自动销毁时间,下一个线程B就能获取锁进入该操作
此时线程A再删除锁,其实删除的是线程B的锁,至此连续错误删除加锁导致系统出错
优化二
解决办法
我们可以为k1设置随机数,使每个线程的锁值都不相同,解锁时需验证k1的值。此时又出现新的问题,因为此时解锁就需要先验证k1的值再删除k1,无法保证原子性,这时我们就需要利用lua脚本来实现一次性执行整个验k1的值和删除k1的。
测试代码
准备lock.lua脚本在resources下和application.properties(或者application.yaml)同级
if redis.call("get",KEYS[1])==ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
编写对应的Redis配置
package com.shao.seckill.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
@Bean
public DefaultRedisScript defaultRedisScript(){
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
//lock.lua脚本位置和application.yaml同级目录
redisScript.setLocation(new ClassPathResource("lock.lua"));
redisScript.setResultType(Long.class);
return redisScript;
}
}
测试使用
package com.shao.seckill;
import com.shao.seckill.utils.UUIDUtil;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.RedisScript;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@SpringBootTest
class SeckillApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedisScript<Boolean> script;
@Test
void contextLoads03(){
ValueOperations valueOperations = redisTemplate.opsForValue();
//针对以上出现的问题,可以通过将锁的值编程随机数
//线程对于锁就需要验证锁,解锁这几个步骤
//这些步骤要满足原子性不然会出先错误
//LUA脚本让多个命令一次进行
// 1.在Redis服务器中写LUA脚本:服务器压力大,每次都运行都会携带该脚本
// 2.在java客户端中写LUA脚本:每次都需要发送给Redis服务器中,通信时间长
//编写RedisConfig设置要使用脚本lock.lup的位置
String value = UUID.randomUUID().toString();
Boolean isLock = valueOperations.setIfAbsent("k1", value,5, TimeUnit.SECONDS);
if(isLock){
valueOperations.set("newnewname","newxxxx");
String name = (String) valueOperations.get("newnewname");
System.out.println(name);
System.out.println(valueOperations.get("k1"));
//执行lua脚本 验证锁值 删除锁
Boolean result = (Boolean) redisTemplate.execute(script, Collections.singletonList("k1"), value);
System.out.println(result);
}else{
System.out.println("有线程在使用,请稍后尝试");
}
}
}
测试结果
解决以上问题,能进行解锁和加锁等操作