创建一个工程
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
</parent>
<dependencies>
<!--springBoot 相关 -->
<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>
<!-- 将库存 和 订单 都存储到 redis 中 ,简化开发,提高响应速度-->
<!-- 引入redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
配置文件
server.port=8080
spring.redis.host=39.105.6.74
spring.redis.port=6379
Test
package k.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Test
* <p>
* Maxim 大鹏起兮云飞扬
* Author ZSK
* Date 2024/5/28 21:03
*/
@RestController
public class Test {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 初始化 库存 订单状态
* @param goods
* @return
*/
@RequestMapping("/init")
public String init(String goods){
// 初始化库存和订单
stringRedisTemplate.opsForValue().set("stack-"+goods,1000+"");
stringRedisTemplate.opsForValue().set("order-"+goods,0+"");
return "当前商品-库存"+stringRedisTemplate.opsForValue().get("stack-"+goods) +
"----订单:"+ stringRedisTemplate.opsForValue().get("order-"+goods);
}
/**
* 获取 库存 订单状态
* @param goods
* @return
*/
@RequestMapping("/state")
public String state(String goods){
return "当前商品-库存"+stringRedisTemplate.opsForValue().get("stack-"+goods) +
"----订单:"+ stringRedisTemplate.opsForValue().get("order-"+goods);
}
}
- 在分布式环境下,传统的一些技术无法保证原子性,还有定时任务也可能会出现重复执行的问题。此时需要分布式锁来解决上述问题
- 为什么锁不住???
- 锁对象不同,所以锁不住
- 锁不在同一个Tomcat服务器
分布式锁原理
分布式锁介绍
Zookeeper实现分布式锁
Zookeeper实现分布式锁原理
实战
引入Zookeeper依赖
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.0</version>
</dependency>
<!--
zk 本身提供的api,比较复杂。难以使用
此时我们可以使用zk封装的工具类 curator
-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.1</version>
</dependency>
配置Zookeeper
package k.config;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* ZkLockConfig
* <p>
* Maxim 大鹏起兮云飞扬
* Author ZSK
* Date 2024/5/28 17:51
*/
@Configuration
public class ZkLockConfig {
/**
* 返回zk 连接
* @return
*/
@Bean// 加入到容器中
public CuratorFramework getConnect(){
/**
* 配置zk 断联以后的重试策略 每隔5s 重试一次,最多3次
*/
RetryPolicy retryPolicy = new ExponentialBackoffRetry(5000,3);
// .builder() .xxx.build() 建造者模式,用于传递 复杂的多参数
CuratorFramework curatorFramework = CuratorFrameworkFactory.builder()
.connectString("39.105.6.74:2181,39.105.6.74:2182,39.105.6.74:2183")
.retryPolicy(retryPolicy).build();
curatorFramework.start();// 开始连接
return curatorFramework;
}
}
使用Zookeeper完成分布式锁
package k.controller;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
* ZookeeperLockController
* <p>
* Maxim 大鹏起兮云飞扬
* Author ZSK
* Date 2024/5/28 20:31
*/
@RestController
public class ZookeeperLockController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private CuratorFramework curatorFramework;
@RequestMapping("/zk")
public String zkKillGoods(String goods) throws Exception {
//CuratorFramework client, zk 客户端
// String path //在那个节点下创建临时有序节点
InterProcessMutex lock = new InterProcessMutex(curatorFramework,"/"+goods);
// lock.acquire(5, TimeUnit.SECONDS)
// 尝试获取锁5s ,5s得到锁返回true ,如果没有得到返回false(客户端也会断开连接,节点自动删除)
if (lock.acquire(5, TimeUnit.SECONDS)){
// 1.读取库存
String strStack = stringRedisTemplate.opsForValue().get("stack-" + goods);
int stack = 0;
if (strStack != null) {
stack = Integer.valueOf(strStack);
if (stack<=0){
lock.release();
return "抱歉库存不足 ,秒杀完毕";
}
}
// 模拟耗时操作
Thread.sleep(1);
// 2.削减库存
stringRedisTemplate.opsForValue().set("stack-" + goods,(stack-1)+"");
// 3.创建订单 订单增加+1
stringRedisTemplate.opsForValue().increment("order-"+goods);
// 释放锁 删除自己创建的临时有序节点
lock.release();
// 4.返回秒杀成功
return "恭喜你,秒杀成功。";
}else {
return "很遗憾抢购失败,请稍后重试";
}
}
}
测试初始化商品,启动两个ab工具去秒杀
http://localhost:8080/init?goods=p70
ab.exe -n 5000 -c 10 http://localhost:8080/zk?goods=p70
ab.exe -n 5000 -c 10 http://localhost:8088/zk?goods=p70
http://localhost:8088/state?goods=p70
Redis完成分布式锁
- 缺点:不可重入,不可以阻塞,setnx和expire分两步执行,非原子操作
Redis实现分布式锁原理
实战
创建redis锁
package k.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* RedisLockConfig
* <p>
* Maxim 大鹏起兮云飞扬
* Author ZSK
* Date 2024/5/28 19:11
*/
@Component
public class RedisLockConfig {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 获取锁
*/
public boolean lock(String key){
//设置国企时间 一定大于业务运行时间
Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(key, "0", 5, TimeUnit.SECONDS);
return absent;
}
/**
* 释放锁
*/
public void unlock(String key){
if (stringRedisTemplate.hasKey(key)){
stringRedisTemplate.delete(key);
}
}
}
基于redis 的 setnx完成分布式不锁
package k.controller;
import k.config.RedisLockConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* RedisLockController
* <p>
* Maxim 大鹏起兮云飞扬
* Author ZSK
* Date 2024/5/28 20:52
*/
@RestController
public class RedisLockController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisLockConfig redisLockConfig;
@RequestMapping("/redis")
public String redis(String goods) throws InterruptedException {
// 尝试获取锁:trues:得到锁 false:阻塞,无法获取锁
if (redisLockConfig.lock(goods)) {
// 读取库存
String strStack = stringRedisTemplate.opsForValue().get("stack-" + goods);
int stack = 0;
if (strStack != null) {
stack = Integer.valueOf(strStack);
if (stack < 0) {
redisLockConfig.unlock(goods);// 释放锁
return "抱歉库存不足,秒杀完毕";
}
}
Thread.sleep(10);//模拟耗时操作
//削减库存
stringRedisTemplate.opsForValue().set("stack-" + goods, (stack - 1) + "");
//创建订单 订单+1
stringRedisTemplate.opsForValue().increment("order-" + goods);
// 释放锁 删除自己创建的临时有序节点
redisLockConfig.unlock(goods);
return "恭喜你,秒杀成功。";
} else {
return "抢购失败,稍后重试";
}
}
}
重新启动2个应用初始化订单,并且测试r
http://localhost:8080/init?goods=p70
ab.exe -n 5000 -c 10 http://localhost:8080/redis?goods=p70
ab.exe -n 5000 -c 10 http://localhost:8088/redis?goods=p70
http://localhost:8088/state?goods=p70
Redisson完成分布式锁
-
本质:setnx+lua脚本
-
优点:解决Redis的不可重入,不可阻塞,非原子操作的问题
-
基于NIO的Netty框架的企业级的开源Redis Client,也提供了分布式锁的支持,Redisson 是架设在 Redis 基础上的一个 Java 驻内存数据网格框架, 充分利用 Redis 键值数据库提供的一系列优势, 基于 Java 实用工具包中常用接口, 为使用者提供了 一系列具有分布式特性的常用工具类.
Redis、Redis lua脚本和Redission加锁对比
方案 | 实现原理 | 优点 | 缺点 |
---|---|---|---|
基于Redis命令 | 1. 加锁:执行setnx,若成功再执行expire添加过期时间2. 解锁:执行delete命令 | 实现简单,相比数据库和分布式系统的实现,该方案最轻,性能最好 | 1.setnx和expire分2步执行,非原子操作;若setnx执行成功,但expire执行失败,就可能出现死锁 2.delete命令存在误删除非当前线程持有的锁的可能 3.不支持阻塞等待、不可重入 |
基于Redis Lua脚本 | 1. 加锁:执行SET lock_name random_value EX seconds NX 命令2. 解锁:执行Lua脚本,释放锁时验证random_value – ARGV[1]为random_value, KEYS[1]为lock_name if redis.call(“get”, KEYS[1]) == ARGV[1] then return redis.call(“del”,KEYS[1]) else return 0 end | 实现逻辑上也更严谨,除了单点问题,生产环境采用用这种方案,问题也不大 | 不支持锁重入,不支持阻塞等待 |
基于Redission | 结合redis和lua脚本实现 | 支持锁重入、支持阻塞等待、Lua脚本原子操作 | Redisson 的宗旨是促进使用者对 Redis 的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。 |
实战
<!-- redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.7</version>
</dependency>
配置redisson 和 redis绑定
@Bean //一般与@Configuration配合使用
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://121.41.51.18:6379").setDatabase(0);
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
使用Redisson 完成分布式锁
@Autowired
private RedissonClient redissonClient;
/**
* 基于redisson 完成分布式锁
* @param goods
* @return
* @throws Exception
*/
@RequestMapping("/redissonKillGoods")
public String redissonKillGoods(String goods) throws Exception {
// redisson的锁
RLock lock = redissonClient.getLock(goods);
//long waitTime, 最大的阻塞等待锁的时间
// long leaseTime, 最少持有锁的时间
// TimeUnit unit 时间单位
if (lock.tryLock(5,5,TimeUnit.SECONDS)){ // 尝试获取锁,返回true,就得到锁,返回fasle 其他线程持有锁 此时无法阻塞获取锁()
// 1.读取库存
String strStack = stringRedisTemplate.opsForValue().get("stack-" + goods);
int stack = 0;
if (strStack != null) {
stack = Integer.valueOf(strStack);
if (stack<=0){
lock.unlock();// 释放锁
return "抱歉库存不足 ,秒杀完毕";
}
}
// 模拟耗时操作
Thread.sleep(10);
// 2.削减库存
stringRedisTemplate.opsForValue().set("stack-" + goods,(stack-1)+"");
// 3.创建订单 订单增加+1
stringRedisTemplate.opsForValue().increment("order-"+goods);
// 释放锁 删除自己创建的临时有序节点
lock.unlock();
// 4.返回秒杀成功
return "恭喜你,秒杀成功";
}else {
return "很遗憾抢购失败,请稍后重试";
}
}
测试
http://localhost:8080/init?goods=p70
ab.exe -n 5000 -c 10 http://localhost:8088/redisson?goods=p70
ab.exe -n 5000 -c 10 http://localhost:8080/redisson?goods=p70
http://localhost:8088/state?goods=p70
Redisson原理
源码分析
redisson所有指令都通过lua脚本执行,redis支持lua脚本原子性执行
- 如果一个key的持有时间是30s,但是执行的业务逻辑超过了30s,此时就有可能锁失效问题,如何解决?
看门狗机制
- redisson中有一个
watchdog
的概念,翻译过来就是看门狗,它会在你获取锁之后,每隔10秒帮你把key的超时时间设为30s,这样的话,就算一直持有锁也不会出现key过期了,其他线程获取到锁的问题了。 - redisson的“看门狗”逻辑保证了没有死锁发生,如果机器宕机了,看门狗也就没了。此时就不会延长key的过期时间,到了30s之后就会自动过期了,其他线程可以获取到锁
Redission请求流程图
不足点
- 它加锁时只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况
- 在Redis的master节点上拿到了锁;但是这个加锁的key还没有同步到slave节点;master故障,发生故障转移,slave节点升级为master节点;导致锁丢失。
RedLock完成分布式锁并解决锁丢失问题
本质:封装了多个RedissonLock,只有超过一半以上的RedissonLock得到锁时,才会认为RedLock 得到锁
RedLock解决锁丢失问题
实战
package k.controller;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
* RedLockController
* <p>
* Maxim 大鹏起兮云飞扬
* Author ZSK
* Date 2024/5/29 15:02
*/
@RestController
public class RedLockController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedissonClient redissonClient;
//基于Red锁 完成分布式锁
@RequestMapping("/red")
public String red(String goods) throws Exception {
RLock lock1 = redissonClient.getLock(goods + "-lock1");
RLock lock2 = redissonClient.getLock(goods + "-lock2");
RLock lock3 = redissonClient.getLock(goods + "-lock3");
//创建RedLock
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
if (redLock.tryLock(5,5, TimeUnit.SECONDS)){//尝试获取Lock true:得到锁 false:其他线程持有锁,此时阻塞无法获取锁
//读取库存
String strStack = stringRedisTemplate.opsForValue().get("stack-" + goods);
int stack = 0;
if (strStack!=null){
stack= Integer.valueOf(strStack);
if (stack<=0){
redLock.unlock();//释放锁
return "库存不足,秒杀失败";
}
}
// 模拟耗时操作
Thread.sleep(10);
// 2.削减库存
stringRedisTemplate.opsForValue().set("stack-" + goods,(stack-1)+"");
// 3.创建订单 订单增加+1
stringRedisTemplate.opsForValue().increment("order-"+goods);
// 释放锁 删除自己创建的临时有序节点
redLock.unlock();
// 4.返回秒杀成功
return "恭喜你,秒杀成功";
}else{
return "抢购失败,青山后重试";
}
}
}
http://localhost:8080/init?goods=p70
ab.exe -n 5000 -c 10 http://localhost:8080/red?goods=p70
ab.exe -n 5000 -c 10 http://localhost:8088/red?goods=p70
http://localhost:8088/state?goods=p70