项目 / 智学云课堂
智学云课堂 · 在线教育微服务系统
十六个微服务模块,一个一个实现、一个一个调通
16 微服务模块
66 完成学习目标
12 业务章节
4 分布式锁封装
这个项目做了什么
智学云课堂是一个在线教育平台的后端系统。我依据课程讲义与学习目标, 把十六个微服务模块逐个实现并调通,覆盖学习计划与进度、互动问答、点赞系统、 积分签到、实时排行榜、优惠券管理与优惠方案推荐。
和城觅不同,这个项目的重点不在某一条链路的极限性能,而在模块之间的协作边界: 哪些状态该放 Redis、哪些该落库、定时任务和实时请求怎么不互相打架、 缓存与数据库的一致性由谁来保证。
架构怎么长出来的
M1 骨架
先把服务注册与调用跑通
Nacos 做注册中心与配置中心,OpenFeign 做服务间调用,网关统一入口与鉴权。这一层不跑通,后面全是空中楼阁。
NacosOpenFeignGateway
M2 学习
计划、进度与互动
学习计划生成、进度上报、互动问答。状态变更频繁但只关心最终值,进度这类热数据放 Redis 而不是每次都写库。
学习计划进度互动问答
M3 互动
点赞要防重复,也要能扛读
点赞是典型的「写少读多 + 必须去重」。用 Redis Set 记录谁点过,计数走缓存,落库异步化。
点赞去重Caffeine
M4 激励
积分签到与实时排行榜
签到用 BitMap 把「某月哪天签过」压进一个整数,排行榜用 Redis ZSet 实时排序,历史榜单分表落库。
BitMapZSet排行榜
M5 交易
优惠券与优惠方案推荐
券的领取、核销要防超发,用分布式锁封装统一处理;优惠方案推荐按用户已有券与订单金额算最优组合。
优惠券分布式锁推荐
M6 批处理
定时任务与历史数据沉降
XXL-JOB 跑批把热点榜单沉降成历史榜单并分表持久化,减轻 Redis 与主表的长期压力。
XXL-JOB分表沉降
我解决过的工程问题
签到用「查一次再写一次」,跨月那天算错了两天
CRITICAL- 问题
- 签到最初按「读上次签到日期 → 判断是否连续 → 更新连续天数」实现,两步之间没有原子性。
- 后果
- 用户跨零点、或跨月最后一天点签到时,两次请求都读到同一份旧状态,连续天数被加了两遍,或者整月记录被重置。
- 方案
- 签到标记改用 BitMap(一个用户一个月一个整数),连续天数从 BitMap 现算而不是存状态,读改写变成幂等的位或操作。
- 验证
- 补边界用例:跨日、跨月、重复点击、断签后重签,四种场景的连续天数与位图都符合预期。
四个模块各写了一遍分布式锁,四份都有点毛病
IMPORTANT- 问题
- 点赞去重、券领取、秒杀、签到各写了一份加锁逻辑,每份细节都不同。
- 后果
- 有的忘了 finally 释放,有的锁过期时间拍脑袋写死,有的没处理重入。同一类问题被修了四次,还修得不一致。
- 方案
- 抽出一层分布式锁封装(基于 Redisson):统一的 key 命名、统一的超时与等待策略、统一的失败返回,四个调用点只关心业务语义。
- 取舍
- 为什么不做成注解式 AOP?因为锁的粒度与业务强相关,注解很容易被误用到「锁了一整个方法」的位置。
排行榜的实时榜单和历史榜单对不上
IMPORTANT- 问题
- 实时榜单在 Redis ZSet,历史榜单靠 XXL-JOB 跑批落库分表。跑批失败或重复执行时两边数据会错位。
- 方案
- 跑批做成按周期幂等:以周期为唯一键写入历史表,重复执行是覆盖而不是追加;实时 ZSet 只在写成功后清理,失败则保留等待下次重跑。
- 验证
- 手动中止一次跑批再重跑,验证历史表不出现重复周期记录。
一段关键实现
BitMap 签到与连续天数计算:把「本月哪天签过」压进一个整数,连续天数从位图现算。
/** 签到:本月第 day 天置位,返回是否首次签到(幂等) */
public boolean signIn(Long userId, LocalDate date) {
String key = "sign:" + userId + ":" + date.getYear() + date.getMonthValue();
int offset = date.getDayOfMonth() - 1;
// setbit 返回的是「原来那一位的值」:1 说明今天已经签过
boolean firstTime = redis.opsForValue().setBit(key, offset, true) == false;
if (!firstTime) return false;
// 连续天数不存状态,直接从位图往前数,天然幂等
int streak = 0;
for (int i = offset; i >= 0; i--) {
if (!redis.opsForValue().getBit(key, i)) break;
streak++;
}
updateStreakCache(userId, streak);
return true;
}
已知限制
- 未接入链路追踪,一个跨五个服务的调用出问题时要靠日志时间戳人工拼链路。
- 优惠方案推荐是规则计算,不是模型推荐;规则数量上去以后维护成本会明显变高。
- 缓存与数据库的一致性用的是「先写库再删缓存」,没有处理删除失败的重试补偿。
- XXL-JOB 的跑批是单机执行器,没有做分片广播,历史榜单数据量再大就只能加长执行窗口。
参考来源
- Redis 官方文档 · Bitmapshttps://redis.io/docs/latest/develop/data-types/bitmaps/官方
- Redis 官方文档 · Sorted setshttps://redis.io/docs/latest/develop/data-types/sorted-sets/官方
- XXL-JOB 官方文档 · 分布式任务调度https://www.xuxueli.com/xxl-job/官方
- Spring Cloud Alibaba · Nacos 配置与服务发现https://sca.aliyun.com/en/docs/2023/user-guide/nacos/quick-start/官方