Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

redis-practice

1. 项目介绍

redis-practice 是一个可直接运行的 Redis 实战项目,使用 Spring Boot 3、MyBatis-Plus、MySQL 和 Redis 实现商品查询、普通订单和秒杀订单。项目覆盖 String、Hash、List、Set、ZSet、Cache Aside、缓存穿透、缓存击穿、缓存雪崩、Redisson 分布式锁、Lua 幂等校验、事务后删缓存以及数据库条件扣库存。

所有接口统一返回:

{
  "code": 200,
  "message": "success",
  "data": {},
  "timestamp": 1710000000000
}

2. 技术栈

  • Java 17
  • Spring Boot 3.3.5
  • Maven
  • MyBatis-Plus 3.5.7
  • MySQL 8
  • Redis 7
  • Spring Data Redis / Redisson
  • Lombok / Jakarta Validation / Jackson
  • JUnit 5 / Spring Boot Test / Mockito

3. 项目结构

redis-practice
├── pom.xml
├── docker-compose.yml
├── README.md
├── src/main/java/com/wang/redispractice
│   ├── common          # 统一响应、响应码、Redis Key
│   ├── config          # Redis、Redisson、MyBatis-Plus、Jackson
│   ├── controller      # HTTP 接口
│   ├── dto             # 请求 DTO 与参数校验
│   ├── entity          # 7 张表的 MyBatis-Plus 实体
│   ├── exception       # 业务异常
│   ├── handler         # 全局异常处理
│   ├── mapper          # MyBatis-Plus Mapper
│   ├── service         # Service 接口
│   ├── service/impl    # 缓存、订单、秒杀实现
│   ├── task            # 可选定时缓存预热
│   ├── util            # 订单号生成器
│   └── vo              # 返回 VO
├── src/main/resources
│   ├── application.yml
│   ├── application-dev.yml
│   ├── lua/idempotency.lua
│   ├── mapper/ProductStockMapper.xml
│   └── sql/init.sql
└── src/test             # 缓存、幂等、秒杀和上下文测试

4. 启动 MySQL 和 Redis

前置要求:Docker Desktop 已启动,3306 和 6379 端口未被占用。

docker compose up -d
docker compose ps

MySQL 容器首次创建数据卷时会自动执行 src/main/resources/sql/init.sql,创建 7 张表和演示商品、库存、秒杀活动。MySQL 与 Redis 分别使用 mysql_dataredis_data 命名卷持久化。

默认连接信息:

服务 Host Port Database/User/Password
MySQL localhost 3306 redis_practice / root / 123456
Redis localhost 6379 database 0 / 无密码

查看日志或停止容器:

docker compose logs -f mysql redis
docker compose down

只有需要彻底重置练习数据时才执行 docker compose down -v;该命令会删除两个数据卷。

5. 启动项目

mvn clean test
mvn spring-boot:run

服务默认监听 http://localhost:8080。开发配置均可由环境变量覆盖:

MYSQL_HOST=localhost MYSQL_PORT=3306 MYSQL_DATABASE=redis_practice \
MYSQL_USERNAME=root MYSQL_PASSWORD=123456 \
REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD= \
mvn spring-boot:run

Windows PowerShell 可先使用 $env:MYSQL_HOST="localhost" 的形式设置环境变量,再运行 Maven。

可选定时预热默认关闭。设置 practice.cache-warmup-enabled=true 后,默认每天 04:00 预热;也可通过 practice.cache-warmup-cron 修改 cron。

6. 接口调用示例

Redis 数据结构

# String:验证码保存 5 分钟
curl -X POST http://localhost:8080/api/redis/string \
  -H "Content-Type: application/json" \
  -d '{"phone":"13800138000","code":"9527"}'
curl http://localhost:8080/api/redis/string/13800138000

# Hash:用户信息
curl -X POST http://localhost:8080/api/redis/hash/user/1001 \
  -H "Content-Type: application/json" \
  -d '{"name":"Wang","email":"wang@example.com","age":28}'
curl http://localhost:8080/api/redis/hash/user/1001

# List:右侧入队、左侧出队
curl -X POST http://localhost:8080/api/redis/list/message \
  -H "Content-Type: application/json" -d '{"message":"hello redis"}'
curl http://localhost:8080/api/redis/list/message

# Set:收藏及人数
curl -X POST http://localhost:8080/api/redis/set/product/1/favorite/1001
curl http://localhost:8080/api/redis/set/product/1/favorite/count

# ZSet:积分与 Top 10
curl -X POST http://localhost:8080/api/redis/zset/ranking \
  -H "Content-Type: application/json" -d '{"userId":1001,"score":50}'
curl http://localhost:8080/api/redis/zset/ranking

商品缓存

# 第一次 cacheHit=false,第二次 cacheHit=true
curl http://localhost:8080/api/products/1
curl http://localhost:8080/api/products/1

