文章目录
-
-
- VoucherOrderController.java
- [IVoucherOrderService 及其实现类](#IVoucherOrderService 及其实现类)
- [ILock 及其实现类](#ILock 及其实现类)
- [lua 脚本](#lua 脚本)
-
- [基于 SETNX(setIfAbsent)手写分布式锁](#基于 SETNX(setIfAbsent)手写分布式锁)
-
- [问题1- 不可重入](#问题1- 不可重入)
- [问题2- 不可重试](#问题2- 不可重试)
- [问题3- 超时释放](#问题3- 超时释放)
- [问题4- Redis主从一致性问题(主从异步复制锁丢失)](#问题4- Redis主从一致性问题(主从异步复制锁丢失))
VoucherOrderController.java
java
package com.hmdp.controller;
import com.hmdp.dto.Result;
import com.hmdp.service.IVoucherOrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {
@Autowired
private IVoucherOrderService voucherOrderService;
// redis分布式锁 - 版本
@PostMapping("seckill-redis/{id}")
public Result seckillVouvherRedis(@PathVariable("id") Long voucherId) {
return voucherOrderService.getOrderBySeckillVoucherRedis(voucherId);
}
}
IVoucherOrderService 及其实现类
java
package com.hmdp.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.hmdp.dto.Result;
import com.hmdp.entity.VoucherOrder;
public interface IVoucherOrderService extends IService<VoucherOrder> {
Result getOrderBySeckillVoucher(Long voucherId);
Result getOrderBySeckillVoucherRedis(Long voucherId);
}
java
package com.hmdp.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.SimpleRedisLock;
import com.hmdp.utils.UserHolder;
import org.springframework.aop.framework.AopContext;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
private final RedisIdWorker redisIdWorker;
private final ISeckillVoucherService seckillVoucherService;
private final StringRedisTemplate stringRedisTemplate;
public VoucherOrderServiceImpl(RedisIdWorker redisIdWorker, ISeckillVoucherService seckillVoucherService, StringRedisTemplate stringRedisTemplate) {
this.redisIdWorker = redisIdWorker;
this.seckillVoucherService = seckillVoucherService;
this.stringRedisTemplate = stringRedisTemplate;
}
// redis分布式锁 - 版本
@Override
public Result getOrderBySeckillVoucherRedis(Long voucherId) {
Long userId = UserHolder.getUser().getId();
if (userId == null) {
return Result.fail("用户未登录");
}
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
if (voucher == null) {
return Result.fail("优惠券,不存在");
}
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("活动,未开始");
}
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("活动,已结束");
}
if (voucher.getStock() < 1) {
return Result.fail("优惠券,库存不足");
}
// 创建锁对象:key=lock:order:userId
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
// 获取锁,锁超时1200秒
boolean isLock = lock.tryLock(1200);
// 加锁失败 → 不允许重复下单
if (!isLock) {
return Result.fail("不允许重复下单");
}
try {
// todo 启动类记得开启代理暴露:`@EnableAspectJAutoProxy(exposeProxy = true)`,否则`AopContext.currentProxy()`报错。
VoucherOrderServiceImpl proxy = (VoucherOrderServiceImpl) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId, userId);
} finally {
// 无论正常/异常,都释放锁
lock.unlock();
}
}
@Transactional
public Result createVoucherOrder(Long voucherId, Long userId) {
// todo 一人一单
Integer orderCount = lambdaQuery()
.eq(VoucherOrder::getUserId, userId)
.eq(VoucherOrder::getVoucherId, voucherId)
.count();
if (orderCount > 0) {
return Result.fail("一人仅限一单");
}
// todo 扣库存
boolean success = seckillVoucherService.lambdaUpdate()
.setSql("stock = stock - 1")
.eq(SeckillVoucher::getVoucherId, voucherId)
.gt(SeckillVoucher::getStock, 0)
.update();
if (!success) {
return Result.fail("券的库存不足");
}
// todo 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
save(voucherOrder);
return Result.ok(orderId);
}
}
ILock 及其实现类
java
package com.hmdp.utils;
public interface ILock {
/**
* todo 尝试获取锁
* @param timeoutSec 锁持有的超时时间,过期后自动释放
* @return true:获取锁成功; false:获取锁失败
*/
boolean tryLock(long timeoutSec);
/**
* todo 释放锁
*/
void unlock();
}
java
package com.hmdp.utils;
import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock{
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
// todo 读取resources目录下的 unlock.lua文件
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
// 锁名字,业务标识 例如:"order",最终redis key = lock:order
private String name;
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
// todo 被`static final`修饰的变量,类加载时执行一次赋值,之后值永久不变。
// 锁的key前缀
private static final String KEY_PREFIX = "lock:";
// toString(true), 参数true:去掉UUID中间的横线(38400000‑8cf0‑11bd‑b12e‑0800200c9a66)
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {
// todo 获取线程的全局唯一标识:uuid-线程longId
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
// todo setIfAbsent 底层是:SET key value NX EX 10, 不存在则设置,过期时间为10秒【NX + EX 是一条原子命令】
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
/*todo
setIfAbsent 返回类型是包装类`Boolean`,不是基础类型boolean。
Boolean有三种可能值:
1. Boolean.TRUE → true,key不存在,设置成功(获取锁成功)
2. Boolean.FALSE → false,key已存在(抢锁失败)
3. null → Redis 通信异常、网络故障,返回 null
如果直接写 return success;
等价于拆箱:return success.booleanValue()
当 success == null 的时候,直接抛出 NullPointerException 空指针异常。*/
}
// todo 当前 unlock() 是`get + if + delete`,非原子,会有bug
public void unlockCopy() {
/*
问题:此方法,没有 Lua 脚本,原子性漏洞
当前 unlock() 是`get + if + delete`,非原子。
极端场景:
同一个用户并发多次请求的时,业务执行时间 超过 锁超时时间,
锁 被 Redis 过期删掉,当前线程会删掉其他线程的锁。
解决方法:
unlock 改成 Lua 脚本,lua 脚本只能解决【误删别人锁】
lua只是保证释放锁的原子性,
key 是 `lock:order:userId`
同一个用户、同一把Redis key,先后出现两个不同线程持有这把锁,老线程把新线程的锁删掉。
*/
// 获取线程的全局唯一标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 查询 redis
String lockValue = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
if (threadId.equals(lockValue)) {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
// 利用 lua脚本,实现原子命令
@Override
public void unlock() {
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId()
);
}
}
"singletonList():JDK工具方法,生成一个只能装 1 个元素的List集合(只读集合)"
lua 脚本
resources / unlock.lua
lua 只是保证释放锁的原子性
java
-- 释放分布式锁脚本
-- KEYS[1]:锁的key
-- ARGV[1]:当前线程的唯一标识(UUID-线程id)
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
基于 SETNX(setIfAbsent)手写分布式锁
底层命令:
SET lock:xxx uuid‑threadId NX EX timeout
存在一下几个问题
问题1- 不可重入
现象:同一个线程,再次去获取同一把锁,直接获取锁失败。
场景:
java
// 线程A 拿到锁 lock:order:100
lock.tryLock(30);
// 业务里面又调用另一个方法,再次尝试获取同一把锁
lock.tryLock(30); // 返回false,拿不到锁,业务直接失败
- SETNX 只判断 key 存不存在,不判断是谁的线程。只要key已经存在,不管是不是当前线程,NX直接返回失败。
- 手写 SimpleRedisLock 没有记录锁的持有计数,不支持重入。
- Redisson内部用hash结构:
key:{threadId:count},获取锁计数+1,释放计数‑1,实现可重入。
问题2- 不可重试
tryLock(timeoutSec)当前你的代码逻辑:抢锁一次,失败直接false返回,不会阻塞、不会重试抢锁。
java
boolean isLock = lock.tryLock(1200);
if(!isLock){
return Result.fail("不允许重复下单");
}
- 当前实现:一次SET NX,不成功直接返回。没有循环、没有自旋重试。
- 这是业务层面设计:秒杀场景,用户抢不到直接返回,不需要自旋等待。
- 如果要实现可重试:需要while循环 + sleep自旋抢锁;但高并发下大量自旋会压垮Redis。
- Redisson的
tryLock(waitTime, leaseTime, unit),waitTime就是允许最大自旋重试等待时间。
注意:不是SETNX命令做不到重试;是你写的 SimpleRedisLock 没有做自旋重试逻辑。
问题3- 超时释放
给锁设置过期时间,防止宕机死锁。但是:业务执行时间 > 锁超时时间
时序:
- 线程A获取锁,EX设置10秒过期;
- 业务复杂,执行需要15秒;
- 10秒到,Redis自动把锁key删掉;
- 线程B拿到同一把锁;
- 线程A业务执行完毕,执行unlock(lua 脚本判断 value 匹配,此时A的value已经不存在,删锁无动作);
后果:两个线程同时执行业务,并发安全失效,出现超卖 / 一人多单。
- 手写SimpleRedis锁没有看门狗续期机制。锁时间写死,过期直接释放。
- Redisson:leaseTime = -1 开启看门狗,默认30s,每10s检查线程是否还持有锁,如果业务没执行完就刷新锁过期时间,解决超时释放。
- 简易解决方案:人为把超时时间设置很大,但治标不治本,宕机会长时间死锁。
问题4- Redis主从一致性问题(主从异步复制锁丢失)
Redis主从架构,主节点写锁,异步同步给从节点。
时序:
- 客户端向 master 执行 SET NX,拿到锁;
- master还没来得及把锁key同步给slave从节点,master宕机;
- slave升级成为新的master;
- 新master没有锁key,其他客户端又可以再次获取锁。
结果:锁失效,多个客户端同时拿到锁。
普通主从、哨兵模式解决不了。
- 方案1:Redisson红锁RedLock,需要多个独立master节点,半数以上节点获取锁成功才算拿到锁;资源消耗大,生产很少使用。
- 方案2:Redis7.0 引入wait命令,写锁之后强制等待至少一个从节点同步完成,再返回客户端。
| 问题 | SETNX手写SimpleRedisLock | Redisson |
|---|---|---|
| 可重入 | ❌不支持 | ✅Hash记录计数,支持重入 |
| 可重试(自旋等待) | ❌只尝试一次 | ✅waitTime参数支持自旋重试 |
| 锁超时释放 | ❌业务超过过期时间锁丢失 | ✅看门狗自动续期(leaseTime=-1) |
| 主从切换锁丢失 | ❌异步复制会丢锁 | ❌普通哨兵依旧会丢;红锁可以缓解 |