day04-redis实战篇 2026-5-4
今天是学习redis的第四天,我会继续学习,并持续更新🚚
redis实战篇
📘优惠卷秒杀
📌添加秒杀券
@RestController
@RequestMapping("/voucher")
public class VoucherController {
@Resource
private IVoucherService voucherService;
/ **
* 新增秒杀券
* @param voucher 优惠券信息,包含秒杀信息
* @return 优惠券id
*/
@PostMapping("seckill")
public Result addSeckillVoucher(@RequestBody Voucher voucher) {
voucherService.addSeckillVoucher(voucher);
return Result.ok(voucher.getId());
}
}

📌实现秒杀下单
下单时需要判断两点:
-
秒杀是否开始或结束,如果尚未开始或已经结束就无法下单
-
库存是否充足,不足无法下单

-
Controller层
@RestController @RequestMapping("/voucher-order") public class VoucherOrderController { @Resource private IVoucherOrderService voucherOrderService; @PostMapping("seckill/{id}") public Result seckillVoucher(@PathVariable("id") Long voucherId) { return voucherOrderService.seckillVoucher(voucherId); } } -
Service层
@Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService { @Resource private ISeckillVoucherService seckillVoucherService; @Resource private RedisWorker redisWorker; @Override @Transactional public Result seckillVoucher(Long voucherId) { //1.查询优惠卷 SeckillVoucher byId = seckillVoucherService.getById(voucherId); //2.判断秒杀是否开始 if (byId.getBeginTime().isAfter(LocalDateTime.now())){ return Result.fail("秒杀尚未开始"); } //3.判断秒杀是否结束 if (byId.getEndTime().isBefore(LocalDateTime.now())){ return Result.fail("秒杀已结束"); } //4.判断库存是否充足 if (byId.getStock() < 1){ return Result.fail("库存不足"); } //5.扣减库存 boolean success = seckillVoucherService.update() //自定义SQL语句 .setSql("stock = stock - 1") .eq("voucher_id", voucherId) .update(); if (!success){ return Result.fail("库存不足"); } //6.创建订单 VoucherOrder voucherOrder = new VoucherOrder(); //6.1.生成订单id long order = redisWorker.nextId("order"); voucherOrder.setId(order); //6.2.用户id Long id = UserHolder.getUser().getId(); voucherOrder.setUserId(id); //6.3.代金券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); //7.返回订单结果 return Result.ok(order); } }
📌超卖问题

超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:

乐观锁实现的两种常见方法:
- 🔥版本号法
为表添加一个version字段,每次查询时查询库存和版本号,减少库存时将版本号加1,同时加上where条件判断,如果version和一开始查询到的版本号相同再更改,否则不能修改

- CAS法
再版本号法的基础上将where条件判断改为判断stock库存是否变化

📌乐观锁解决超卖
-
Service层
//5.扣减库存 boolean success = seckillVoucherService.update() //自定义SQL语句 .setSql("stock = stock - 1")//set stock = stock - 1 .eq("voucher_id", voucherId).gt("stock", 0)// where id = ? and stock > 0 .update(); if (!success){ return Result.fail("库存不足"); }
超卖这样的线程安全问题,解决方案有哪些?
-
悲观锁:添加同步锁,让线程串行执行
- 优点:简单粗暴
- 缺点:性能一般
-
乐观锁:不加锁,在更新时判断是否有其它线程在修改
-
优点:性能好
-
缺点:存在成功率低的问题
-
📌一人一单
因为这里是要用新增记录,所以要使用悲观锁synchronized
-
Service层
@Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService { @Resource private ISeckillVoucherService seckillVoucherService; @Resource private RedisWorker redisWorker; @Override public Result seckillVoucher(Long voucherId) { //1.查询优惠卷 SeckillVoucher byId = seckillVoucherService.getById(voucherId); //2.判断秒杀是否开始 if (byId.getBeginTime().isAfter(LocalDateTime.now())) { return Result.fail("秒杀尚未开始"); } //3.判断秒杀是否结束 if (byId.getEndTime().isBefore(LocalDateTime.now())) { return Result.fail("秒杀已结束"); } //4.判断库存是否充足 if (byId.getStock() < 1) { return Result.fail("库存不足"); } Long userid = UserHolder.getUser().getId(); synchronized (userid.toString().intern()) { //获取当前对象的代理对象,因为Spring的事务是通过代理对象来实现的,不获取代理对象,就会导致事务失效 IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVourcherOrder(voucherId); } } @Transactional public Result createVourcherOrder(Long voucherId) { //5.一人一单 Long userid = UserHolder.getUser().getId(); //5.1.查询订单 int count = query().eq("user_id", userid).eq("voucher_id", voucherId).count(); //5.2.判断订单是否存在 if (count > 0) { //用户已经购买过 return Result.fail("不能重复下单"); } //6.扣减库存 boolean success = seckillVoucherService.update() //自定义SQL语句 .setSql("stock = stock - 1")//set stock = stock - 1 .eq("voucher_id", voucherId).gt("stock", 0)// where id = ? and stock > 0 .update(); if (!success) { return Result.fail("库存不足"); } //7.创建订单 VoucherOrder voucherOrder = new VoucherOrder(); //7.1.生成订单id long order = redisWorker.nextId("order"); voucherOrder.setId(order); //7.2.用户id Long id = UserHolder.getUser().getId(); voucherOrder.setUserId(id); //7.3.代金券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); //8.返回订单结果 return Result.ok(order); } } -
引入依赖
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency> -
启动方法添加自动代理注解
@MapperScan("com.hmdp.mapper") @SpringBootApplication @EnableAspectJAutoProxy(exposeProxy = true) public class HmDianPingApplication { public static void main(String[] args) { SpringApplication.run(HmDianPingApplication.class, args); } }
安全问题
当我们使用集群部署时,synchronized是基于jvm的,即它只会监视当前jvm的线程不能对其他jvm的线程做出操作,所以synchronized不能解决集群下的一人一单问题,这是就要使用分布式锁

📌分布式锁
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。

| 🧠 对比项 | 💙 MySQL | 💚 Redis(推荐) | 🧡 Zookeeper |
|---|---|---|---|
| 🔒 互斥实现 | 数据库锁机制 | SETNX + 过期时间 | 临时有序节点 |
| 🟢 高可用 | ✔️ 好 | ✔️ 好 | ✔️ 好 |
| ⚡ 性能 | ⚠️ 一般 | 🚀 很高(首选) | ⚠️ 一般 |
| 🛡️ 安全性 | 断开连接自动释放 | ⏰ 超时自动释放 | 📌 会话断开自动删除节点 |
| 💡 使用建议 | 简单场景可用 | 🔥 高并发首选方案 | 适合强一致 / 分布式协调 |
实现分布式锁时需要实现的两个基本方法:
- 获取锁:
- 互斥:确保只能有一个线程获取锁
- 非阻塞:尝试一次,成功返回true,失败返回false
添加锁,NX是互斥、EX是设置超时时间 SET lock thread1 NX EX 10
- 释放锁:
- 手动释放
- 超时释放:获取锁时添加一个超时时间
释放锁,删除即可 DEL key

基于redis的分布式锁
-
Utils层
public interface ILock { /* * 尝试获取锁 *timeoutSec:超时时间 * return:是否获取成功 * */ boolean tryLock(long timeoutSec); /* * 释放锁 * */ void unLock(); }public class SimpleRedisLock implements ILock{ private String name; private StringRedisTemplate stringRedisTemplate; private final static String KEY_PREFIX = "lock:"; public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) { this.name = name; this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean tryLock(long timeoutSec) { //获取线程的id long thread_id = Thread.currentThread().getId(); //获取锁 Boolean success = stringRedisTemplate.opsForValue() .setIfAbsent(KEY_PREFIX + name, thread_id + "", timeoutSec, TimeUnit.SECONDS); return Boolean.TRUE.equals(success); } @Override public void unLock() { //释放锁 stringRedisTemplate.delete(KEY_PREFIX + name); } } -
Service层
//创建锁对象 SimpleRedisLock simpleRedisLock = new SimpleRedisLock("order:" + userid, stringRedisTemplate); //获取锁 boolean success = simpleRedisLock.tryLock(10); //判断获取锁成功 if (!success) { return Result.fail("一人只允许下一单"); } try { //获取当前对象的代理对象,因为Spring的事务是通过代理对象来实现的,不获取代理对象,就会导致事务失效 IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVourcherOrder(voucherId); } finally { //释放锁 simpleRedisLock.unLock(); }
分布式锁误删问题

解决步骤
-
在获取锁时存入线程标示(可以用UUID表示)
-
在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致
-
如果一致则释放锁
-
如果不一致则不释放锁

-
-
Utils层
public class SimpleRedisLock implements ILock { private String name; private StringRedisTemplate stringRedisTemplate; private final static String KEY_PREFIX = "lock:"; private final static String ID_PREFIX = UUID.randomUUID().toString(); public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) { this.name = name; this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean tryLock(long timeoutSec) { //获取线程的id String thread_id = ID_PREFIX + Thread.currentThread().getId(); //获取锁 Boolean success = stringRedisTemplate.opsForValue() .setIfAbsent(KEY_PREFIX + name, thread_id , timeoutSec, TimeUnit.SECONDS); return Boolean.TRUE.equals(success); } @Override public void unLock() { //获取线程的id String thread_id = ID_PREFIX + Thread.currentThread().getId(); //获取锁中标识 String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name); //判断标识是否一致 if (thread_id.equals(id)) { //释放锁 stringRedisTemplate.delete(KEY_PREFIX + name); } } }
使用lua脚本解决分布式锁原子性问题

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:lua脚本官方网站
这里重点介绍Redis提供的调用函数,语法如下:
-- 执行redis命令
redis.call('命令名称','key',‘其它参数',…)
例如,我们要执行set name jack,则脚本是这样:
-- 执行set name jack
redis.call('set', 'name', 'jack')
例如,我们要先执行set name Rose,再执行get name,则脚本如下:
--先执行 set name jack
redis.call('set', 'name', 'jack')
-- 再执行 get name
local name = redis.call('get', 'name')
-- 返回
return name
例如,我们要执行redis.call(‘set’,‘name’,‘jack’)这个脚本,语法如下:
-- 调用脚本
EVAL "return redis.call('set', 'name', 'jack' )" 0
-- EVAL后面跟的是脚本内容,脚本内容后面跟的是脚本需要的key类型的变量
-
lua脚本
-- 比较线程的标识与锁的标识是否一致 if (redis.call('get',KEYS[1])==ARGV[1]) then -- 释放锁 return redis.call('del',KEYS[1]) end return 0 -
Utils层
@Override public void unLock() { //调用lua脚本 stringRedisTemplate.execute (UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX + name), ID_PREFIX + Thread.currentThread().getId()); }
Redisson
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
官方链接
-
步骤
- 引入依赖
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.6</version> </dependency>- 配置Redisson客户端
@Configuration public class RedissonConfig { @Bean public RedissonClient redissonClient() { //配置 Config config = new Config(); config.useSingleServer().setAddress("redis://192.168.228.129:6379").setPassword("1234"); //创建RedissonClient对象 return Redisson.create(config); } }- 使用Redisson分布式锁
@Resource private RedissonClient redissonClient; @Test void testRedisson() throws InterruptedException { //获取锁(可重入),指定锁的名称 RLock lock = redissonClientlgetLock("anyLock"); // 尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位 boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS); // 判断释放获取成功 if(isLock) { try { System.out.println("执行业务”); }finally { // 释放锁 lock.unlock(); } } }
🧠Redisson可重入锁
- 原理:
🚀 加锁流程
- 判断锁是否存在
-
❌ 不存在:
- 直接加锁(
SETNX) - 创建
Hash结构 field = 线程标识value = 1(重入次数)- ⏰ 设置过期时间(避免死锁)
- 直接加锁(
-
✅ 已存在:
-
判断
field(线程标识)是否是自己-
👍 是自己:
- 说明是重入
value + 1
-
❌ 不是自己:
- 获取锁失败
-
-
-
🔓 释放锁流程
-
判断当前线程是否为锁持有者
-
❌ 不是自己:
- 提示:锁已被其他线程释放或无权限
-
👍 是自己:
value - 1(减少重入次数)
-
-
判断是否真正释放锁:
-
🔢
value > 0:- 仍有重入,不释放锁
-
🧹
value = 0:-
删除锁(释放)

获取锁的lua脚本local key = KEYS[1] ;-- 锁的key local threadId= ARGV[1] ;-- 线程唯一标识 local releaseTime = ARGV[2] ;-- 锁的自动释放时间 -- 判断是否存在 if(redis.call('exists', key) == 0) then -- 不存在,获取锁 redis.call('hset', key, threadId, '1'); -- 设置有效期 redis.call('expire', key, releaseTime) ; return 1 ;-- 返回结果 end; -- 锁已经存在,判断threadId是否是自己 if(redis.call('hexists', key, threadId) == 1) then -- 不存在,获取锁,重入次数+1 redis.call('hincrby', key, threadId, '1'); -- 设置有效期 redis.call('expire', key, releaseTime) ; return 1 ;-- 返回结果 end; return 0 ;-- 代码走到这里,说明获取锁的不是自己,获取锁失败释放锁的lua脚本
local key = KEYS[1] ;-- 锁的key local threadId = ARGV[1] ;-- 线程唯一标识 local releaseTime = ARGV[2] ;-- 锁的自动释放时间 -- 判断当前锁是否还是被自己持有 if (redis.call('HEXISTS', key, threadId) == 0) then return nil ;-- 如果已经不是自己,则直接返回 end; -- 是自己的锁,则重入次数-1 local count = redis. call('HINCRBY', key, threadId, -1); -- 判断是否重入次数是否已经为0 if (count > 0) then -- 大于说明不能释放锁,重置有效期然后返回 redis.call('EXPIRE', key, releaseTime); return nil; else -- 等于0说明可以释放锁,直接删除 redis.call('DEL', key); return nil; end;
-
-
🧠Redisson锁重试和WacthDog机制

Redisson分布式锁原理:
-
可重入:利用hash结构记录线程id和重入次数
-
可重试:利用信号量和PubSub功能实现等待、唤醒,获取
锁失败的重试机制
- 超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间
🧠Redisson分布式锁主从一致性问题

1)不可重入Redis分布式锁:
-
原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判
断线程标示 -
缺陷:不可重入、无法重试、锁超时失效
2)可重入的Redis分布式锁:
-
原理:利用hash结构,记录线程标示和重入次数;利用
watchDog延续锁时间;利用信号量控制锁重试等待 -
缺陷:redis宕机引起锁失效问题
3)Redisson的multiLock:
-
原理:多个独立的Redis节点,必须在所有节点都获取重入
锁,才算获取锁成功 -
缺陷:运维成本高、实现复杂
🧾总结:今天在学习redis实战篇,还是太高估自己的自治力了,想着在51期间学习的,结果玩了3天,今天才开始学习,在学redisson的时候根本就听不懂完全被拷打,原理这一部分对于初学者还是太难了.今天快学完了优惠劵的秒杀了,争取明天能学完实战篇,明天继续,加油!🔥🔥🔥
更多推荐


所有评论(0)