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

Redis-缓存-击穿-分布式锁

Redis-缓存-击穿-分布式锁

  • 一、来因宫
    • 1.1、形成原因
    • 1.2、问题分析
  • 二、技术方案
    • 2.1、重点分析锁
    • 2.2、技术实现
  • 三、总结

一、来因宫

在这里插入图片描述

1.1、形成原因

redis缓存流程图

一个或多个高频热点key在redis中失效或者被删除,导致大量的请求瞬间穿透缓存,直接查询数据库,造成数据库压力剧增或者宕机

举个栗子🌰:

25年1月12日9点整,发布一条某某国企招考信息
大量的备考人员开始涌进app内查看招考信息,
由于我们的程序设置了缓存机制,发布的文章同步redis中,默认48小时,可以支撑大量用户的访问;
但是招考信息从发布到开始报名期间,假设5天的时间,就是120小时。
就会出现招考信息48小时在redis过期的时间点,存在大量的用户访问请求上来
redis查询不到,大量的请求直接去查询数据库,增大压力、宕机的可能
**这瞬间的访问直接透过Redis到了数据库**

1.2、问题分析

  • 缓存时间太短了
  • key值失效后没能及时同步
  • 即使过期,也不能批量数据查询数据库

1、缓存时间短,加时间,加多少?怎么加?全部加?部分加?
2、过期了,进行同步,怎么同步?周期性同步?同步哪些?
3、不让大批量数据访问,那就加锁🔒,在哪加?怎么加?

1、加时间可行,但是需要思考加多长时间,根据实际业务场景,明年招考之时总会有考生过来看看去年的公告,对比一下;另外一种:管理端发布文章同步设置缓存时间,程序自动获取设置时长,同时设置下架时间,考试结束后,自动将当前招考简章下架,不对外进行展示方法可行,但是不推荐,站在产品角度,用户体验感不好
===================================================================================
2、缓存失效立即同步,可以实现,站在业务角度上,没必要,定时更新,有些key可能是过期后不在使用的,浪费内存,程序管理不科学
===================================================================================
3、加锁可行大量请求进来,按照顺序第一个进来的拿到锁,去执行逻辑;后面的请求校验锁状态不对,不会往下执行**对锁没概念的同学,简单理解成执行前,判断status值为1,当第一个进来,将status设置成0,后面的再进来的请求拿到的都是0,只有等第一个释放,status改成1,后面的请求在进行抢夺执行权**		

二、技术方案

2.1、重点分析锁

关于锁的内容也比较重要繁琐,先放个 空链接 内容还未整理,后续再挂上,这里就只简单的说明,增加一下大家的印象

分布式锁,锁,缓存锁

上面的图示,展示了利用锁控制只能一个获取到锁的请求进行数据库查询以及重新载入数据,
有效的缓解数据库压力,但是会造成数据拥堵,延长响应时长,性能方面相对较差

这种方式减轻了DB的压力,具体使用的锁类型,可以通过锁相关信息进行了解

分布式锁、锁、java、缓存

这种情况说明一下:存储缓存的时候,并没有设置失效时间,在参数中添加了时间戳字段线程1获取到数据后,去计算时间戳过期了,然后获取锁去新线程更新缓存内容,同时将老数据返回线程3进来发现缓存过期,但是锁被线程1拿走了,线程3就直接将老数据返回例如:这时候还有新的线程4,此时的线程1已经更新完数据,那么线程4直接将获取到的最新数据返回

这个好处就是节约时间了,提升性能了,但是会导致数据不准确,返回的是老数据,不确定数据是否有更新

2.2、技术实现

第一种:获取锁,其他线程等待更新

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import redis.clients.jedis.Jedis;public class RedisCacheWithMutex {// TODO 这里只是做演示,具体根据业务封装使用private final Jedis jedis = new Jedis("localhost", 6379);// 为每个热点key创建独立的锁(避免锁竞争影响其他key)// 基于 ReentrantLock 实现的互斥锁实例// ReentrantLock 是 Lock 接口的最常用实现private final Lock lock = new ReentrantLock();// 获取数据(缓存+数据库)public String getData(String key) {// 1. 先查缓存String value = jedis.get(key);if (value != null) {return value; // 缓存命中,直接返回}// 2. 缓存未命中,尝试获取锁重建缓存try {// 尝试获取锁(可设置超时时间,避免死等)if (lock.tryLock()) {// 3. 再次查缓存(防止锁释放后其他线程已更新缓存)value = jedis.get(key);if (value == null) {// 4. 从数据库查询数据value = queryFromDB(key);// 5. 更新缓存(设置合理的过期时间)jedis.setex(key, 3600, value);}return value;} else {// 未获取到锁,等待片刻后重试(避免频繁尝试)Thread.sleep(50);return getData(key); // 递归重试}} catch (InterruptedException e) {Thread.currentThread().interrupt();return null;} finally {// 释放锁(只有持有锁的线程才需要释放)if (lock.isHeldByCurrentThread()) {lock.unlock();}}}// 模拟从数据库查询数据private String queryFromDB(String key) {System.out.println("从数据库查询:" + key);return "data_" + key; // 实际场景中是数据库查询结果}
}

第二种:不设置过期时间,但是参数中添加时间戳

