Redis分布式锁(二)
1、初始化版本简单添加
2、nginx分布式微服务架构
3、redis分布式锁 v3.0
4、宕机与过期+防止死锁 v4.0
5、防止误删除key的问题 v5.0
6、Lua保证原子性 v6.0
InventoryController
package com.zzrg.redislock.controller;
import com.zzrg.redislock.service.InventoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author ZzRG
* @Date 2023/6/14 1:14
* @Version 1.0
*/
@RestController
@Api(tags = "redis分布式锁测试")
public class InventoryController {
@Autowired
private InventoryService inventoryService;
@ApiOperation("扣减库存sale,一次卖一个")
@GetMapping(value = "/inventory/sale")
public String sale()
{
return inventoryService.sale();
}
}
InventoryService
package com.zzrg.redislock.service;
/**
* @Author ZzRG
* @Date 2023/6/14 1:09
* @Version 1.0
*/
public interface InventoryService {
String sale();
}
InventoryServiceImpl:
package com.zzrg.redislock.service.Impl;
import cn.hutool.core.util.IdUtil;
import com.zzrg.redislock.service.InventoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.sql.Time;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author ZzRG
* @Date 2023/6/14 1:09
* @Version 1.0
*/
@Service
@Slf4j
public class InventoryServiceImpl implements InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${service.port}")
private String port;
/**
* v6.0 Lua脚本的redis分布式锁调用,必须保证原子性
* 但是基本 v6.0 版本已经够用
* @return
*/
@Override
public String sale() {
String retMessage = "";
String key = "zzrgRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
//不用递归了,高并发下容易出错,我们用自旋替代递归方法重试调用;也不用if 用while来代替
//添加Key同时添加过期时间 保证原子性
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS)){
//等待20毫秒
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果这样设置过期时间 就会无法保证原子性 还会出现死锁,添加了key 没走到这一步 就不会有过期时间
//因此需要 与上面的添加Key同时添加过期时间 保证原子性
// stringRedisTemplate.expire(key,30L,TimeUnit.SECONDS);
}
//枪锁成功的请求线程。进行正常业务逻辑操作,扣减库存
try {
// 1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
// 2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
// 3 扣减库存 每次减少一个
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
System.out.println(retMessage);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
//改进点,修改为Lua脚本的redis分布式锁调用,必须保证原子性,参考官网脚本案例
String luaScript =
"if redis.call('get',KEYS[1]) == ARGV[1] then" +
"return redis.call('del',KEYS[1])" +
"else " +
"return 0 " +
"end";
stringRedisTemplate.execute(new DefaultRedisScript(luaScript,Boolean.class), Arrays.asList(key),uuidValue);
}
return retMessage + "\t" + "服务端口号:" + port;
}
/**
* v4.0 加了过期时间 stringRedisTemplate.opsForValue().setIfAbsent(key, value, 30L, TimeUnit.SECONDS)
* 问题:误删锁,如果线程A运行时间超出了过期时间
* 在线程A运行时,xfcyRedisLock这个key过期,另一个线程B进来加了key
* 线程A结束后,把线程B的锁删了
* stringRedisTemplate.delete(key); 只能自己删除自己的,需要添加判断是否是自己的锁
* 设置的key + 过期时间不能分开 必须合并在一行具备原子性!
* v5.0 防止误删Key 但是还存在问题是最后的 finally 中的判断+del key 不是一行原子性操作,需要用Lua脚本修改。
* @return
@Override
public String sale() {
String retMessage = "";
String key = "zzrgRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
//不用递归了,高并发下容易出错,我们用自旋替代递归方法重试调用;也不用if 用while来代替
//添加Key同时添加过期时间 保证原子性
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS)){
//等待20毫秒
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果这样设置过期时间 就会无法保证原子性 还会出现死锁,添加了key 没走到这一步 就不会有过期时间
//因此需要 与上面的添加Key同时添加过期时间 保证原子性
// stringRedisTemplate.expire(key,30L,TimeUnit.SECONDS);
}
//枪锁成功的请求线程。进行正常业务逻辑操作,扣减库存
try {
// 1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
// 2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
// 3 扣减库存 每次减少一个
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
System.out.println(retMessage);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
//改进点,只能删除数据自己的key,不能删除被人的
if (stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)){
stringRedisTemplate.delete(key);
}
}
return retMessage + "\t" + "服务端口号:" + port;
}
*/
/**
* v3.2 出现的问题死锁:分布式微服务中突然一个挂了,代码层面跟们没有走到finally,这个服务就一直拿着这个锁,没办法保证解锁(redis 中的key一直存在)
* 这个key没有被删除,需要加入一个过期时间的key。
* ---------------------------------------------
* setnx 用while判断
* 问题: setnx过后,正在进行业务逻辑操作时,没有走到finally之前,整个微服务down机了,导致锁一直存在
* 不是程序出了问题,如果程序问题,最后还是会执行finally
* 没办法保证解锁(没有过期时间,该key一直存在),需要加入过期时间限定key
* @return
@Override
public String sale() {
String retMessage = "";
String key = "zzrgRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
//不用递归了,高并发下容易出错,我们用自旋替代递归方法重试调用;也不用if 用while来代替
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue)){
//等待20毫秒
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//枪锁成功的请求线程。进行正常业务逻辑操作,扣减库存
try {
// 1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
// 2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
// 3 扣减库存 每次减少一个
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
System.out.println(retMessage);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
stringRedisTemplate.delete(key);
}
return retMessage + "\t" + "服务端口号:" + port;
}
*/
/**
* v3.1 版本的出现了递归重试,容易导致stackoverflowerror,所以不太推荐; 另外,高并发缓刑后推荐while判断而不是if
@Override
public String sale() {
String retMessage = "";
String key = "zzrgRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue);
//flag == false 抢不到的线程要继续重试。。。。
if (!flag){
//等待20毫秒,进行递归重试。。。
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
sale();
}else {
//枪锁成功的请求线程。进行正常业务逻辑操作,扣减库存
try {
// 1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
// 2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
// 3 扣减库存 每次减少一个
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
System.out.println(retMessage);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
stringRedisTemplate.delete(key);
}
}
return retMessage + "\t" + "服务端口号:" + port;
}
*/
/**
* v2.0, 单机版枷锁配合nigx和jmeter压测后,不满足高并发分布式锁的性能要求,出现超卖
private Lock lock = new ReentrantLock();
@Override
public String sale() {
String retMessage = "";
lock.lock();
try {
// 1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
// 2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
// 3 扣减库存 每次减少一个
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
System.out.println(retMessage);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
lock.unlock();
}
return retMessage + "\t" + "服务端口号:" + port;
}
*/
@Override
public String saleByRedisson() {
}
}
//改进点,修改为Lua脚本的redis分布式锁调用,必须保证原子性,参考官网脚本案例
String luaScript =
"if redis.call('get',KEYS[1]) == ARGV[1] then" +
"return redis.call('del',KEYS[1])" +
"else " +
"return 0 " +
"end";
stringRedisTemplate.execute(new DefaultRedisScript(luaScript,Boolean.class), Arrays.asList(key),uuidValue);
7、可重入锁+设计模式
可重入锁又名递归锁
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有synchronized修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
隐式锁(即synchronized关键字使用的锁)默认是可重入锁
指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么]ava虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
//加锁的Lua脚本,对标我们的lock方法 v1.0
if redis.call('exists','key') ==0 then
redis.call('hest','key','uuid:threadid',1)
redis.call('expire','key',50)
return 1
elseif redis.call('hexists','key','uuid:threadid') ==1 then
redis.call('hincrby','key','uuid:threadid',1)
redis.call('expire','key',50)
return 1
else
return 0
end
//v2.0 合并相同的代码,用hincrby替代hset,精简代码
if redis.call('exists','key') ==0 or redis.call('hexists','key','uuid:threadid') == 1 then
redis.call('hincrby','key','uuid:threadid',1)
redis.call('expire','key',50)
return 1
else
return 0
end
//v3.0 脚本换成动态参数来代替
if redis.call('exists',KEYS[1]) ==0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then
redis.call('hincrby',KEYS[1],ARGV[1],1)
redis.call('expire',KEYS[1],ARGV[2])
return 1
else
return 0
end
//解锁的Lua脚本,对标我们的unlock方法//V1
if redis.call( 'hexists ',key , uuid:threadid) == 0 then
return nil
elseif redis.call( 'hincrby' , key, uuid:threadid,-1)== 0 then
return redis.call( 'del ' ,key)
else
return 0
end
//V2
if redis.call( ' HEXISTS',KEYS[1],ARGV[1])== 0 then
return nil
elseif redis.cal1( 'HINCRBY' ,KEYS[1],ARGV[1],-1) == 0 then
return redis.call( 'del',KEYS[1])
else
return 0
end
程序编写
创建RedisDistributedLock
package com.zzrg.redislock.myLock;
import cn.hutool.core.util.IdUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @Author ZzRG
* @Date 2023/6/17 16:42
* @Version 1.0
* 我们自研的redis分布式锁,实现了Lock接口
*/
public class RedisDistributedLock implements Lock {
private StringRedisTemplate stringRedisTemplate;
private String lockName; //KEYS[1]
private String uuidValue; //ARGV[1]
private long expireTime; //ARGV[2]
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName) {
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = IdUtil.simpleUUID()+":" + Thread.currentThread().getId();
this.expireTime = 50L;
}
@Override
public void lock() {
tryLock();
}
@Override
public boolean tryLock() {
try {
tryLock(-1L,TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time == -1L){
String script = "if redis.call('exists',KEYS[1]) ==0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then" +
"redis.call('hincrby',KEYS[1],ARGV[1],1)" +
"redis.call('expire',KEYS[1],ARGV[2])" +
"return 1" +
"else" +
"return 0" +
"end ";
while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))){
//等待20毫秒
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return true;
}
return false;
}
@Override
public void unlock() {
String script = "if redis.call( ' HEXISTS',KEYS[1],ARGV[1])== 0 then" +
"return nil" +
"elseif redis.cal1( 'HINCRBY' ,KEYS[1],ARGV[1],-1) == 0 then" +
"return redis.call( 'del',KEYS[1])" +
"else" +
"return 0" +
"end";
// nil =false 1=true 0=false
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
if (null ==flag){
throw new RuntimeException("this lock doesn't exists");
}
}
// ===========下面两个暂时用不到 不在重写!=====
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public Condition newCondition() {
return null;
}
}
业务逻辑
package com.zzrg.redislock.service.Impl;
import cn.hutool.core.util.IdUtil;
import com.zzrg.redislock.myLock.RedisDistributedLock;
import com.zzrg.redislock.service.InventoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.sql.Time;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author ZzRG
* @Date 2023/6/14 1:09
* @Version 1.0
*/
@Service
@Slf4j
public class InventoryServiceImpl implements InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${service.port}")
private String port;
//v7.0 如何将我们的Lock/unLock+lua脚本自研版的redis分布式锁搞定?|
Lock redisDistributedLock=new RedisDistributedLock(stringRedisTemplate,"RedisLock");
@Override
public String sale() {
String retMessage = "";
redisDistributedLock.lock();
try {
// 1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
// 2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
// 3 扣减库存 每次减少一个
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
System.out.println(retMessage);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
redisDistributedLock.unlock();
}
return retMessage + "\t" + "服务端口号:" + port;
}
}
这样设计出现了分布式锁写死的问题
改造引入工厂模式
package com.zzrg.redislock.config;
import cn.hutool.core.util.IdUtil;
import com.zzrg.redislock.myLock.RedisDistributedLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.locks.Lock;
/**
* @Author ZzRG
* @Date 2023/6/17 17:23
* @Version 1.0
*/
@Component
public class DistributedLockFactory {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private String lockName;
private String uuidValue;
public Lock getDistributedLock(String lockType) {
if (lockType == null) {
return null;
}
if (lockType.equalsIgnoreCase("REDIS")) {
this.lockName = "RedisLock";
return new RedisDistributedLock(stringRedisTemplate,lockName);
} else if (lockType.equalsIgnoreCase("ZOOKEEPER")) {
this.lockName = "ZookeeperLock";
// TODO zoookeeper 版本的分布式锁
return null;
} else if (lockType.equalsIgnoreCase("MYSQL")) {
this.lockName = "MysqlLock";
// TODO MYSQL 版本的分布式锁
return null;
}
return null;
}
}
引入可重入锁业务逻辑
package com.zzrg.redislock.service.Impl;
import cn.hutool.core.util.IdUtil;
import com.zzrg.redislock.config.DistributedLockFactory;
import com.zzrg.redislock.myLock.RedisDistributedLock;
import com.zzrg.redislock.service.InventoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.sql.Time;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author ZzRG
* @Date 2023/6/14 1:09
* @Version 1.0
*/
@Service
@Slf4j
public class InventoryServiceImpl implements InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${service.port}")
private String port;
@Autowired
private DistributedLockFactory distributedLockFactory;
//v7.0 如何将我们的Lock/unLock+lua脚本自研版的redis分布式锁搞定?
@Override
public String sale() {
String retMessage = "";
Lock redisLock=distributedLockFactory.getDistributedLock("REDIS");
redisLock.lock();
try {
// 1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
// 2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
// 3 扣减库存 每次减少一个
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
System.out.println(retMessage + "\t" + "服务端口号:" + port);
testReEntry();
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
redisLock.unlock();
}
return retMessage + "\t" + "服务端口号:" + port;
}
//可重性测试
private void testReEntry() {
Lock redisLock=distributedLockFactory.getDistributedLock("REDIS");
redisLock.lock();
try {
System.out.println("==========测试可重入锁==========");
} finally {
redisLock.unlock();
}
}
中间加入可重入性测试出现问题
改造DistributedLockFactory
package com.zzrg.redislock.config;
import cn.hutool.core.util.IdUtil;
import com.zzrg.redislock.myLock.RedisDistributedLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.locks.Lock;
/**
* @Author ZzRG
* @Date 2023/6/17 17:23
* @Version 1.0
*/
@Component
public class DistributedLockFactory {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private String lockName;
private String uuidValue;
public DistributedLockFactory() {
this.uuidValue = IdUtil.simpleUUID();
}
public Lock getDistributedLock(String lockType) {
if (lockType == null) {
return null;
}
if (lockType.equalsIgnoreCase("REDIS")) {
this.lockName = "RedisLock";
return new RedisDistributedLock(stringRedisTemplate,lockName,uuidValue);
} else if (lockType.equalsIgnoreCase("ZOOKEEPER")) {
this.lockName = "ZookeeperLock";
// TODO zoookeeper 版本的分布式锁
return null;
} else if (lockType.equalsIgnoreCase("MYSQL")) {
this.lockName = "MysqlLock";
// TODO MYSQL 版本的分布式锁
return null;
}
return null;
}
}
改造RedisDistributedLock
// public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName) {
// this.stringRedisTemplate = stringRedisTemplate;
// this.lockName = lockName;
// this.uuidValue = IdUtil.simpleUUID()+":" + Thread.currentThread().getId();
// this.expireTime = 50L;
// }
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName,String uuidValue) {
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = uuidValue+":" + Thread.currentThread().getId();
this.expireTime = 50L;
}
8、自动续期
//自动续期加个钟的Lua脚本
if redis.call('HEISTS',KEYS[1],ARGV[1]) ==1 then
return redis.call('expire',KEYS[1],ARGV[2])
else
return 0
end
package com.zzrg.redislock.myLock;
import cn.hutool.core.util.IdUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @Author ZzRG
* @Date 2023/6/17 16:42
* @Version 1.0
* 我们自研的redis分布式锁,实现了Lock接口
*/
//@component引入DistributedLockFactory 工厂模式,从工厂获得即可
public class RedisDistributedLock implements Lock {
private StringRedisTemplate stringRedisTemplate;
private String lockName; //KEYS[1]
private String uuidValue; //ARGV[1]
private long expireTime; //ARGV[2]
// public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName) {
// this.stringRedisTemplate = stringRedisTemplate;
// this.lockName = lockName;
// this.uuidValue = IdUtil.simpleUUID()+":" + Thread.currentThread().getId();
// this.expireTime = 50L;
// }
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName,String uuidValue) {
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = uuidValue+":" + Thread.currentThread().getId();
this.expireTime = 50L;
}
@Override
public void lock() {
tryLock();
}
@Override
public boolean tryLock() {
try {
tryLock(-1L,TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time == -1L){
String script = "if redis.call('exists',KEYS[1]) ==0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then" +
"redis.call('hincrby',KEYS[1],ARGV[1],1)" +
"redis.call('expire',KEYS[1],ARGV[2])" +
"return 1" +
"else" +
"return 0" +
"end ";
System.out.println("lockName:" + lockName + "\t" + "uuidValue" + uuidValue);
while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))){
//等待20毫秒
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//新建一个后台扫描程序,来坚持key目前的ttl,是否到我们规定的1/2 1/3未来实现续期
this.renewExpire();
return true;
}
return false;
}
@Override
public void unlock() {
String script = "if redis.call( ' HEXISTS',KEYS[1],ARGV[1])== 0 then" +
"return nil" +
"elseif redis.cal1( 'HINCRBY' ,KEYS[1],ARGV[1],-1) == 0 then" +
"return redis.call( 'del',KEYS[1])" +
"else" +
"return 0" +
"end";
// nil =false 1=true 0=false
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
if (null ==flag){
throw new RuntimeException("this lock doesn't exists");
}
}
private void renewExpire() {
String script = "if redis.call('HEISTS',KEYS[1],ARGV[1]) ==1 then" +
"return redis.call('expire',KEYS[1],ARGV[2])" +
"else" +
"return 0" +
"end";
new Timer().schedule(new TimerTask() {
@Override
public void run() {
if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))) {
renewExpire();
}
}
},(this.expireTime * 1000/3));
}
// ===========下面两个暂时用不到 不在重写!=====
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public Condition newCondition() {
return null;
}
}
package com.zzrg.redislock.service.Impl;
import cn.hutool.core.util.IdUtil;
import com.zzrg.redislock.config.DistributedLockFactory;
import com.zzrg.redislock.myLock.RedisDistributedLock;
import com.zzrg.redislock.service.InventoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.sql.Time;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author ZzRG
* @Date 2023/6/14 1:09
* @Version 1.0
*/
@Service
@Slf4j
public class InventoryServiceImpl implements InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${service.port}")
private String port;
@Autowired
private DistributedLockFactory distributedLockFactory;
//V8.0,实现自动续期功能的完善,后台自定义扫描程序,如果规定时间内没有完成业务逻辑,会调用加钟自动续期的脚本nublic string ca1e( )
@Override
public String sale() {
String retMessage = "";
Lock redisLock=distributedLockFactory.getDistributedLock("REDIS");
redisLock.lock();
try {
// 1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
// 2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
// 3 扣减库存 每次减少一个
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
System.out.println(retMessage + "\t" + "服务端口号:" + port);
//暂停120秒钟线程 故意的 主要演示自动续期的功能
try {
TimeUnit.MILLISECONDS.sleep(120);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
redisLock.unlock();
}
return retMessage + "\t" + "服务端口号:" + port;
}
}