当前位置: 首页 > news >正文

【Redis】缓存读/写操作流程

Redis缓存读/写操作流程

1. Redis读操作和写操作

1.1 读操作

Redis读操作通常遵循以下流程:

应用程序 -> 检查缓存 -> 缓存命中 -> 返回数据|v缓存未命中 -> 查询数据库 -> 更新缓存 -> 返回数据

读操作的关键点:

  • 缓存命中时直接返回数据,性能高
  • 缓存未命中时需要访问数据库,性能较低
  • 需要考虑缓存击穿、穿透、雪崩等问题

1.2 写操作

Redis写操作有多种策略,主要包括:

1.2 写操作策略对比

策略流程优点缺点适用场景
先更新DB,再更新缓存DB更新 → 缓存更新数据一致性较好并发更新可能脏数据读多写少,一致性要求高
先更新DB,再删除缓存DB更新 → 缓存删除简单,避免并发更新问题可能短暂不一致推荐:通用场景
先删除缓存,再更新DB缓存删除 → DB更新避免脏读可能缓存击穿写多读少,强一致性

每种策略都有其适用场景和潜在问题。

2. Redis内存淘汰机制

2.1 淘汰淘汰策略分类在这里插入图片描述

Redis作为内存数据库,当内存使用达到上限时,需要根据淘汰策略来释放空间。Redis提供了多种内存淘汰策略:

2.1 淘汰策略分类

策略描述
noeviction默认策略,不淘汰数据,内存满时写操作返回错误
allkeys-lru从所有key中淘汰最近最少使用的key
allkeys-lfu从所有key中淘汰最不经常使用的key
allkeys-random从所有key中随机淘汰
volatile-lru从设置了过期时间的key中淘汰最近最少使用的key
volatile-lfu从设置了过期时间的key中淘汰最不经常使用的key
volatile-random从设置了过期时间的key中随机淘汰
volatile-ttl从设置了过期时间的key中淘汰剩余时间最短的key

2.2 策略选择建议

  • allkeys-lru:适用于大部分场景,通用性好
  • allkeys-lfu:适用于访问模式相对固定的场景
  • volatile-ttl:适用于明确知道key过期时间的场景
  • noeviction:适用于缓存数据量可控且不希望数据被淘汰的场景

3. 业务场景中缓存更新策略是什么?

这是缓存更新策略中的经典问题,需要根据业务场景和数据一致性要求来选择。

在这里插入图片描述

核心概念

  • 缓存:Redis 等内存数据库,用于快速读取数据。
  • 数据库:MySQL 等持久化存储,是数据的"真相源"。
  • 线程1 和 线程2:两个并发执行的请求线程。

3.1 推荐方案:先更新数据库,再删除缓存

在这里插入图片描述
步骤顺序如下:

  1. 线程1 查询缓存未命中 -> 查询数据库(获取到 v=10)
  2. 线程2 更新数据库(v = 20)
  3. 线程2 删除缓存
  4. 线程1 写入缓存(把 v=10 写回缓存)

分析:

这种情况确实会发生,但概率较低。
因为通常步骤 1(读数据库)和步骤 4(写缓存)之间的时间间隔很短,在线程2更新数据库并删缓存的操作(步骤2、3)正好发生在它们之间的概率不高。

而且这种不一致会在下次更新或缓存过期时修复。

3.2 不推荐方案:先删除缓存,再更新数据库

在这里插入图片描述

步骤顺序如下:

线程1 删除缓存

线程2 查询缓存未命中 → 查数据库(旧值 20)

线程2 写入缓存(旧值 20)

线程1 更新数据库(新值 30)

结果:缓存中是旧值 20,数据库是新值 30,出现不一致。

高级优化建议(生产环境常用):

  1. 双删策略

    • 先删缓存
    • 再更新数据库
    • 延迟一段时间后再次删除缓存(防止其他线程在这期间写入旧值)
  2. 使用消息队列异步更新缓存

    • 数据库更新后发消息给 Redis 更新服务,保证最终一致性
  3. 设置缓存过期时间

    • 即使偶尔出现不一致,也能通过 TTL 自动恢复

总结一句话:

不要先改数据库再删缓存,否则可能让缓存写入旧数据,造成"脏读"。正确的做法是:先删缓存,再改数据库。

这正是图中右边被标记为"胜出"的原因 —— 它是错误的,应该被淘汰!

4. 高一致性要求场景的解决方案

