电商缓存强一致方案:数据库锁保障

背景

在电商系统中,为提升商品详情页的访问速度,我们通常会使用 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:

  1. 更新商品时,通过for update锁定记录,确保更新期间查询操作等待。
  1. 查询商品时,同样通过for update获取最新数据,避免写入旧数据到缓存。

该方案将并发操作转换为顺序执行,从根源上解决了数据不一致问题,虽可能降低部分并发性能,但能保障核心数据的准确性,适合对一致性要求高的业务场景。在实际应用中,可根据业务对一致性和性能的要求,灵活选择合适的方案。