9.1 分布式锁需要的条件和刚需
- 独占性
- 任何时刻有且只有一个线程持有这个锁
- 高可用
- 若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况
- 高并发请求下,依旧性能很好
- 防死锁
- 不能出现死锁问题,必须有超时重试机制或者撤销操作,有个终止跳出的途径
- 不乱抢
- 防止张冠李戴,只能解锁自己的锁,不能把别人的锁给释放了
- 重入性
- 同一节点的同一线程如果获得锁之后,他可以再次获取这个锁
9.2 编码
1 搭建环境
-
创建工程 redis_distributed_lock2
-
POM
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.10</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.xfcy</groupId> <artifactId>redis_distributed_lock2</artifactId> <version>0.0.1-SNAPSHOT</version> <name>redis_distributed_lock2</name> <description>redis_distributed_lock2</description> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <lombok.version>1.16.18</lombok.version> </properties> <dependencies> <!-- redisson--> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.4</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--SpringBoot与Redis整合依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!--swagger2--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> <!--通用基础配置lombok/hutool--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.8</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
-
YML
server.port=7777 spring.application.name=redis_distributed_lock2 # ========================swagger2===================== # http://localhost:7777/swagger-ui.html swagger2.enabled=true spring.mvc.pathmatch.matching-strategy=ant_path_matcher # ========================redis单机===================== spring.redis.database=0 spring.redis.host=192.168.238.111 spring.redis.port=6379 spring.redis.password=123456 spring.redis.lettuce.pool.max-active=8 spring.redis.lettuce.pool.max-wait=-1ms spring.redis.lettuce.pool.max-idle=8 spring.redis.lettuce.pool.min-idle=0
-
主启动类
@SpringBootApplication public class RedisDistributedLock2Application { public static void main(String[] args) { SpringApplication.run(RedisDistributedLock2Application.class, args); } }
-
业务类
-
Swagger2Config
import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; /** * @author 晓风残月Lx * @date 2023/4/1 10:25 */ @Configuration @EnableSwagger2 public class Swagger2Config { @Value("${swagger2.enabled}") private Boolean enabled; @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .enable(enabled) .select() .apis(RequestHandlerSelectors.basePackage("com.xfcy")) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("springboot利用swagger2构建api接口文档 "+"\t"+ DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDateTime.now())) .description("springboot+redis整合") .version("1.0") .termsOfServiceUrl("https://www.baidu.com/") .build(); } }
-
RedisConfig
import org.redisson.Redisson; import org.redisson.config.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * @author 晓风残月Lx * @date 2023/4/1 10:31 */ @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) { RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(lettuceConnectionFactory); //设置key序列化方式string redisTemplate.setKeySerializer(new StringRedisSerializer()); //设置value的序列化方式json redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.afterPropertiesSet(); return redisTemplate; } }
-
InventoryService
import cn.hutool.core.util.IdUtil; 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.StringRedisTemplate; import org.springframework.stereotype.Service; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author 晓风残月Lx * @date 2023/4/1 10:34 */ @Service @Slf4j public class InventoryService { @Autowired private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port; private Lock lock = new ReentrantLock(); 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; } }
-
InvetoryController
import com.xfcy.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 晓风残月Lx * @date 2023/4/1 10:32 */ @RestController @Api(tags = "redis分布式锁测试") public class InvetoryController { @Autowired private InventoryService inventoryService; @ApiOperation("扣减库存sale,一次卖一个") @GetMapping(value = "/inventory/sale") public String sale() { return inventoryService.sale(); } @ApiOperation("扣减库存saleByRedisson,一次卖一个") @GetMapping(value = "/inventory/saleByRedisson") public String saleByRedisson() { return inventoryService.saleByRedisson(); } }
-
2 分布式锁
v2.0 - v6.0
InventoryService 实现的一个简单版本的 分布式锁
import cn.hutool.core.util.IdUtil;
import com.xfcy.mylock.DistributedLockFactory;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 晓风残月Lx
* @date 2023/4/1 10:34
*/
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
/**
* v6.0 使用 Lua 脚本 将 final的判断 + del 弄成原子操作
* 问题: 兼顾锁的可重入性
* 但是基本 v6.0 版本已经够用
* @return
*/
public String sale() {
String retMessage = "";
String key = "xfcyRedisLock";
String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value, 30L, TimeUnit.SECONDS)) {
// 暂停20毫秒,进行递归重试
try {
Thread.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 {
// 改进点,修改为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), value);
}
return retMessage + "\t" + "服务端口号:" + port;
}
/**
* v5.0 存在问题: final的判断 + del 不是一行原子操作,需要lua脚本进行修改
* @return
*/
// public String sale() {
// String retMessage = "";
// String key = "xfcyRedisLock";
// String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
// while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value, 30L, TimeUnit.SECONDS)) {
// // 暂停20毫秒,进行递归重试
// try {
// Thread.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 {
// // 改进点,只能删除自己的key,而不是别的客户端
// // 问题:不能保证原子操作
// if (stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(value)) {
// stringRedisTemplate.delete(key);
// }
// }
// 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); 只能自己删除自己的,需要添加判断是否是自己的锁
* @return
*/
// public String sale() {
// String retMessage = "";
// String key = "xfcyRedisLock";
// String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
//
// // 不用递归了,高并发下容易出错,我们用自旋替代递归方法调用;也不用if,用while来替代
// while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value, 30L, TimeUnit.SECONDS)) {
// // 暂停20毫秒,进行递归重试
// try {
// Thread.sleep(20);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// // 无法保证原子性
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 {
// stringRedisTemplate.delete(key);
// }
// return retMessage + "\t" + "服务端口号:" + port;
// }
/**
* 3.2版 setnx 用while判断
* 问题: setnx过后,正在进行业务逻辑操作时,没有走到finally之前,整个微服务down机了,导致锁一直存在
* 不是程序出了问题,如果程序问题,最后还是会执行finally
* 没办法保证解锁(没有过期时间,该key一直存在),需要加入过期时间限定key
*/
// public String sale() {
// String retMessage = "";
// String key = "xfcyRedisLock";
// String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
//
// // 不用递归了,高并发下容易出错,我们用自旋替代递归方法调用;也不用if,用while来替代
// while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value)) {
// // 暂停20毫秒,进行递归重试
// try {
// Thread.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;
// }
/**
* 3.1版 setnx 递归调用 容易导致 StackOverflowError
* 高并发唤醒后推荐用while判断而不是if
* @return
*/
// public String sale() {
// String retMessage = "";
// String key = "xfcyRedisLock";
// String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
//
// Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
// // flag = false 抢不到的线程要继续重试 。。。
// if (!flag) {
// // 暂停20毫秒,进行递归重试
// try {
// Thread.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 单机版加锁配合nginx和jmeter压测后,不满足高并发分布式锁的性能要求,出现超卖
*/
// private Lock lock = new ReentrantLock();
//
// 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;
// }
}
v7.0 - v8.0
v 8.0 其实面对不是特别高的并发场景足够用了,单机redis也够用了
- 要兼顾锁的重入性
- setnx不满足了,需要hash结构的hset
- 上锁和解锁都用 Lua 脚本来实现原子性
- 引入工厂模式 DistributedLockFactory, 实现 Lock 接口,实现redis的可重入锁
- lock() 加锁的关键逻辑
- 加锁 实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间
- 自旋
- 续期
- unlock() 解锁关键逻辑
- 将 Key 键删除,但是也不能乱删,只能自己删自己的锁
- lock() 加锁的关键逻辑
- 实现自动续期功能的完善,后台自定义扫描程序,如果规定时间内没有完成业务逻辑,会调用加钟自动续期的脚本
InventoryService
@Autowired
private DistributedLockFactory distributedLockFactory;
/**
* v8.0 实现自动续期功能的完善,后台自定义扫描程序,如果规定时间内没有完成业务逻辑,会调用加钟自动续期的脚本
*
* @return
*/
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;
// 演示自动续期的的功能
// try {
// TimeUnit.SECONDS.sleep(120);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
redisLock.unlock();
}
return retMessage + "\t" + "服务端口号:" + port;
}
/**
* v7.0 兼顾锁的可重入性 setnx不满足了,需要hash结构的hset
* 上锁和解锁都用 Lua 脚本实现原子性
* 引入工厂模式 DistributedLockFactory 实现Lock接口 ,实现 redis的可重入锁
*
* @return
*/
// //private Lock redisDistributedLock = new RedisDistributedLock(stringRedisTemplate, "xfcyRedisLock");
//
// public String sale() {
// String retMessage = "";
//
// Lock redisLock = distributedLockFactory.getDistributedLock("redis");
// redisLock.lock();
//
// //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);
//
// // 测试可重入性
// //testReEntry();
//
// } else {
// retMessage = "商品卖完了,o(╥﹏╥)o";
// }
// } finally {
// redisLock.unlock();
// //redisDistributedLock.unlock();
// }
// return retMessage + "\t" + "服务端口号:" + port;
// }
//
// private void testReEntry() {
// Lock redisLock = distributedLockFactory.getDistributedLock("redis");
// redisLock.lock();
//
// //redisDistributedLock.lock();
// try {
// System.out.println("测试可重入锁");
// } finally {
// redisLock.unlock();
// //redisDistributedLock.unlock();
// }
// }
mylock/DistributedLockFactory
package com.xfcy.mylock;
import cn.hutool.core.util.IdUtil;
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 晓风残月Lx
* @date 2023/4/1 22:14
*/
@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 = "xfcyRedisLock";
return new RedisDistributedLock(stringRedisTemplate, lockName, uuidValue);
}else if (lockType.equalsIgnoreCase("ZOOKEEPER")) {
this.lockName = "xfcyZookeeperLock";
// TODO zoookeeper 版本的分布式锁
return null;
}else if (lockType.equalsIgnoreCase("MYSQL")){
this.lockName = "xfcyMysqlLock";
// TODO MYSQL 版本的分布式锁
return null;
}
return null;
}
}
mylock/RedisDistributedLock
package com.xfcy.mylock;
import cn.hutool.core.util.IdUtil;
import com.sun.org.apache.xpath.internal.operations.Bool;
import org.springframework.beans.factory.annotation.Autowired;
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 晓风残月Lx
* @date 2023/4/1 21:38
* 自研的redis分布式锁,实现 Lock 接口
*/
// @Component 引入DistributedLockFactory工厂模式,从工厂获得即可
public class RedisDistributedLock implements Lock {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private String lockName; // KEYS[1]
private String uuidValue; // ARGV[1]
private long expireTime; // ARGV[2]
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName, String uuidValue) {
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = uuidValue + ":" + Thread.currentThread().getId();
this.expireTime = 30L;
}
@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))) {
// 暂停 60ms
Thread.sleep(60);
}
// 新建一个后台扫描程序,来监视key目前的ttl,是否到我们规定的 1/2 1/3 来实现续期
resetExpire();
return true;
}
return false;
}
@Override
public void unlock() {
String script = "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
"return nil " +
"elseif redis.call('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 exists0");
}
}
private void resetExpire() {
String script = "if redis.call('HEXISTS',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))) {
resetExpire();
}
}
}, (this.expireTime * 1000) / 3);
}
// 下面两个用不上
// 下面两个用不上
// 下面两个用不上
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public Condition newCondition() {
return null;
}
}
9.3 缺点
如果我们的分布式锁的服务器挂了,也就是单点故障,我们添加一个从机,但是复制是异步的。
- Client A 获取 master 中的锁
- 在对密钥的写入传输到从机之前,主服务器崩溃了
- 从机被提升为主机
- Client B 获取对统一资源 A 已持有锁的锁,违反安全规定