对于对数据一致性要求极高的场景,可以考虑以下方案:

41 延迟双删

public void updateProductWithDelayDelete(Product product) {// 1. 删除缓存String cacheKey = "product:" + product.getId();redisCache.deleteProduct(cacheKey);// 2. 更新数据库productService.updateProductInDatabase(product);// 3. 延迟删除缓存(防止其他请求将旧数据写入缓存)Thread.sleep(100); // 短暂延迟redisCache.deleteProduct(cacheKey);
}

4.2 异步更新缓存

public void updateProductWithAsyncCache(Product product) {// 1. 更新数据库productService.updateProductInDatabase(product);// 2. 删除缓存String cacheKey = "product:" + product.getId();redisCache.deleteProduct(cacheKey);// 3. 异步更新缓存executorService.submit(() -> {// 延迟一段时间后更新缓存,确保数据库事务已提交Thread.sleep(1000);Product updatedProduct = productService.getProductFromDatabase(product.getId());redisCache.setProduct(cacheKey, updatedProduct, 300); // 5分钟过期});
}

5. 实际应用思考

5.1 选择合适的缓存更新策略

  1. 读多写少:可以容忍短暂不一致,使用"先更新数据库,再删除缓存"
  2. 强一致性要求:使用延迟双删或异步更新缓存
  3. 写多读少:考虑是否真的需要缓存,或者使用较短的过期时间

5.2 监控和报警

  1. 缓存命中率监控:确保缓存有效
  2. 数据库查询次数监控:避免缓存失效导致数据库压力增大
  3. 缓存更新失败监控:及时发现和处理异常情况

5.3 缓存设计原则

  1. 合理设置过期时间:根据业务特点设置合适的过期时间
  2. 缓存预热:系统启动时预加载热点数据
  3. 缓存穿透防护:对空值也进行缓存
  4. 缓存雪崩防护:设置随机过期时间
  5. 缓存击穿防护:使用互斥锁或逻辑过期

6. 线程安全问题

在高并发场景下,缓存操作的线程安全是一个重要考虑因素。当多个线程同时访问缓存时,可能会出现以下线程安全问题:

5.1 缓存击穿与线程安全

缓存击穿是指热点数据在缓存中过期时,大量请求同时访问数据库的情况。这不仅会造成数据库压力,还可能导致线程安全问题。

1) 使用同步锁解决

最简单的解决方案是使用synchronized关键字:

public Product getProductWithSynchronized(Long productId) {String cacheKey = "product:" + productId;// 1. 先从缓存中获取Product product = redisCache.getProduct(cacheKey);if (product != null) {return product;}// 2. 缓存中没有,需要从数据库获取,使用同步锁保证只有一个线程去查询数据库synchronized (this) {// 双重检查,可能其他线程已经查询并放入缓存product = redisCache.getProduct(cacheKey);if (product != null) {return product;}// 从数据库获取product = productService.getProductFromDatabase(productId);// 如果数据库中有数据,放入缓存if (product != null) {redisCache.setProduct(cacheKey, product, 5); // 设置5秒过期}}return product;
}

这种方法在单机环境下有效,但在分布式环境下无法跨节点生效。

2)使用分布式锁解决

在分布式系统中,需要使用分布式锁来保证线程安全:

public Product getProductWithDistributedLock(Long productId) {String cacheKey = "product:" + productId;String lockKey = "lock:product:" + productId;// 1. 先从缓存中获取Product product = redisCache.getProduct(cacheKey);if (product != null) {return product;}// 2. 获取分布式锁Jedis jedis = jedisPool.getResource();try {// 尝试获取锁,超时时间10秒,过期时间30秒String lockValue = UUID.randomUUID().toString();boolean lockAcquired = jedis.setnx(lockKey, lockValue) == 1;if (lockAcquired) {jedis.expire(lockKey, 30);try {// 双重检查product = redisCache.getProduct(cacheKey);if (product != null) {return product;}// 从数据库获取product = productService.getProductFromDatabase(productId);// 如果数据库中有数据,放入缓存if (product != null) {redisCache.setProduct(cacheKey, product, 5);}} finally {// 释放锁String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +"return redis.call('del', KEYS[1]) else return 0 end";jedis.eval(script, 1, lockKey, lockValue);}} else {// 获取锁失败,短暂等待后重试Thread.sleep(100);return getProductWithDistributedLock(productId); // 递归重试}} finally {jedis.close();}return product;
}