# 互斥锁与逻辑过期版本
curl http://localhost:8080/api/products/1/mutex
curl http://localhost:8080/api/products/1/logical-expire

# 前 100 个有效商品预热
curl -X POST http://localhost:8080/api/products/cache/warm-up

# 更新商品:数据库提交成功后删除缓存
curl -X PUT http://localhost:8080/api/products/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"Redis 实战课程(新版)","description":"高并发缓存实战","price":109.00,"status":1}'

# 手工删除普通缓存与逻辑过期缓存
curl -X DELETE http://localhost:8080/api/products/1/cache

幂等订单

先获取 Token,再将响应中的 token 放入请求头。相同 Token 第二次提交会返回重复请求。

curl -X POST http://localhost:8080/api/idempotency/token

curl -X POST http://localhost:8080/api/orders \
  -H "Content-Type: application/json" \
  -H "X-Request-Token: 替换为上一步token" \
  -d '{"userId":1001,"items":[{"productId":1,"quantity":2}]}'

秒杀

初始化脚本中的活动 1 默认有效、商品库存 100。

curl -X POST http://localhost:8080/api/seckill/1 \
  -H "Content-Type: application/json" \
  -d '{"userId":2001,"quantity":1}'

7. Redis Key 设计

所有 Key 都由 RedisKeyConstants 生成,Controller 和业务代码不散落无规则字符串。

Key 类型 TTL/用途
practice:captcha:{phone} String 5 分钟验证码
practice:user:{userId} Hash 用户属性
practice:message:queue List FIFO 消息队列
practice:product:favorite:{productId} Set 商品收藏用户
practice:user:ranking ZSet 用户积分榜
practice:product:{productId} String(JSON) 商品缓存,30~35 分钟
practice:product:logical:{productId} String(JSON) 数据与逻辑过期时间
practice:lock:product:{productId} Redisson Lock 商品缓存重建锁
practice:idempotency:token:{token} String 10 分钟一次性 Token
practice:lock:seckill:{activityId}:{userId} Redisson Lock 用户级秒杀锁
practice:seckill:user:{activityId}:{userId} String 已秒杀标记

8. 缓存穿透解决方案

缓存穿透是对数据库中不存在的数据持续发起查询:Redis 永远未命中,请求每次都会落到数据库。项目在数据库确认商品不存在后写入字符串 NULL,TTL 为 2 分钟;后续请求命中该哨兵后直接返回商品不存在,且不会向 Redis 写 Java null

空值缓存的问题是短暂的数据不一致:在 2 分钟内新创建同 ID 商品仍可能被旧空值挡住;大量随机不存在 ID 也会占用内存。写入商品时应主动删除对应空值。更大规模场景可以在缓存之前增加布隆过滤器:它空间效率高且可快速拒绝一定不存在的 ID,但存在可控误判率,并需要处理元素初始化、更新和删除问题。

9. 缓存击穿解决方案

热点 Key 到期时,大量线程可能同时查询数据库,这就是缓存击穿。

  • 互斥锁版本:读取缓存失败后竞争 RLock,成功者双重检查并重建;失败者短暂休眠后重试。锁设置等待时间和租约时间,finally 中仅由持锁线程释放。
  • 逻辑过期版本:缓存保存 data + expireTime。未过期直接返回;已过期仍返回旧值,并向线程池提交重建任务。工作线程获取锁后再次检查过期时间,仅一个线程查询数据库。该方式以短暂旧数据换低延迟和高可用。

10. 缓存雪崩解决方案

商品缓存 TTL 为 30 分钟加 0~5 分钟随机值,预热时每个商品独立取随机值,避免同批 Key 集中失效。缓存读写异常时商品查询降级到 MySQL,并记录 productId 和异常日志。

生产环境还可叠加:网关或服务端限流保护入口;熔断器在数据库或 Redis 持续异常时快速失败;本地 Caffeine + Redis 形成多级缓存;Redis Cluster/哨兵提高可用性;预热任务分批执行并设置抖动;数据库连接池和只读副本作为最后容量边界。

11. 分布式锁实现原理

Redisson 的 RLock 以 Redis 原子命令和 Lua 保证加解锁安全,锁值关联客户端/线程身份,防止误删其他线程的锁。项目所有锁都:设置有限等待和租约、在 finally 中释放、释放前调用 isHeldByCurrentThread()

商品锁粒度为 productId;秒杀锁粒度为 activityId + userId,因此同一用户的重复请求串行化,不同用户仍能并行。Redis 锁不是库存安全的唯一依据,数据库条件更新始终保留。

12. 接口幂等实现原理

