HoRain云--高并发场景下,到底先更新缓存还是先更新数据库?

  

🎬 HoRain云小助手个人主页

 🔥 个人专栏: 《Linux 系列教程》《c语言教程

⛺️生活的理想,就是为了理想的生活!


⛳️ 推荐

前些天发现了一个超棒的服务器购买网站,性价比超高,大内存超划算!忍不住分享一下给大家。点击跳转到网站。

专栏介绍

专栏名称

专栏介绍

《C语言》

本专栏主要撰写C干货内容和编程技巧,让大家从底层了解C,把更多的知识由抽象到简单通俗易懂。

《网络协议》

本专栏主要是注重从底层来给大家一步步剖析网络协议的奥秘,一起解密网络协议在运行中协议的基本运行机制!

《docker容器精解篇》

全面深入解析 docker 容器,从基础到进阶,涵盖原理、操作、实践案例,助您精通 docker。

《linux系列》

本专栏主要撰写Linux干货内容,从基础到进阶,知识由抽象到简单通俗易懂,帮你从新手小白到扫地僧。

《python 系列》

本专栏着重撰写Python相关的干货内容与编程技巧,助力大家从底层去认识Python,将更多复杂的知识由抽象转化为简单易懂的内容。

《试题库》

本专栏主要是发布一些考试和练习题库(涵盖软考、HCIE、HRCE、CCNA等)

目录

⛳️ 推荐

专栏介绍

核心矛盾与权衡

典型场景与解决方案

1. 读多写少场景(如商品详情页)

2. 写多读多场景(如秒杀库存)

3. 强一致性场景(如金融交易)

高级优化技术

1. 版本号防ABA问题

2. 读写分离 + 数据同步

3. 熔断降级策略

决策树:如何选择更新顺序?

性能压测数据参考

总结建议


img


核心矛盾与权衡

策略优点缺点适用场景
先更新数据库保证数据持久化优先可能短暂读取到旧缓存强一致性要求高,写后读少
先更新缓存读性能更高可能丢失数据(缓存成功但数据库失败)读多写少,允许短暂不一致

典型场景与解决方案

1. 读多写少场景(如商品详情页)
  • 策略Cache-Aside(旁路缓存) + 延迟双删
    // 伪代码示例
    public void updateProduct(Product product) {
        // 1. 先删除缓存(防止旧数据被后续读取)
        cache.delete(product.getId());
        
        // 2. 更新数据库
        db.update(product);
        
        // 3. 延迟二次删除(应对步骤1和2之间可能出现的脏读)
        threadPool.schedule(() -> {
            cache.delete(product.getId());
        }, 500, TimeUnit.MILLISECONDS);
    }
    
  • 关键点
    • 延迟时间需根据业务读写耗时调整(通常 100ms~1s)
    • 通过消息队列(如Kafka)保证二次删除的可靠性
2. 写多读多场景(如秒杀库存)
  • 策略Write-Behind(写缓冲) + 合并更新
    // 使用本地缓存合并短时间内的多次写操作
    ConcurrentHashMap<Long, AtomicInteger> buffer = new ConcurrentHashMap<>();
    
    public void deductStock(Long itemId, int count) {
        buffer.compute(itemId, (k, v) -> {
            if (v == null) v = new AtomicInteger(0);
            v.addAndGet(count);
            return v;
        });
        
        // 异步批量更新数据库
        asyncFlushBuffer();
    }
    
    private void asyncFlushBuffer() {
        buffer.forEach((itemId, delta) -> {
            // 使用CAS操作保证原子性
            db.execute("UPDATE stock SET count = count - ? WHERE item_id = ?", delta.get(), itemId);
            cache.decr(itemId, delta.get());
        });
        buffer.clear();
    }
    
  • 关键点
    • 本地缓冲层减少数据库压力
    • 最终一致性需结合版本号或时间戳