5.2 缓存更新的线程安全

在缓存更新时,也需要考虑线程安全问题,特别是在高并发写操作场景下。

逻辑过期避免并发更新

逻辑过期是一种有效的线程安全方案,它避免了物理过期时的并发问题:

public class LogicalExpireWrapper {private Product product;private long expireTime; // 逻辑过期时间戳// 构造函数、getter、setter省略
}public Product getProductWithLogicalExpire(Long productId) {String cacheKey = "product_logical:" + productId;try (Jedis jedis = jedisPool.getResource()) {String cachedValue = jedis.get(cacheKey);if (cachedValue != null) {// 解析缓存值和逻辑过期时间LogicalExpireWrapper wrapper = objectMapper.readValue(cachedValue, LogicalExpireWrapper.class);// 检查是否逻辑过期if (System.currentTimeMillis() < wrapper.getExpireTime()) {return wrapper.getProduct();}}// 缓存不存在或已逻辑过期,需要查询数据库// 获取该商品的锁ReentrantLock lock = lockMap.computeIfAbsent(productId, k -> new ReentrantLock());lock.lock();try {// 双重检查String cachedValueAgain = jedis.get(cacheKey);if (cachedValueAgain != null) {LogicalExpireWrapper wrapper = objectMapper.readValue(cachedValueAgain, LogicalExpireWrapper.class);if (System.currentTimeMillis() < wrapper.getExpireTime()) {return wrapper.getProduct();}}// 查询数据库Product product = productService.getProductFromDatabase(productId);if (product != null) {// 设置逻辑过期时间为5秒后LogicalExpireWrapper wrapper = new LogicalExpireWrapper(product, System.currentTimeMillis() + 5000);String wrapperJson = objectMapper.writeValueAsString(wrapper);jedis.set(cacheKey, wrapperJson);}return product;} finally {lock.unlock();}} catch (Exception e) {e.printStackTrace();return null;}
}

6. 总结

Redis缓存更新策略的选择需要综合考虑业务场景、数据一致性要求、系统复杂度等因素。

对于大多数业务场景,采用「先更新数据库,再删除缓存」策略,配合合适的过期时间和监控告警,即可满足性能和数据一致性要求。对于特殊的高并发、强一致性场景,再考虑使用更复杂的方案。

http://www.dtcms.com/a/485879.html

相关文章:

  • 网站开发的网页模板移动网站建设价格便宜
  • Grafana 数据展示全流程指南:从安装到高级可视化实践
  • 电脑没法ping通某个网段的ip
  • ​rxnfp 仓库介绍(https://rxn4chemistry.github.io/rxnfp)​
  • iOS 26 文件管理实战,多工具组合下的 App 数据访问与系统日志调试方案
  • 工信部网站找回不了密码网站用什么做
  • MaxScale:MySQL读写分离实战指南
  • 基于Vue的体检中心管理系统的开发bk1825s9(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
  • RabbitMQ的概述
  • 使用PyTorch实现图像分类任务的全流程详解
  • JAVA代泊车接机送机服务代客泊车系统源码支持小程序+APP+H5
  • 吃谷机主题商城小程序的界面功能设计
  • 创建网站超市网络免费推广平台
  • 【征文计划】码上分享:基于 Rokid CXR-M SDK 构建「AI远程协作助手」实战全记录
  • PortSwigger靶场之CSRF where token is tied to non-session cookie通关秘籍
  • laya报错:GET http://xxx/bin/%22%22 404(Not Found)
  • 兴义市住房和城乡建设局网站莲花网站
  • 标题:Linux 系统中的“保险库管理员”:深入浅出理解 /etc/shadow 文件
  • CSS3》》 transform、transition、translate、animation 区别
  • HTML实现流星雨
  • JavaWeb-html、css-网页正文制作
  • GaussDB 分布式下, 报错concurrent update under Stream mode is not yet support
  • 服务器连接百度网盘并下载文件
  • 云计算实验3——CentOS中storm的安装
  • 一次被“动画关闭”启发的思考:Animate.css 与 prefers-reduced-motion 的无障碍设计
  • 《突破同质化:太空殖民地NPC行为差异化的底层架构》
  • 做网站ppt常见c2c网站有哪些
  • 专业手机网站建设价格明细表wordpress xiu 5.6
  • CSS 组合选择符详解
  • css:`target-before and :target-after 和 scroll-target-group`