POST /api/idempotency/token 创建 UUID 并保存 10 分钟。下单时 lua/idempotency.lua 在 Redis 内原子执行 EXISTS + DEL:首次返回 1,后续返回 0,避免客户端侧先 GET 再 DELETE 的竞态。t_order.request_not_idempotency_record.request_no 都有唯一索引,即使 Redis 边界发生异常,数据库仍会拒绝重复记录。

一次性 Token 在数据库事务失败后已经被消费,这是本练习方案刻意暴露的取舍。生产环境可将请求状态机持久化,支持处理中、成功、失败可重试以及相同响应重放。

13. 缓存一致性方案

读使用 Cache Aside;更新不直接覆盖缓存。updateProduct 在 Spring 数据库事务内更新 MySQL,通过 TransactionSynchronizationManager 注册 afterCommit 回调,只有提交成功后才删除普通缓存和逻辑缓存。这样避免提交前删除缓存后事务回滚造成的数据不一致。删除失败会记录错误,生产环境可用消息表/事务消息/延迟双删进行最终补偿。

14. 防止库存超卖方案

秒杀流程为:校验活动 → 获取用户级锁 → 检查用户标记 → 进入独立事务 Service → 数据库条件扣库存 → 创建订单和明细 → 写扣减日志 → 提交事务 → 写 Redis 用户标记 → 释放锁。

核心 SQL 只有在 available_stock >= quantity 时更新成功,通过受影响行数判断库存是否充足:

UPDATE t_product_stock
SET available_stock = available_stock - ?,
    version = version + 1,
    update_time = NOW()
WHERE product_id = ?
  AND available_stock >= ?
  AND deleted = 0;

同一活动、同一用户还有数据库唯一索引 uk_activity_user。即使 Redis 标记写入失败或分布式锁发生边界问题,条件更新保证库存不会小于 0,唯一索引保证用户不会生成第二个秒杀订单。

15. 并发测试方法

全部测试无需启动 MySQL/Redis,使用可控 Mock 模拟依赖边界并验证核心并发不变量:

mvn clean test

# 单独运行热点缓存并发测试
mvn -Dtest=ProductLogicalExpireConcurrencyTest test

# 单独运行 100 线程秒杀测试
mvn -Dtest=SeckillConcurrencyTest test

# 单独运行幂等并发测试
mvn -Dtest=OrderIdempotencyConcurrencyTest test

当前测试覆盖 8 个用例:Spring 上下文、首次 MySQL/再次 Redis、空值缓存、Redis 降级、逻辑过期单次重建、同 Token 单订单、100 线程不超卖、同用户不重复秒杀。

连接真实容器做压测时,可用 JMeter、wrk 或 Apache Bench 为每个虚拟用户分配不同 userId,结束后检查:

SELECT available_stock FROM t_product_stock WHERE product_id = 1;
SELECT COUNT(*) FROM t_order WHERE activity_id = 1 AND deleted = 0;
SELECT activity_id, user_id, COUNT(*) AS cnt
FROM t_order WHERE activity_id = 1 AND deleted = 0
GROUP BY activity_id, user_id HAVING cnt > 1;

16. 每个练习完成后的验证方式

练习 验证方法 预期结果
String 写验证码后 TTL practice:captcha:13800138000 TTL 接近 300 秒
Hash HGETALL practice:user:1001 返回 name/email/age
List 连续入队再出队 先进先出
Set 同一用户重复收藏后查询人数 人数只增加一次
ZSet 多用户增加积分后查询 Top 10 按积分倒序
Cache Aside 连续查询商品两次 cacheHit 为 false、true
穿透 连续查询不存在 ID,观察日志和 Redis 只查一次 DB,缓存值为 NULL
雪崩 预热后批量执行 TTL practice:product:* TTL 分散在 1800~2100 秒
互斥击穿 删除商品 Key 后并发访问 /mutex 仅一个线程重建
逻辑过期 构造过期缓存后并发访问 立即返回旧值,仅一次异步 DB 查询
缓存一致性 更新商品后检查 Redis 事务提交后 Key 被删除
幂等 同 Token 连续创建两次订单 首次成功,第二次重复请求
秒杀锁 同一 userId 连续请求两次 只有一次成功
防超卖 运行 100 线程测试或真实压测 库存始终大于等于 0
Redis 降级 暂停 Redis 后查询已存在商品 从 MySQL 返回且记录降级日志

Redis CLI 辅助命令:

docker exec -it redis-practice-redis redis-cli
GET practice:product:1
TTL practice:product:1
KEYS practice:*

开发环境可以用 KEYS 观察练习结果;生产环境应使用渐进式 SCAN,避免阻塞 Redis 单线程事件循环。

redis_practice

About

基于 Spring Boot 3、MyBatis-Plus、MySQL、Redis 和 Redisson 的实战练习项目,涵盖缓存穿透、击穿、雪崩、分布式锁、Lua 幂等校验、缓存一致性及高并发秒杀防超卖。

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages