缓存与数据库打架了?六个实战技巧教你平息数据内战
作为程序员,你一定遇到过这样的场景:用户刚修改了昵称,页面刷新后却显示旧的名称;购物车明明已清空,重新登录后商品又回来了。这些"灵异现象"的背后,往往都是缓存与数据库在"闹别扭"。今天我们就来聊聊,当缓存数据与后端数据库不一致时,如何当好这个"和事佬"。
一、数据不一致的根源在哪里?
想象缓存和数据库就像两个爱较劲的朋友:
- 张三(缓存)总是抢着回答问题,但记性不太好
- 李四(数据库)做事严谨但反应慢
当用户提问时,张三总是抢先回答,但如果张三记错了或者李四偷偷改了答案,矛盾就产生了。
典型场景包括:
- 先更新缓存失败,数据库却成功了
- 并发写操作导致更新顺序错乱
- 缓存过期时间设置不合理
- 主从同步延迟期间读取到旧数据
二、六大和解方案(以Redis+MySQL技术栈为例)
方案1:延迟双删法(简单粗暴型)
// 技术栈:Spring Boot + Redis + MySQL
public void updateUser(User user) {
// 第一步:先删缓存
redisTemplate.delete("user:" + user.getId());
// 第二步:更新数据库
userMapper.update(user);
// 第三步:睡一会儿再删一次(建议500ms-1s)
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
redisTemplate.delete("user:" + user.getId());
}
注释说明
- 首次删除:防止后续读请求拿到旧缓存
- 延迟删除:应对在数据库更新期间可能有其他请求写入旧缓存
- 适合场景:对一致性要求较高的用户基础信息更新
缺点:硬编码延迟时间,可能影响接口响应速度
方案2:订阅Binlog(优雅监听型)
def process_binlog_event(event):
if event.table == 'user_table':
user_id = event.data['id']
redis_client.delete(f'user:{user_id}')
# 通过Canal客户端监听数据库变更
while True:
message = canal_client.get(100)
for entry in message.entries:
if entry.entryType == ENTRYTYPE_ROWDATA:
process_binlog_event(entry)
注释说明
- 利用MySQL的binlog机制实现准实时同步
- 数据库任何变更都会触发缓存删除
- 适合场景:金融交易记录等强一致性场景
缺点:架构复杂度高,需要维护消息队列
三、技术选型指南
方案 | 一致性强度 | 实现难度 | 适用场景 |
---|---|---|---|
延迟双删 | 较高 | ⭐⭐ | 中小型项目 |
订阅Binlog | 强 | ⭐⭐⭐⭐ | 分布式系统 |
设置短过期时间 | 弱 | ⭐ | 低频修改数据 |
互斥锁 | 强 | ⭐⭐⭐ | 秒杀类场景 |
四、经典应用场景
场景1:电商库存大战
当万人抢购时,采用Redis+Lua脚本保证原子性:
-- 技术栈:Redis Lua脚本
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock > 0 then
redis.call('DECR', KEYS[1])
return 1
end
return 0
配合定时任务同步数据库,既保证抢购体验又最终一致
场景2:社交动态更新
用户发朋友圈后,采用"先更库再删缓存"策略:
// 技术栈:Node.js + Redis
async function postMoment(userId, content) {
await db.query('INSERT INTO moments ...');
await redis.del(`recent_moments:${userId}`);
}
允许短时间缓存未更新,通过客户端本地缓存降级
五、避坑指南
- 雪崩预防:批量更新时添加随机过期时间
redisTemplate.expire(key, 60 + new Random().nextInt(30), TimeUnit.SECONDS);
脑裂处理:使用Redlock等分布式锁方案
兜底策略:缓存击穿时采用互斥锁重建
// 技术栈:Go + Redis
func getData(key string) string {
data := redis.Get(key)
if data == "" {
if redis.SetNX("lock:"+key, 1, 10*time.Second) {
defer redis.Del("lock:"+key)
// 查库重建缓存
} else {
time.Sleep(100 * time.Millisecond)
return getData(key)
}
}
return data
}
六、总结建议
缓存一致性就像走钢丝,要在性能与准确性之间保持平衡:
- 强一致性场景:优先考虑Binlog监听方案
- 最终一致性场景:延迟双删+重试机制更实用
- 高频读写数据:建议设置较短过期时间(30-60秒)
- 历史数据:直接设置较长过期时间(24小时+)
最后记住三个救命锦囊:
- 重要操作记录日志方便追溯
- 给缓存删除操作添加重试机制
- 在管理后台保留手动清除缓存入口
数据一致性不是一劳永逸的战役,而是需要持续优化的过程。选择适合业务场景的方案,比追求理论完美更重要。毕竟,我们的目标不是消灭所有不一致,而是让用户感知不到不一致。