电商缓存强一致方案:数据库锁保障
背景
在电商系统中,为提升商品详情页的访问速度,我们通常会使用 Redis 缓存商品信息。获取商品信息时,会先从 Redis 中查询,若未命中,则从数据库获取并写入 Redis 后返回。
但后台存在商品信息修改的操作,此时如何确保 Redis 缓存与数据库数据一致?即如何保证商品详情接口返回的数据与数据库中的数据完全一致?
方案 1:常规缓存更新策略及问题分析
商品表结构(t_goods)
字段 | 类型 | 说明 |
goodsId | int | 商品 id |
stock | int | 库存 |
核心逻辑
获取商品详情接口
step1:从Redis中查询商品信息,若存在则直接返回;若不存在,继续下一步
step2:从数据库中查询商品信息
step3:将查询到的商品信息写入Redis
step4:返回商品信息
后台更新商品逻辑
step1:更新商品信息到数据库
step2:删除Redis中对应的商品记录
预期目标
在并发情况下,数据库与 Redis 中的数据保持一致。例如,若数据库中商品库存为 10,那么商品详情接口返回的库存也应为 10。
并发场景验证
假设商品 1 的初始库存为 10,模拟 3 个线程同时操作:
- thread1:执行商品更新操作(将库存改为 0)
- thread2、thread3:调用商品详情接口
时间点 | thread1(更新商品) | thread2(获取商品信息) | thread3(获取商品信息) |
T1 | step1:Redis 中无商品信息 | ||
T2 | step2:从数据库查询到商品信息(goodsId:1,stock:10) | ||
T3 | step1:将数据库中商品 1 的库存更新为 0 | ||
T4 | step2:删除 Redis 中商品 1 的记录 | ||
T5 | step3:将库存 10 的商品信息写入 Redis | ||
T6 | step4:返回商品信息(stock:10) | ||
T7 | step1:从 Redis 中获取到库存 10 的商品信息 | ||
T8 | step2:返回商品信息(stock:10) |
结果分析
此时数据库中商品 1 的库存为 0,而 Redis 中为 10,数据不一致,该方案无法满足一致性要求。
方案 2:基于数据库锁的强一致性方案
问题根源
方案 1 中数据不一致的核心原因是:商品更新操作与商品查询操作并行执行时,查询线程可能在更新线程删除缓存后,仍将旧数据写入 Redis,导致缓存与数据库数据不符。
要解决此问题,需让更新操作与查询操作串行执行,确保查询操作能获取到最新的数据库数据。
优化方案
通过数据库的for update行锁,实现更新操作与查询操作的互斥,保证数据一致性。
后台更新商品逻辑(优化后)
step1:开启数据库事务
step2:更新商品信息到数据库
step3:执行加锁查询:select * from t_goods where goodsId = #{goodsId} for update;(锁定该商品记录)
step4:删除Redis中对应的商品记录
step5:提交数据库事务(释放锁)
获取商品详情接口(优化后)
step1:从Redis中查询商品信息,若存在则直接返回;若不存在,继续下一步
step2:开启数据库事务
step3:执行加锁查询:select * from t_goods where goodsId = #{goodsId} for update;(等待更新操作释放锁)
step4:将查询到的最新商品信息写入Redis
step5:提交事务(释放锁)
step6:返回商品信息
并发场景验证
同样模拟商品 1 库存从 10 更新为 0 的场景,3 个线程操作如下:
时间点 | thread1(更新商品) | thread2(获取商品信息) | thread3(获取商品信息) |
T1 | step1:Redis 中无商品信息 | ||
T2 | step1:开启事务,将数据库中商品 1 的库存更新为 0 | ||
T3 | step2:执行select ... for update,锁定商品 1 记录 | ||
T4 | step3:删除 Redis 中商品 1 的记录 | ||
T5 | step2:开启事务,执行select ... for update,等待锁释放 | ||
T6 | step4:提交事务,释放锁 | ||
T7 | step3:获取到数据库中最新商品信息(stock:0) | step1:Redis 中无商品信息 | |
T8 | step4:将库存 0 的商品信息写入 Redis | ||
T9 | step5:提交事务,释放锁 | step2:开启事务,执行select ... for update | |
T10 | step6:返回商品信息(stock:0) | step3:获取到数据库中库存 0 的商品信息 | |
T11 | step4:将库存 0 的商品信息写入 Redis | ||
T12 | step5:提交事务,释放锁 | ||
T13 | step6:返回商品信息(stock:0) |
结果分析
数据库与 Redis 中商品 1 的库存均为 0,数据一致,该方案实现了强一致性。
方案解析
核心原理
利用数据库的for update行锁,使商品更新操作与查询操作串行执行:
- 当更新操作执行select ... for update时,会锁定该商品记录,其他查询操作执行相同 SQL 时需等待锁释放。
- 待更新事务提交后,查询操作才能获取到最新数据并写入 Redis,确保缓存与数据库数据一致。
优势
- 强一致性:通过锁机制严格保证缓存与数据库数据一致。
- 实现简单:无需引入额外中间件,依托数据库自身锁机制即可实现。
- 适用性广:适用于对数据一致性要求高的场景,如商品库存、价格等核心信息。
注意事项
- 加锁会增加系统开销,可能降低并发性能,需根据业务场景权衡。
- 事务要尽可能短,减少锁持有时间,降低阻塞影响。
总结
在电商系统商品信息缓存场景中,若需保证 Redis 与数据库数据强一致,推荐采用方案 2:
- 更新商品时,通过for update锁定记录,确保更新期间查询操作等待。
- 查询商品时,同样通过for update获取最新数据,避免写入旧数据到缓存。
该方案将并发操作转换为顺序执行,从根源上解决了数据不一致问题,虽可能降低部分并发性能,但能保障核心数据的准确性,适合对一致性要求高的业务场景。在实际应用中,可根据业务对一致性和性能的要求,灵活选择合适的方案。