《黑马点评》04-分布式锁_完整代码【简单易懂注释版】
·
目录
3.VoucherOrderServiceImpl.java
二十二、分布式锁-Redisson快速入门-Redisson设置锁
4.VoucherOrderServiceImpl.java
十九、分布式锁-实现Redis分布式锁版本1
1.interface ILock
package com.hmdp.utils;
import java.util.concurrent.TimeUnit;
/**
* 分布式锁接口
*/
public interface ILock {
/**
* 尝试获取锁
* @param key 锁的键
* @param timeout 超时时间
* @param timeUnit 时间单位
* @return 是否获取成功
*/
boolean tryLock(String key, Integer timeout, TimeUnit timeUnit);
/**
* 释放锁
* @param key 锁的键
*/
void unlock(String key);
}
2.SimpleRedisLock.java
package com.hmdp.utils;
import cn.hutool.core.util.BooleanUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* 简单Redis分布式锁实现类
*/
@Component
public class SimpleRedisLock implements ILock{
@Resource
private StringRedisTemplate stringRedisTemplate;
// 锁的前缀,用于构建完整的Redis键
private static final String KEY_PREFIX = "lock:";
/**
* 尝试获取锁
* @param key 锁的键
* @param timeout 超时时间
* @param timeUnit 时间单位
* @return 是否获取成功
*/
@Override
public boolean tryLock(String key, Integer timeout, TimeUnit timeUnit) {
// 获取当前线程的id
long threadId = Thread.currentThread().getId();
// 尝试获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + key, String.valueOf(threadId), timeout, timeUnit);
return BooleanUtil.isTrue(success);
}
/**
* 释放锁
* @param key 锁的键
*/
@Override
public void unlock(String key) {
stringRedisTemplate.delete(KEY_PREFIX + key);
}
}
3.VoucherOrderServiceImpl.java
package com.hmdp.service.impl;
@Resource
private SimpleRedisLock simpleRedisLock;
/**
* 优惠券秒杀下单
* @param voucherId 优惠券ID
* @return Result
*/
@Override
public Result seckillVoucher(Long voucherId) {
// 1.查询优惠券信息
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判断秒杀是否开始
LocalDateTime now = LocalDateTime.now();
if (voucher.getBeginTime().isAfter(now)) {
return Result.fail("秒杀尚未开始");
}
// 3.判断秒杀是否结束
if (voucher.getEndTime().isBefore(now)) {
return Result.fail("秒杀已结束");
}
// 4.判断库存是否充足
if (voucher.getStock() < 1) {
return Result.fail("库存不足");
}
// TODO 实现Redis分布式锁版本
// 5.1获取当前登录用户id
Long userId = UserHolder.getUser().getId();
// 5.2获取锁
String key = "order:" + userId; //业务名+用户id
boolean isLock = simpleRedisLock.tryLock(key, 1200, TimeUnit.SECONDS); // 超时设置为业务运行的10倍就行,这里是为了测试调高
// 5.3判断锁是否存在
if (!isLock){
// 5.4获取锁失败,返回错误信息或重试
return Result.fail("不允许重复下单");
}
try {
// 6.创建订单
// TODO 使用代理对象调用事务方法(不然下面的事务会失效)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} catch (IllegalStateException e) {
throw new RuntimeException(e);
} finally {
// 释放锁
simpleRedisLock.unlock(key);
}
}
二十、分布式锁-解决Redis分布式锁误删问题
1.SimpleRedisLock.java
package com.hmdp.utils;
/**
* 简单Redis分布式锁实现类
*/
@Component
public class SimpleRedisLock implements ILock{
@Resource
private StringRedisTemplate stringRedisTemplate;
// 锁的前缀,用于构建完整的Redis键
private static final String KEY_PREFIX = "lock:";
// 线程标识前缀,用于区分不同JVM实例中的线程
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
/**
* 尝试获取锁
* @param key 锁的键
* @param timeout 超时时间
* @param timeUnit 时间单位
* @return 是否获取成功
*/
@Override
public boolean tryLock(String key, Integer timeout, TimeUnit timeUnit) {
// 获取当前线程的标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 尝试获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + key, threadId, timeout, timeUnit);
return BooleanUtil.isTrue(success);
}
/**
* 释放锁
* @param key 锁的键
*/
@Override
public void unlock(String key) {
// 获取当前线程的标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁中的标识
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + key);
// 判断标识是否一致
if (threadId.equals(id)){
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + key);
}
}
}
二十一、分布式锁-Lua脚本解决多条命令原子性问题
1.Redis EVAL 使用笔记
Redis 支持使用 Lua 脚本 来执行一组原子操作,常用命令是 EVAL。
语法:
EVAL script numkeys key [key ...] arg [arg ...]
- script:Lua 脚本
- numkeys:后面有多少个 key
- key:传递的 key
- arg:传递的参数
下面是一个简单的例子 👇
# 1. 使用 EVAL 执行 Lua 脚本,直接 set 一个 key
eval "return redis.call('set','name','jack')" 0
# 查看结果,name 被设置为 "jack"
get name
# 2. 使用 KEYS 和 ARGV 参数,动态传递 key 和 value
# 这里 numkeys = 1,表示有一个 key
# KEYS[1] = name, ARGV[1] = rose
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 name rose
# 查看结果,name 被更新为 "rose"
get name
# 3. 再次执行 Lua 脚本,修改 name 的值为 "shushu"
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 name shushu
# 查看结果,name 被更新为 "shushu"
get name
2.笔记总结
redis.call用来在 Lua 脚本里调用 Redis 命令。KEYS用来传递 key,ARGV用来传递参数(value 等)。numkeys表示有多少个KEYS参数,其余都算到ARGV里。- 脚本在 Redis 内部执行,具备原子性,不会被打断。
3.unlock.lua
释放锁的业务流程是这样的:
- 获取锁中的线程标示
- 判断是否与指定的标示(当前线程标示)一致
- 如果一致则释放锁(删除)
- 如果不一致则什么都不做
这个流程确保了只有拥有正确线程标示的线程才能释放锁,从而避免了其他线程误操作的可能性。
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by 20719.
--- DateTime: 2025/8/29 15:47
---
--获取锁中的线程标识
local id = redis.call('get',KEYS[1])
--比较线程标识与锁中的标识是否一致
if(id == ARGV[1]) then
--释放锁 del key
return redis.call('del',KEYS[1])
end
return 0
4.SimpleRedisLock.java
package com.hmdp.utils;
/**
* 简单Redis分布式锁实现类
*/
@Component
public class SimpleRedisLock implements ILock{
@Resource
private StringRedisTemplate stringRedisTemplate;
// 锁的前缀,用于构建完整的Redis键
private static final String KEY_PREFIX = "lock:";
// 线程标识前缀,用于区分不同JVM实例中的线程
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
// Lua脚本用于安全地释放锁,确保只有持有锁的线程才能释放它
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
// static 代码块用于初始化类级别的静态变量
static {
// 初始化解锁Lua脚本
UNLOCK_SCRIPT = new DefaultRedisScript<>();
// 设置Lua脚本文件的位置
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
// 设置Lua脚本的返回值类型
UNLOCK_SCRIPT.setResultType(Long.class);
}
/**
* 尝试获取锁
* @param key 锁的键
* @param timeout 超时时间
* @param timeUnit 时间单位
* @return 是否获取成功
*/
@Override
public boolean tryLock(String key, Integer timeout, TimeUnit timeUnit) {
// 获取当前线程的标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 尝试获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + key, threadId, timeout, timeUnit);
return BooleanUtil.isTrue(success);
}
/**
* 释放锁
* @param key 锁的键
*/
@Override
public void unlock(String key) {
// 获取锁中的标识
String KEYS = KEY_PREFIX + key;
// 获取当前线程的标识
String ARGC= ID_PREFIX + Thread.currentThread().getId();
// 释放锁
stringRedisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(KEYS),ARGC);
}
// /**
// * 释放锁
// * @param key 锁的键
// */
// @Override
// public void unlock(String key) {
// // 获取当前线程的标识
// String threadId = ID_PREFIX + Thread.currentThread().getId();
// // 获取锁中的标识
// String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + key);
// // 判断标识是否一致
// if (threadId.equals(id)){
// // 释放锁
// stringRedisTemplate.delete(KEY_PREFIX + key);
// }
// }
}
二十二、分布式锁-Redisson快速入门-Redisson设置锁
1.pom.xml
<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
2.application.yaml
其实这个还是原来Redis的配置,引入Redisson时无需更改的
server:
port: 8081
spring:
application:
name: hmdp
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/hmdp?allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC
username: root
password: 123456
redis:
host: 192.168.200.128 # Redis服务器地址
port: 6379 # Redis服务器端口
password: # Redis服务器密码(如果有的话)
database: 0 # 指定Redis数据库索引,默认为0
lettuce:
pool:
max-active: 10 # 连接池最大连接数
max-idle: 10 # 连接池最大空闲连接数
min-idle: 1 # 连接池最小空闲连接数
time-between-eviction-runs: 10s # 空闲连接检测间隔时间
jackson:
default-property-inclusion: non_null # JSON处理时忽略非空字段
mybatis-plus:
type-aliases-package: com.hmdp.entity # 别名扫描包
logging:
level:
com.hmdp: debug
3.RedissonConfig.java
虽然 redisson-spring-boot-starter 可以自动装配,省去手写 Config 配置类,但在某些场景下可能会与 Spring 自带的 Redis 配置产生冲突,因此一般不推荐使用。
package com.hmdp.config;
//Redisson客户端的配置
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host:localhost}")
private String host;
@Value("${spring.redis.port:6379}")
private int port;
@Value("${spring.redis.password:}") // 如果没密码就是空
private String password;
@Value("${spring.redis.database:0}")
private int database;
@Bean
public RedissonClient redissonClient(){
// 创建Redisson配置对象
Config config = new Config();
// 配置单节点模式的Redis连接信息
config.useSingleServer()
.setAddress("redis://" + host + ":" + port) // 设置Redis服务器地址和端口
.setDatabase(database); // 设置使用的数据库索引
// 如果设置了密码,则配置密码
if (password != null && !password.isEmpty()){
config.useSingleServer()
.setPassword(password); // 设置Redis访问密码
}
// 根据配置创建并返回Redisson客户端实例
return Redisson.create(config);
}
}
4.VoucherOrderServiceImpl.java
使用Redisson获取锁,参数分别为:等待时间、过期时间、时间单位
过期时间一定一定一定要默认,这样才能有看门狗续期机制
boolean isLock = lock.tryLock(-1,TimeUnit.SECONDS);
package com.hmdp.service.impl;
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Resource
private SimpleRedisLock simpleRedisLock;
@Resource
private RedissonClient redissonClient;
/**
* 优惠券秒杀下单
* @param voucherId 优惠券ID
* @return Result
*/
@Override
public Result seckillVoucher(Long voucherId) throws InterruptedException {
// 1.查询优惠券信息
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判断秒杀是否开始
LocalDateTime now = LocalDateTime.now();
if (voucher.getBeginTime().isAfter(now)) {
return Result.fail("秒杀尚未开始");
}
// 3.判断秒杀是否结束
if (voucher.getEndTime().isBefore(now)) {
return Result.fail("秒杀已结束");
}
// 4.判断库存是否充足
if (voucher.getStock() < 1) {
return Result.fail("库存不足");
}
// TODO 实现Redis分布式锁版本
// 5.1获取当前登录用户id
Long userId = UserHolder.getUser().getId();
// 5.2获取锁
// String key = "order:" + userId; //业务名+用户id
// boolean isLock = simpleRedisLock.tryLock(key, 1200, TimeUnit.SECONDS); // 超时设置为业务运行的10倍就行,这里是为了测试调高
// TODO 使用Redisson创建锁对象
RLock lock = redissonClient.getLock("lock:order:" + userId);
// TODO 使用Redisson获取锁,参数分别为:等待时间、过期时间、时间单位
// -1表示无限等待,直到获取到锁为止
// 过期时间默认,这样才能有看门狗续期机制
boolean isLock = lock.tryLock(-1,TimeUnit.SECONDS);
// 5.3判断锁是否存在
if (!isLock){
// 5.4获取锁失败,返回错误信息或重试
return Result.fail("不允许重复下单");
}
try {
// 6.创建订单
// TODO 使用代理对象调用事务方法(不然下面的事务会失效)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} catch (IllegalStateException e) {
throw new RuntimeException(e);
} finally {
// TODO 使用Redisson释放锁
lock.unlock();
}
}
// 6.创建订单(方法)
@Transactional
public Result createVoucherOrder(Long voucherId) {
// 5.一人一单
// 获取当前登录用户id
Long userId = UserHolder.getUser().getId();
// 5.1查询订单
long count = lambdaQuery()
.eq(VoucherOrder::getUserId, userId)
.eq(VoucherOrder::getVoucherId, voucherId)
.count();
// 5.2判断是否存在
if (count > 0){
// 5.3用户已经购买过了
return Result.fail("领取次数过多");
}
// 6.扣减库存
boolean success = seckillVoucherService.lambdaUpdate()
.setSql("stock = stock - 1")
.eq(SeckillVoucher::getVoucherId, voucherId)
.gt(SeckillVoucher::getStock, 0) // TODO 乐观锁解决库存超卖问题
.update();
if (!success){
return Result.fail("库存不足");
}
// 7、创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 7.1设置id
long id = redisIdWorker.nextId("order");
voucherOrder.setId(id);
// 7.2设置用户id
voucherOrder.setUserId(userId);
// 7.3设置优惠券id
voucherOrder.setVoucherId(voucherId);
// 保存到数据库
save(voucherOrder);
// 8.返回订单id
return Result.ok(voucherId);
}
}
更多推荐
所有评论(0)