Redis实现分布式锁
2022年5月29日
redis实现分布式锁
1、为什么要用分布式锁
要介绍分布式锁,首先要提到与分布式锁相对应的是线程锁、进程锁。
- 线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。
- 进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。
- 分布式锁:当多个进程不在同一个系统中(比如分布式系统中控制共享资源访问),用分布式锁控制多个进程对资源的访问。
2、设计分布式锁需要的条件和刚需
- 独占性:OnlyOne,任何时刻只能有且仅有一个线程持有。
- 高可用:若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况
- 防死锁:杜绝死锁,必须有超时控制机制或者撤销操作。
- 不乱抢:防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放。
- 重入性:同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁。
3、设计分布式锁
这里以一个案例的演变,加深对分布式锁设计思想的理解
入门案例
新建Module
boot_redis01和boot_redis02
pom
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.10.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<!--guava-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
<!--web+actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</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>
<!-- jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
<!-- springboot-aop 技术-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
<!--一般通用基础配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
yml
# 端口号
server:
port: 1111 #两个服务1111和2222
# ========================redis相关配置=====================
spring:
redis:
host: localhost
port: 6379
# password: #默认为空
主启动
@SpringBootApplication
public class BootRedis01Application {
public static void main(String[] args) {
SpringApplication.run(BootRedis01Application.class, args);
}
}
业务类controller
@GetMapping("/buy_goods")
public String buy_Goods() {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort;
}
测试
设计案例演变
1.单机版
上面的入门案例,由于buy_Goods
方法没有加锁,因此在单机模式下就会出现线程安全问题。
解决办法:加锁
@GetMapping("/buy_goods")
public String buy_Goods() {
synchronized (this) {//**解决办法**
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort;
}
}
2.分布式场景下
在单机环境下,可以使用synchronized或Lock来实现。
但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),所以需要一个让所有进程都能访问到的锁来实现(比如redis或者zookeeper来构建
)
解决方案:通过redis实现分布式锁
@GetMapping("/buy_Goods2")
public String buy_Goods2() {
String key = "key_lock";
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
if (!flagLock) {
return "抢锁失败";
}
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort;
} finally {
stringRedisTemplate.delete(key);//释放锁
}
}
3.删除锁时加判断
加锁和解锁需要操作同一个线程,防止张冠李戴,删除掉别人的锁
@GetMapping("/buy_Goods3")
public String buy_Goods3() {
String key = "key_lock";
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
if (!flagLock) {
return "抢锁失败";
}
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort;
} finally {
if (stringRedisTemplate.opsForValue().get(key).equals(value)) {//判断value是不是当前线程加的锁
stringRedisTemplate.delete(key);
}
}
}
4.加有效时间
分布式锁必须加有效时间,防止宕机无法释放锁。
注意:加锁和设置有效期需要保证原子性,Redis调用Lua脚本通过eval命令保证代码执行的原子性
@GetMapping("/buy_Goods4")
public String buy_Goods4() throws Exception {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("redisLockPay", value, 30L, TimeUnit.SECONDS);
if (!flag) {
return "抢夺锁失败,请下次尝试";
}
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort;
} finally {
Jedis jedis = RedisUtils.getJedis();
String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
"then " +
"return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
try {
Object result = jedis.eval(script, Collections.singletonList("redisLockPay"), Collections.singletonList(value));
if ("1".equals(result.toString())) {
System.out.println("------del REDIS_LOCK_KEY success");
} else {
System.out.println("------del REDIS_LOCK_KEY error");
}
} finally {
if (null != jedis) {
jedis.close();
}
}
}
}
至此,基于单个Redis节点实现分布式锁完成。
5.最终版本
redis集群环境下,第四个版本也不行,需要RedLock之Redisson落地实现
RedisConfig
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
controller
@Autowired
private Redisson redisson;
@GetMapping("/buy_goods5")
public String buy_Goods5() {
String key = "key_lock";
RLock redissonLock = redisson.getLock(key);
redissonLock.lock();
try {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort;
} finally {
if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {//redissonLock还持有锁,并且持有锁的是当前线程,才可以释放锁
redissonLock.unlock();
}
}
}