天机学堂项目总结(day11~day12)
目录
Day11-01. 今日课程介绍
Day11-02. 分布式锁 - 集群下的锁失效问题
问题:集群下锁失效的原因?
Day11-03. 分布式锁 - 简单分布式锁原理
Day11-04. 分布式锁 - 实现简单分布式锁
Day11-05. 分布式锁 - 分布式锁的问题及 Redisson简介
Day11-06. 分布式锁 - Redisson 快速入门
Day11-07. 分布式锁 - 基于自定义注解改造分布式锁
Day11-08. 分布式锁 - 简单工厂模式改造分布式锁
问题:下图switch代码不够优雅,该什么哪个工厂模式?
Day11-09. 分布式锁 - 策略模式改造分布式锁
问题:上面工厂模式使用Hash,频繁哈希运行影响性能,该怎么办 ?
问题:获取锁有多种方式,失败处理也有多种方式,该使用什么策略?
Day11-10. 分布式锁 - SPEL 表达式动态锁名称
问题:在当前业务中,我们的锁对象本来应该是当前登录用户,是动态获取的。而加锁是基于注解参数添加的,在编码时就需要指定。怎么办?
Day11-11. 异步领券 - 优化思路
Day11-12. 异步领券 - 管理优惠券缓存
Day11-13. 异步领券 - 基于 Redis 的领券和消息发送
问题:解释下面代码?
Day11-14. 异步领券 - 监听 MQ 消息实现异步领券
Day11 - 练习 1 - 兑换码异步兑换的思路分析
Day11 - 练习 2 - 基于 Lua 的优化思路分析
4.1.超发问题
锁实现的问题
性能问题
Day12-01. 今日课程介绍
Day12-02. 定义优惠规则 - 业务流程分析
Day12-03. 定义优惠规则 - 编写优惠规则
Day12-04. 优惠方案推荐 - 思路分析
Day12-05. 优惠方案推荐 - 定义接口
Day12-06. 优惠方案推荐 - 优惠券查询和初筛
Day12-07. 优惠方案推荐 - 细筛和券的全排列组合
Day12-08. 优惠方案推荐 - 优惠明细的算法分析
Day12-09. 优惠方案推荐 - 实现优惠明细的算法
Day12-10. 优惠方案推荐 - 与交易服务联调测试
Day12-11. 优惠方案推荐 - CompletableFuture 并发运算
Day12-12. 优惠方案推荐 - 筛选最优解
末尾页
Day11-01. 今日课程介绍
Day11-02. 分布式锁 - 集群下的锁失效问题
问题:集群下锁失效的原因?
一、本质原因:JVM 级锁的 “孤岛效应”
本地锁(synchronized/Lock)只能锁住当前 JVM 进程内的线程,跨 JVM 集群共享资源时,锁机制完全隔离,互不感知。
如图所示:
JVM1拿到了本地锁,正在操作 MySQL
JVM2看不到 JVM1 的锁,本地线程也拿到了锁,同样去操作 MySQL
结果:多个 JVM 同时操作同一批数据,并发安全被破坏,数据一致性失效
二、失效场景详细拆解
1. 本地锁无法跨进程通信
现象:单机下
synchronized完美生效,但集群部署多实例后,锁彻底失效。原理:每个 JVM 有独立的监视器(ObjectMonitor),彼此 “井水不犯河水”。
2. 数据库无分布式锁机制
MySQL 本身提供的是行锁 / 表锁,是针对数据库资源的锁,并非针对业务代码逻辑的锁。
图中 JVM1 和 JVM2 都连接到同一 MySQL,但 MySQL 无法感知 Java 层的
synchronized,无法自动为业务逻辑加锁。3. 竞态条件(Race Condition)
假设业务逻辑是:查询库存 -> 扣减库存
JVM1 查询库存 = 100,还没来得及扣减
JVM2 同时查询库存也 = 100
两个 JVM 都执行扣减,最终库存只减了 1,导致超卖 / 超发
三、如何解决?(必背解决方案)
必须使用分布式锁替代本地锁,实现跨 JVM 强一致性。
方案 1:基于 Redis 分布式锁(推荐)
核心:利用 Redis 的单线程特性 +
SETNX命令代码逻辑:
java
运行
// 只有一个客户端能设置成功,返回1;其他返回0 if (jedis.setnx("lock_key", "thread_1") == 1) { try { // 执行业务 } finally { // 释放锁 jedel.del("lock_key"); } }方案 2:基于 ZooKeeper 分布式锁
核心:临时有序节点 + Watcher 监听机制
优势:强一致性,自动释放锁,无死锁风险
方案 3:基于 Redisson 分布式锁(企业级首选)
核心:Redis 的 Java 驻留客户端,封装了复杂的锁机制
特点:支持可重入、锁超时、主从高可用、分布式锁、阻塞锁等
四、面试一句话总结
JVM 级的本地锁(synchronized)仅在单机环境有效,无法跨进程控制。集群环境下多个 JVM 并行执行,缺乏统一的全局锁协调,导致锁机制完全失效,必须引入 Redis/ZooKeeper 等分布式锁来保证数据一致性。
Day11-03. 分布式锁 - 简单分布式锁原理
Day11-04. 分布式锁 - 实现简单分布式锁
package com.tianji.promotion.utils; import com.tianji.common.utils.BooleanUtils; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.StringRedisTemplate; import java.util.concurrent.TimeUnit; @RequiredArgsConstructor public class RedisLock { private final String key; private final StringRedisTemplate redisTemplate; /** * 尝试获取锁 * @param leaseTime 锁自动释放时间 * @param unit 时间单位 * @return 是否获取成功,true:获取锁成功;false:获取锁失败 */ public boolean tryLock(long leaseTime, TimeUnit unit){ // 1.获取线程名称 String value = Thread.currentThread().getName(); // 2.获取锁 Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, leaseTime, unit); // 3.返回结果 return BooleanUtils.isTrue(success); } /** * 释放锁 */ public void unlock(){ redisTemplate.delete(key); } }Day11-05. 分布式锁 - 分布式锁的问题及 Redisson简介
这个的话可以回顾微服务基础知识课程那里复习
Day11-06. 分布式锁 - Redisson 快速入门
Day11-07. 分布式锁 - 基于自定义注解改造分布式锁
Day11-08. 分布式锁 - 简单工厂模式改造分布式锁
问题:下图switch代码不够优雅,该什么哪个工厂模式?
使用键值对将类型作为key,操作作为value;
Day11-09. 分布式锁 - 策略模式改造分布式锁
问题:上面工厂模式使用Hash,频繁哈希运行影响性能,该怎么办 ?
MyLockFactory内部持有了一个Map,key是锁类型枚举,值是创建锁对象的Function。注意这里不是存锁对象,因为锁对象必须是多例的,不同业务用不同锁对象;同一个业务用相同锁对象。
MyLockFactory内部的Map采用了
EnumMap。只有当Key是枚举类型时可以使用EnumMap,其底层不是hash表,而是简单的数组。由于枚举项数量固定,因此这个数组长度就等于枚举项个数,然后按照枚举项序号作为角标依次存入数组。这样就能根据枚举项序号作为角标快速定位到数组中的数据。
问题:获取锁有多种方式,失败处理也有多种方式,该使用什么策略?
Day11-10. 分布式锁 - SPEL 表达式动态锁名称
问题:在当前业务中,我们的锁对象本来应该是当前登录用户,是动态获取的。而加锁是基于注解参数添加的,在编码时就需要指定。怎么办?
Day11-11. 异步领券 - 优化思路
Day11-12. 异步领券 - 管理优惠券缓存
Day11-13. 异步领券 - 基于 Redis 的领券和消息发送
问题:解释下面代码?
整体功能
根据优惠券 ID,从 Redis Hash 结构缓存中查询优惠券信息,缓存不存在则返回 null,后续会走数据库查询,是标准缓存穿透前置逻辑
1. 拼接 Redis 缓存 Key
java
运行
String key = PromotionConstants.COUPON_CACHE_KEY_PREFIX + couponId;
用固定前缀 + 优惠券 ID拼接唯一 Redis Key,做 Key 隔离,避免不同业务 Key 冲突
例:前缀
coupon:+ id=10 → 最终 key=coupon:102. 查询 Redis Hash 全量数据
java
运行
Map<Object, Object> objMap = redisTemplate.opsForHash().entries(key);
opsForHash():操作 RedisHash 哈希结构(适合存储对象,字段 field = 属性名,value = 属性值)
entries(key):一次性取出该 Key 下所有 field-value 键值对,封装为 Map 返回优惠券对象所有字段,都存在同一个 Hash Key 里
3. 缓存空值判断
java
运行
if (objMap == null || objMap.isEmpty()) { return null; }
如果 Redis 里没有这个 Hash、或者 Hash 为空,说明缓存无数据
直接返回 null,上层逻辑就会去查询 MySQL 数据库
同时这里也为缓存空值防穿透做铺垫:数据库不存在的优惠券,也往 Redis 存空 Hash,避免重复击穿 DB
4. Map 转 Java 实体 Bean
java
运行
return BeanUtils.mapToBean(objMap, Coupon.class, false, CopyOptions.create());
BeanUtils.mapToBean:把 Redis 取出的 Map 键值对,反射封装成 Coupon 优惠券实体对象
isToCamelCase: false:不做下划线转驼峰匹配,Redis 字段名和实体属性名严格一致
CopyOptions.create():配置拷贝规则,规范字段匹配、类型转换面试核心考点
为什么用 Hash 不用 String 存对象?Hash 可以单独更新优惠券某一个字段,不用全序列化覆盖;String 每次都要整个对象序列化 / 反序列化,性能差
objMap == null || objMap.isEmpty()判断顺序先判空指针,再判集合空,防止空指针异常标准缓存流程:先查 Redis 缓存 → 缓存无 → 查 DB → 写入 Redis 缓存
Hash 结构非常适合存储电商商品、优惠券这类对象型数据
一句话面试总结
这段代码是优惠券Redis Hash 缓存查询逻辑:拼接唯一缓存 Key,查询 Hash 全字段数据,缓存不存在直接返回 null,存在则将 Hash 键值对转换为优惠券 Java 实体返回。
Day11-14. 异步领券 - 监听 MQ 消息实现异步领券
Day11 - 练习 1 - 兑换码异步兑换的思路分析
Day11 - 练习 2 - 基于 Lua 的优化思路分析
4.1.超发问题
面试官:你做的优惠券功能如何解决券超发的问题?
答:券超发问题常见的有两种场景:
券库存不足导致超发
发券时超过了每个用户限领数量
这两种问题产生的原因都是高并发下的线程安全问题。往往需要通过加锁来保证线程安全。不过在处理细节上,会有一些差别。
首先,针对库存不足导致的超发问题,也就是典型的库存超卖问题,我们可以通过乐观锁来解决。也就是在库存扣减的SQL语句中添加对于库存余量的判断。当然这里不必要求必须与查询到的库存一致,因为这样可能导致库存扣减失败率太高。而是判断库存是否大于0即可,这样既保证了安全,也提高了库存扣减的成功率。
其次,对于用户限领数量超出的问题,我们无法采用乐观锁。因为要判断是否超发,需要先查询用户已领取数量,然后判断有没有超过限领数量,没有超过才会新增一条领取记录。这就导致后续的新增操作会影响超发的判断,只能利用悲观锁将查询已领数量、判断超发、新增领取记录几个操作封装为原子操作。这样才能保证线程的安全。
锁实现的问题
面试官:那你这里聊到悲观锁,是用什么来实现的呢?
由于在我们项目中,优惠券服务是多实例部署形成的负载均衡集群。因此考虑到分布式下JVM锁失效问题,我们采用了基于Redisson的分布式锁。
(此处面试官可能会追问怎么实现的呢?如果没有追问就自己往下说,不要停)
不过Redisson分布式锁的加锁和释放锁逻辑对业务侵入比较多,因此我就对其做了二次封装(强调是自己做的),利用自定义注解,AOP,以及SPEL表达式实现了基于注解的分布式锁。(面试官可能会问SPEL用来做什么,没问的话就自己说)
我在封装的时候用了工厂模式来选择不同的锁类型,利用了策略模式来选择锁失败重试策略,利用SPEL表达式来实现动态锁名称。
(面试官可能追问锁失败重试的具体策略,没有就自己往下说)
因为获取锁可能会失败嘛,失败后可以重试,也可以不重试。如果重试结束可以直接报错,也可以快速结束。综合来说可能包含5种不同失败重试策略。例如:失败后直接结束、失败后直接抛异常、失败后重试一段时间然后结束、失败后重试一段时间然后抛异常、失败后一直重试。
(面试官如果追问Redisson原理,可以参考黑马的Redis视频中对于Redisson的讲解)
注意,这个回答也可以用作这个面试题:你在项目中用过什么设计模式啊?要学会举一反三。
性能问题
面试官:加锁以后性能会比较差,有什么好的办法吗?
答:解决性能问题的办法有很多,针对领券问题,我们可以采用MQ来做异步领券,起到一个流量削峰和整型的作用,降低数据库压力。
具体来说,我们可以将优惠券的关键信息缓存到Redis中,用户请求进入后先读取Redis缓存,做好优惠券库存、领取数量的校验,如果校验不通过直接返回失败结果。如果校验通过则通过MQ发送消息,异步去写数据库,然后告诉用户领取成功即可。
当然,前面说的这种办法也存在一个问题,就是可能需要多次与Redis交互。因此还有一种思路就是利用Redis的LUA脚本来编写校验逻辑来代替java编写的校验逻辑。这样就只需要向Redis发一次请求即可完成校验。
Day12-01. 今日课程介绍
Day12-02. 定义优惠规则 - 业务流程分析
Day12-03. 定义优惠规则 - 编写优惠规则
Day12-04. 优惠方案推荐 - 思路分析
Day12-05. 优惠方案推荐 - 定义接口
Day12-06. 优惠方案推荐 - 优惠券查询和初筛
Day12-07. 优惠方案推荐 - 细筛和券的全排列组合
Day12-08. 优惠方案推荐 - 优惠明细的算法分析
Day12-09. 优惠方案推荐 - 实现优惠明细的算法
Day12-10. 优惠方案推荐 - 与交易服务联调测试
Day12-11. 优惠方案推荐 - CompletableFuture 并发运算
Day12-12. 优惠方案推荐 - 筛选最优解
末尾页
分布式锁失效问题与解决方案
在集群环境下,JVM本地锁(synchronized/Lock)会因"孤岛效应"失效,无法跨JVM进程协调。这导致多个实例同时操作共享数据,破坏数据一致性。解决方案包括:
- Redis分布式锁:利用SETNX命令实现
- ZooKeeper分布式锁:基于临时有序节点和Watcher机制
- Redisson分布式锁:企业级方案,支持多种锁特性
项目中采用Redisson分布式锁,并通过自定义注解、AOP和SPEL表达式进行封装,结合工厂模式和策略模式优化实现。针对性能问题,可通过MQ异步处理或Redis Lua脚本优化校验逻辑。