import com.alibaba.fastjson.JSON;
import redis.clients.jedis.Jedis;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;// 缓存数据包装类,包含实际数据和时间戳
class CacheData<T> {private T data;          // 实际数据private long timestamp;  // 数据更新时间戳(毫秒)public CacheData(T data, long timestamp) {this.data = data;this.timestamp = timestamp;}// getter和setterpublic T getData() { return data; }public long getTimestamp() { return timestamp; }
}public class RedisLogicalExpireCache {private final Jedis jedis = new Jedis("localhost", 6379);private final Lock lock = new ReentrantLock();// 数据过期阈值(例如5分钟),超过此时长则需要更新private static final long EXPIRE_THRESHOLD = 5 * 60 * 1000;/*** 获取数据* @param key 缓存key* @return 数据*/public <T> T getData(String key, Class<T> clazz) {// 1. 从Redis获取缓存String json = jedis.get(key);// 2. 缓存不存在,直接查库并初始化缓存if (json == null) {return loadDataAndInitCache(key, clazz);}// 3. 解析缓存数据CacheData<T> cacheData = JSON.parseObject(json, new com.alibaba.fastjson.TypeReference<CacheData<T>>(){});// 4. 判断是否需要更新(当前时间 - 数据时间戳 > 阈值)if (System.currentTimeMillis() - cacheData.getTimestamp() > EXPIRE_THRESHOLD) {// 5. 尝试获取锁,只有一个线程去更新数据if (lock.tryLock()) {// 启动异步线程更新数据,避免阻塞当前请求new Thread(() -> {try {updateCache(key, clazz);} finally {lock.unlock(); // 确保锁释放}}).start();}// 无论是否获取到锁,都先返回旧数据}// 6. 返回缓存数据(旧数据或刚初始化的数据)return cacheData.getData();}/*** 初始化缓存(首次加载数据时使用)*/private <T> T loadDataAndInitCache(String key, Class<T> clazz) {try {if (lock.tryLock()) {// 双重检查,避免多线程同时初始化String json = jedis.get(key);if (json != null) {return JSON.parseObject(json, new com.alibaba.fastjson.TypeReference<CacheData<T>>(){}).getData();}// 查库获取数据T data = queryFromDB(key, clazz);// 存入缓存,不设置过期时间jedis.set(key, JSON.toJSONString(new CacheData<>(data, System.currentTimeMillis())));return data;} else {// 未获取到锁,等待片刻后重试Thread.sleep(50);return getData(key, clazz);}} catch (InterruptedException e) {Thread.currentThread().interrupt();return null;} finally {if (lock.isHeldByCurrentThread()) {lock.unlock();}}}/*** 更新缓存数据*/private <T> void updateCache(String key, Class<T> clazz) {// 1. 查库获取最新数据T newData = queryFromDB(key, clazz);// 2. 更新缓存(使用当前时间戳)jedis.set(key, JSON.toJSONString(new CacheData<>(newData, System.currentTimeMillis())));}/*** 模拟从数据库查询数据*/private <T> T queryFromDB(String key, Class<T> clazz) {System.out.println("从数据库查询数据: " + key);// 实际场景中这里是数据库查询逻辑try {// 模拟数据库查询耗时Thread.sleep(100);} catch (InterruptedException e) {Thread.currentThread().interrupt();}return JSON.parseObject("{\"id\":\"" + key + "\",\"name\":\"数据" + key + "\"}", clazz);}
}

三、总结

这是实际业务以及大家分享经验得到的解决方案,有一些感悟和大家说一下
1、实际业务产生问题,先结合现有的资源以及技术栈进行分析问题进行解决
2、网上提供的解决方案同样也是经过验证的,大家也可以放心使用
3、如果遇到的比较偏僻或者不是大众的问题,那么就集思广益,各抒己见,各种尝试,当然项目不会停下来等着我们,紧急刺手问题先解决,能让项目正常的进行运转,利用后续的时间进行优化总结经验
4、学习这些问题以及解决方案,是为了让大家在开发过程中就能想到设计方案,而不是出现问题再来解决,大众化的问题,当做变成的习惯,解决开发时间,养成良好习惯

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

相关文章:

  • 使用ros2跑mid360的fastlio2算法详细教程
  • 【数据结构】用堆解决TOPK问题
  • 算法训练营day56 图论⑥ 108. 109.冗余连接系列
  • C++---为什么迭代器常用auto类型?
  • 强、软、弱、虚引用
  • 在 Qt C++ 中利用 OpenCV 实现视频处理技术详解
  • 尝试Claude Code的安装
  • 学习笔记分享——基于STM32的平衡车项目
  • Mac调试ios的safari浏览器打开的页面
  • 电子电气架构 --- 软件项目成本估算
  • 技术攻坚全链铸盾 锁定12月济南第26届食品农产品安全高峰论坛
  • 任务十二 我的页面及添加歌曲功能开发
  • Typescript入门-对象讲解
  • Python量化交易:结合爬虫与TA-Lib技术指标分析
  • Matplotlib数据可视化实战:Matplotlib子图布局与管理入门
  • Ansible 角色管理指南
  • Pandas数据处理与分析实战:Pandas数据处理与Matplotlib可视化入门
  • 0819 使用IP多路复用实现TCP并发服务器
  • Tomcat 的核心脚本catalina.sh 和 startup.sh的关系
  • 陪诊小程序系统开发:开启智慧就医新时代
  • CNN 在故障诊断中的应用:原理、案例与优势
  • BEV:隐式相机视角转换-----BEVFormer
  • 简单实现监听redis的Key过期事件
  • Shopee本土店账号安全运营:规避封禁风险的多维策略
  • 微服务-08.微服务拆分-拆分商品服务
  • 什么是强化学习
  • JMeter高级性能测试训练营 – 从入门到企业级实战
  • pytest高级用法之插件开发
  • Quartus Prime 18.1网盘资源下载与安装指南
  • 从线性回归到神经网络到自注意力机制 —— 激活函数与参数的演进