3. 强一致性场景(如金融交易)
  • 策略2PC(两阶段提交) + 分布式锁
    public boolean transfer(TransferRequest request) {
        // 1. 获取分布式锁(如Redis RedLock)
        Lock lock = redisson.getLock("lock:transfer:" + request.getId());
        if (!lock.tryLock(3, TimeUnit.SECONDS)) throw new RetryException();
        
        try {
            // 2. 预提交阶段:检查余额并预扣款
            Account from = db.selectForUpdate(request.getFromAccount());
            if (from.getBalance() < request.getAmount()) return false;
            
            // 3. 同时更新缓存和数据库(原子操作)
            cache.decr("balance:" + from.getId(), request.getAmount());
            db.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", 
                       request.getAmount(), from.getId());
            
            // 4. 提交后释放锁
            return true;
        } finally {
            lock.unlock();
        }
    }
    
  • 关键点
    • 牺牲部分性能换取强一致性
    • 需配合数据库行锁(SELECT ... FOR UPDATE

高级优化技术

1. 版本号防ABA问题
-- 数据库表设计增加版本字段
ALTER TABLE products ADD version INT DEFAULT 0;
// 更新时校验版本
public boolean updateWithVersion(Product product) {
    int rows = db.update(
        "UPDATE products SET name = ?, price = ?, version = version + 1 " +
        "WHERE id = ? AND version = ?", 
        product.getName(), product.getPrice(), product.getId(), product.getVersion());
    if (rows > 0) {
        cache.set(product.getId(), product.withVersion(product.getVersion() + 1));
        return true;
    }
    return false;
}
2. 读写分离 + 数据同步
  • 架构设计
    客户端 → 代理层(ShardingSphere) → 主库(写) / 从库(读)
                                ↓
                            缓存集群(Redis)
    
  • 同步工具
    • MySQL Binlog → Canal → Kafka → 缓存更新服务
    • 确保数据库变更实时反映到缓存
3. 熔断降级策略
# Hystrix配置示例
hystrix:
  command:
    default:
      circuitBreaker:
        requestVolumeThreshold: 20
        errorThresholdPercentage: 50
        sleepWindowInMilliseconds: 5000
      fallback:
        enabled: true
  • 降级逻辑
    • 缓存不可用时直接读数据库(牺牲性能保可用)
    • 数据库压力大时返回静态默认值

决策树:如何选择更新顺序?

  1. 是否需要强一致性?

    • → 2PC + 分布式锁(牺牲性能)
    • → 进入下一步
  2. 写后是否立即高频读取?

    • → 先更新缓存(减少缓存击穿)
    • → 进入下一步
  3. 是否有批量写场景?

    • → Write-Behind + 合并更新
    • → 延迟双删策略

性能压测数据参考

策略QPS(万级)平均延迟(ms)数据不一致时间窗口(ms)
先更新数据库1.2~1.58~1250~200
先更新缓存2.5~3.03~50(但可能永久不一致)
延迟双删1.8~2.210~1510~50
2PC强一致性0.3~0.520~500

总结建议

  • 常规场景:优先采用 Cache-Aside + 延迟双删,通过消息队列保证最终一致性
  • 秒杀类场景:使用 本地写缓冲 + 异步批量更新,结合缓存版本号
  • 金融交易场景:必须使用 2PC + 分布式锁,确保强一致性
  • 兜底方案:所有缓存设置过期时间(如5~30秒),防止长期不一致

通过合理的策略组合,可以在一致性、可用性和性能之间找到最佳平衡点。实际生产中建议根据业务特点进行A/B测试,使用Arthas等工具监控缓存命中率和数据库负载。

❤️❤️❤️本人水平有限,如有纰漏,欢迎各位大佬评论批评指正!😄😄😄

💘💘💘如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!👍 👍 👍

🔥🔥🔥Stay Hungry Stay Foolish 道阻且长,行则将至,让我们一起加油吧!🌙🌙🌙

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值