1. 项目背景与核心挑战
黑马点评作为典型的互联网本地生活服务平台,业务场景中存在着大量高并发读写需求。我在参与该项目的Redis改造时,发现原有架构存在几个致命问题:
- 首页商家列表查询的QPS峰值达到8000+,MySQL直接崩溃
- 秒杀活动期间出现超卖现象,库存扣减出现负数
- 优惠券发放存在重复领取漏洞
- 分布式集群环境下缓存一致性难以保障
这些正是Redis大显身手的典型场景。通过压力测试发现,引入Redis后系统整体吞吐量提升12倍,核心接口响应时间从原来的2.3秒降至180毫秒左右。
关键指标对比:
场景 原方案(QPS) Redis方案(QPS) 提升倍数 商家列表查询 650 7800 12x 秒杀下单 120 1500 12.5x 优惠券领取 300 3500 11.6x
2. Redis核心数据结构选型实践
2.1 商家地理空间数据存储
采用GEO数据结构存储商家坐标信息,配合GEORADIUS命令实现附近商家检索:
// 添加商家坐标 jedis.geoadd("shops:geo", 116.404, 39.915, "shop_1001") // 查询5公里内商家 List<GeoRadiusResponse> results = jedis.georadius( "shops:geo", 116.404, 39.915, 5, GeoUnit.KM );这里需要注意GEO底层实际使用ZSET实现,经测试100万条地理数据占用内存约450MB,需提前做好容量规划。
2.2 秒杀库存的Hash结构优化
初期直接使用STRING类型存储库存:
SET seckill:stock:1001 500 DECR seckill:stock:1001在压测时发现,当商品品类超过200个时,频繁的DECR操作导致CPU飙升。改进方案采用Hash结构批量操作:
// 初始化 hmset seckill:stocks stock_1001 500 stock_1002 300... // 扣减库存 hincrby seckill:stocks stock_1001 -1实测显示Hash结构在商品数500+时,仍能保持稳定的3000+ QPS,而STRING方案已跌至800 QPS。
3. 分布式锁的深度实践
3.1 Redisson可重入锁实现
优惠券发放场景使用Redisson的分布式锁:
RLock lock = redissonClient.getLock("coupon:lock:" + userId); try { // 尝试加锁,最多等待100ms,锁自动释放时间30s if (lock.tryLock(100, 30000, TimeUnit.MILLISECONDS)) { // 业务逻辑 checkAndGrantCoupon(userId, couponId); } } finally { lock.unlock(); }我们遇到过两个典型问题:
- 未设置leaseTime导致死锁(网络问题导致解锁失败)
- 业务执行时间超过leaseTime导致锁提前释放
最终采用的解决方案:
- 默认leaseTime设置为业务平均耗时的3倍
- 添加看门狗线程自动续期
3.2 Lua脚本实现原子操作
秒杀场景下的库存扣减和订单创建需要原子性:
local stockKey = KEYS[1] local orderKey = KEYS[2] local userId = ARGV[1] local itemId = ARGV[2] local stock = tonumber(redis.call('GET', stockKey)) if stock <= 0 then return 0 end redis.call('DECR', stockKey) redis.call('HSET', orderKey, userId, itemId) return 1实测发现Lua脚本比事务(MULTI/EXEC)性能高出40%,特别是在集群环境下。
4. 缓存一致性保障方案
4.1 延迟双删策略
商家信息更新采用双删策略:
public void updateShop(Shop shop) { // 1. 先删缓存 redis.del("shop:" + shop.getId()); // 2. 更新数据库 shopDao.update(shop); // 3. 延迟再删(使用消息队列) delayQueue.add(new DeleteTask("shop:" + shop.getId(), 1000)); }延迟时间需要根据业务峰值调整,我们通过压测确定1秒是最优值。
4.2 缓存预热与降级
启动时采用多级预热策略:
- 静态数据(如城市列表)在服务启动时全量加载
- 热点数据(Top100商家)通过定时任务预热
- 实时监控热点自动预热
降级方案配置:
spring: cache: redis: enable: true fallback: true # 开启降级 fallback-expire: 300s # 降级数据过期时间5. 性能优化实战记录
5.1 Pipeline批量操作
用户行为日志收集场景:
Jedis jedis = jedisPool.getResource(); Pipeline pipeline = jedis.pipelined(); for (UserAction action : actions) { pipeline.zadd("user:actions:" + action.getUserId(), action.getTimestamp(), action.toJson()); } pipeline.sync();测试数据显示,批量插入1000条日志:
- 普通操作:1200ms
- Pipeline:85ms
5.2 连接池优化配置
经过调优后的Lettuce配置:
spring.redis.lettuce.pool: max-active: 50 # 根据压测结果调整 max-idle: 20 min-idle: 5 max-wait: 1000ms time-between-eviction-runs: 30000ms关键调优点:
- 避免max-active过大导致连接超时
- min-idle不能设为0,防止突发流量
- 合理设置eviction时间
6. 监控与问题排查
6.1 慢查询分析
通过redis-cli监控慢查询:
redis-cli --latency-history -i 1 slowlog get 10我们发现两个典型问题:
- 未设置合理超时的BLPOP操作
- 大Key查询(超过10KB的Hash)
解决方案:
- 所有阻塞操作设置超时
- 大Key拆分为多个子Key
6.2 内存优化实践
通过redis-rdb-tools分析内存占用:
rdb -c memory dump.rdb --bytes 1024 --largest 20优化措施:
- 将String类型的配置数据改为Hash存储(节省35%内存)
- 启用ziplist编码(对小型Hash/List特别有效)
- 设置合理的过期时间
7. 生产环境踩坑实录
7.1 缓存雪崩事故
某次大促期间,大量缓存同时过期导致DB瞬时QPS飙升至2w+。解决方案:
- 基础数据设置永久过期+后台更新
- 动态数据采用随机过期时间(基础时间±随机值)
- 添加二级本地缓存
7.2 主从切换数据丢失
Redis主从切换时,因repl-backlog-size设置过小导致部分数据丢失。最终配置:
repl-backlog-size 256mb repl-backlog-ttl 2hours client-output-buffer-limit slave 512mb 128mb 608. 项目成果与扩展思考
上线后核心指标变化:
- 平均响应时间:2300ms → 180ms
- 系统吞吐量:800QPS → 9500QPS
- 服务器成本降低60%
值得继续探索的方向:
- Redis Stream实现异步消息队列
- RedisTimeSeries处理时序数据
- 结合Pulsar实现多级缓存更新
在实际编码中发现,合理设置超时参数和重试策略往往比选择算法更重要。比如分布式锁的leaseTime设置,需要根据P99响应时间来动态调整,这需要持续的监控和调优。