Redis实战常用二、缓存的使用
一、什么是缓存
-
在实际开发中,系统需要"避震器",
防止过高的数据访问猛冲系统,导致其操作线程无法及时处理信息而瘫痪.- 这在实际开发中对企业讲,对产品口碑,用户评价都是致命的。所以企业非常重视缓存技术;
-
缓存(Cache):就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据,一般从数据库中获取,存储于本地代码中。
- 缓冲区:是存储数据的临时地方,一般读写性能较高。
- 例子:
// 例1:
static final ConcurrentHashMap<K,V> map = new ConcurrentHashMap<>(); //本地用于高并发缓存// 例2:
static final Cache<K,V> USER_CACHE = CacheBuilder.newBuilder().build(); // 用于redis等缓存// 例3:
Static final Map<K,V> map = new HashMap(); // 本地缓存
由于其被Static修饰,所以随着类的加载而被加载到内存之中,作为本地缓存,由于其又被final修饰,所以其引用(例3:map)和对象(例3:new HashMap())之间的关系是固定的,不能改变,因此不用担心赋值(=)导致缓存失效。
二、为什么使用缓存
- 优点:访问速度快,好用
- 缓存数据存储于代码中,而代码运行在内存中,
内存的读写性能远高于磁盘,缓存可以大大降低用户访问并发量带来的服务器读写压力。- 实际开发过程中,企业的数据量,少则几十万,多则几千万,这么大数据量,如果没有缓存来作为"避震器",系统是几乎撑不住的,所以企业会大量运用到缓存技术。
- 缓存也会增加代码复杂度和运营的成本。
三、使用缓存的缺点与优点

四、如何使用缓存
- 实际开发中,会构建
多级缓存来使系统运行速度进一步提升,例如:本地缓存与redis中的缓存并发使用等等。- 浏览器缓存:主要是存在于浏览器端的缓存
- 应用层缓存:可以分为tomcat本地缓存,比如之前提到的map,或者是使用redis作为缓存
- 数据库缓存:在数据库中有一片空间是 buffer pool,增改查数据都会先加载到mysql的缓存中
- CPU缓存:当代计算机最大的问题是 cpu性能提升了,但内存读写速度没有跟上,所以为了适应当下的情况,增加了cpu的L1,L2,L3级的缓存

四、1、添加缓存的思路
-
标准的操作方式就是
查询数据库之前先查询缓存:- 如果缓存数据存在,则直接从缓存中返回。
- 如果缓存数据不存在,再查询数据库,然后将查询到的数据存入redis,再把数据库的数据返回。
-
不使用缓存

-
使用缓存

-
举例:根据id查询商铺的信息

四、2、缓存的更新策略
- 缓存更新是redis为了
节约内存而设计出来的一个东西,主要是因为内存数据宝贵,当我们向redis插入太多数据,此时就可能会导致缓存中的数据过多,所以redis会对部分数据进行更新,或者把它叫为淘汰更合适。主要有一下三种策略:- 内存淘汰:redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)
- 超时剔除:当我们给redis设置了过期时间ttl之后,redis会将超时的数据进行删除,方便后面继续使用缓存
- 主动更新:我们可以
手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题

四、2.1、数据库和缓存不一致的问题解决:
- 由于我们的缓存的数据源来自于数据库,而数据库的数据是会发生变化的,因此,如果当数据库中数据发生变化,而缓存却没有同步,此时就会有一致性问题存在,其后果是:
- 用户使用缓存中的过时数据,就会产生类似多线程数据安全问题,从而影响业务,产品口碑等。怎么解决呢?有如下几种方案:
- 1、
Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案 - 2、
Read/Write Through Pattern: 由系统本身完成,数据库与缓存的问题交由系统本身去处理 - 3、
Write Behind Caching Pattern:调用者只操作缓存,其他线程去异步处理数据库,实现最终一致

- 1、
- 用户使用缓存中的过时数据,就会产生类似多线程数据安全问题,从而影响业务,产品口碑等。怎么解决呢?有如下几种方案:
数据库和缓存不一致采用Cache Aside Pattern 人工编码方式
- 数据库和缓存不一致采用方法为:
Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案
操作缓存和数据库时有三个问题需要考虑:
- 假设我们每次操作数据库后,都操作缓存,但是中间如果没有人查询,那么这个更新动作实际上只有最后一次生效,中间的更新动作意义并不大,我们可以
把缓存删除,等待再次查询时,将缓存中的数据加载出来. - 问题一:选择删除缓存还是更新缓存? (
删除缓存)- 更新缓存:每次更新数据库都更新缓存,无效写操作较多
- 删除缓存:更新数据库时让缓存失效,查询时再更新缓存
- 为什么不更新缓存儿选择直接删除缓存呢?
- 因为
缓存的更新成本比删除成本更高。(因为你写入数据库的值在很多情况下并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么每次写入数据库后都再次计算写入缓存的值,无疑是浪费性能的,所以删除缓存更为适合。
- 因为
- 问题二:如何保证缓存与数据库的操作的同时成功或失败?
- 单体系统,将缓存与数据库操作放在一个事务
- 分布式系统,利用TCC等分布式事务方案
- 问题三:选择先操作缓存还是先操作数据库?(
先操作数据库,再删除缓存)- 先删除缓存,再操作数据库
- 先操作数据库,再删除缓存
- 具体操作缓存还是操作数据库,我们应当是先操作数据库,再删除缓存
- 原因:如果你选择第一种方案(Cache Aside Pattern 人工编码方式),在两个线程并发来访问时,假设线程1先来,他先把缓存删了,此时线程2过来查询缓存数据并不存在,此时线程2查询数据库并把查询出来的数据写入缓存,当线程2把数据写入缓存后,线程1再执行更新动作时,实际上写入的就是旧的数据,新的数据被旧数据覆盖了。
先删除缓存还是先执行数据库操作的选择分析
- 写缓存的时间是很快的(几毫秒),而操作数据库的时间是很长的。(操作缓存和操作数据库效率差别大)
1、先删除缓存,再操作数据
- 先删除缓存,再操作数据的正常情况:

- 先删除缓存,再操作数据的异常情况(出现数据库和缓存数据不一致的问题):
- 解决办法一:
延迟双删:最终一致性的方法,即使用双删的办法。(就是每次修改完数据之后再把缓存删除了(使用延迟删除,避免删除缓存操作在旧数据的写入缓存操作之前)),这样就保证了数据的一致性了,但还是会出现一次数据不一致的问题,如果想避免一次数据一致性都不出现就得使用强一致性的方法了)(推荐使用这个方法)- 这方法感觉就相当于先操作数据库再删除缓存一样了,由此看来还是选择先操作再删除缓存
- 解决办法二:使用
强一致性的办法(就是保证redis的操作和数据库的操作的原子性),可以使用加锁的方法(使用这种方法会影响性能,我们使用redis的意义就是为了提高性能,所以加锁就得不偿失了,所以不太推荐了)

- 解决办法一:
2、 先操作数据,再删除缓存
- 先操作数据,再删除缓存的正常情况:

- 先操作数据,再删除缓存的异常情况1(出现数据库和缓存数据不一致的问题):因为缓存操作是很快的,所以这种异常情况几乎不可能发生。

- 先操作数据,再删除缓存的异常情况2:删除缓存失败的情况,就是线程2执行删除缓存操作时出现删除失败的问题,导致最终的缓存数据还是旧数据。
- 解决方案一:使用异步删除重试的办法(结合mq)
- 解决方案二:cannal解耦(使用cannal提供的java客户端)
最终选择:先操作数据库再删除缓存

缓存穿透
缓存雪崩
缓存击穿(热点key)
最后可以把缓存击穿和缓存穿透的解决方法封装成一个工具类
基于StringRedisTemplate封装一个缓存工具类,满足下列需求:
- 方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
- 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓
存击穿问题
- 方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
- 方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
@Component
public class CacheClient {private final StringRedisTemplate stringRedisTemplate;private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);//使用构造器注入第三方beanStringRedisTemplatepublic CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}// 添加缓存,设置key的过期时间public void set(String key, Object value, Long time, TimeUnit unit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);}// 添加缓存,设置逻辑过期时间public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {// 设置逻辑过期RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));// 写入RedisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}// 获取缓存,解决缓存穿透public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){String key = keyPrefix + id;// 1.从redis查询商铺缓存String json = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isNotBlank(json)) {// 3.存在,直接返回return JSONUtil.toBean(json, type);}// 判断命中的是否是空值if (json != null) {// 返回一个错误信息return null;}// 4.不存在,根据id查询数据库R r = dbFallback.apply(id);// 5.不存在,返回错误if (r == null) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redisthis.set(key, r, time, unit);return r;}// 使用逻辑过期时间,解决缓存击穿public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.从redis查询商铺缓存String json = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isBlank(json)) {// 3.存在,直接返回return null;}// 4.命中,需要先把json反序列化为对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime = redisData.getExpireTime();// 5.判断是否过期if(expireTime.isAfter(LocalDateTime.now())) {// 5.1.未过期,直接返回店铺信息return r;}// 5.2.已过期,需要缓存重建// 6.缓存重建// 6.1.获取互斥锁String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);// 6.2.判断是否获取锁成功if (isLock){// 6.3.成功,开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 查询数据库R newR = dbFallback.apply(id);// 重建缓存this.setWithLogicalExpire(key, newR, time, unit);} catch (Exception e) {throw new RuntimeException(e);}finally {// 释放锁unlock(lockKey);}});}// 6.4.返回过期的商铺信息return r;}// 使用互斥锁解决缓存穿透public <R, ID> R queryWithMutex(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.从redis查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isNotBlank(shopJson)) {// 3.存在,直接返回return JSONUtil.toBean(shopJson, type);}// 判断命中的是否是空值if (shopJson != null) {// 返回一个错误信息return null;}// 4.实现缓存重建// 4.1.获取互斥锁String lockKey = LOCK_SHOP_KEY + id;R r = null;try {boolean isLock = tryLock(lockKey);// 4.2.判断是否获取成功if (!isLock) {// 4.3.获取锁失败,休眠并重试Thread.sleep(50);return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);}// 4.4.获取锁成功,根据id查询数据库r = dbFallback.apply(id);// 5.不存在,返回错误if (r == null) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redisthis.set(key, r, time, unit);} catch (InterruptedException e) {throw new RuntimeException(e);}finally {// 7.释放锁unlock(lockKey);}// 8.返回return r;}private boolean tryLock(String key) {Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}private void unlock(String key) {stringRedisTemplate.delete(key);}
}
相关文章:
Redis实战常用二、缓存的使用
一、什么是缓存 在实际开发中,系统需要"避震器",防止过高的数据访问猛冲系统,导致其操作线程无法及时处理信息而瘫痪. 这在实际开发中对企业讲,对产品口碑,用户评价都是致命的。所以企业非常重视缓存技术; 缓存(Cache):就是数据交换的缓冲区&…...
G口服务器和普通服务器之间的区别
今天小编主要来为大家介绍一下G口服务器和普通服务器之间的区别! 首先,从硬件配置上看,普通服务器通常都会配备中央处理器、内存和硬盘等基本的硬件配置,能够适用于各种应用程序和服务;G口服务器除了基础的硬件配置还增…...
通过国内源在Ubuntu20.0.4安装repo
国内三大免费源: 清华大学:清华大学开源软件镜像站 | Tsinghua Open Source Mirror中国科技大学:USTC Open Source Software Mirror阿里云:阿里巴巴开源镜像站-OPSX镜像站-阿里云开发者社区 repo只在清华源网站里搜到:…...
多维动态规划 力扣hot100热门面试算法题 面试基础 核心思路 背题
多维动态规划 不同路径 https://leetcode.cn/problems/unique-paths/ 核心思路 比较简单 f[i][j] f[i - 1][j] f[i][j - 1] ; 示例代码 class Solution {public int uniquePaths(int n, int m) {int[][] f new int[n][m];for (int i 0; i < n; i)f[i][0] 1;for…...
《Java到Go的平滑转型指南》
文章目录 **文章摘要****核心主题****关键内容提炼****决策者行动清单****核心结论** **第一章:转型决策:为什么要从Java转向Go?****1.1 性能对比:GC机制与并发模型差异****GC机制对比****并发模型基准测试** **1.2 开发效率&…...
【软件测试】:软件测试实战
1. ⾃动化实施步骤 1.1 编写web测试⽤例 1.2 ⾃动化测试脚本开发 common public class AutotestUtils {public static EdgeDriver driver;// 创建驱动对象public static EdgeDriver createDriver(){// 驱动对象已经创建好了 / 没有创建if( driver null){driver new EdgeDr…...
SpringMVC 请求处理
SpringMVC 请求处理深度解析:从原理到企业级应用实践 一、架构演进与核心组件协同 1.1 从传统Servlet到前端控制器模式 SpringMVC采用前端控制器架构模式,通过DispatcherServlet统一处理请求,相比传统Servlet的分散处理方式,实…...
unittest自动化测试实战
🍅 点击文末小卡片,免费获取软件测试全套资料,资料在手,涨薪更快 为什么要学习unittest 按照测试阶段来划分,可以将测试分为单元测试、集成测试、系统测试和验收测试。单元测试是指对软件中的最小可测试单元在与程…...
leetcode3.无重复字符的最长字串
采用滑动窗口方法 class Solution { public:int lengthOfLongestSubstring(string s) {int ns.size();if(n0)return 0;int result0;unordered_set<char> set;set.insert(s[0]);for(int i0,j0;i<n;i){while(j1<n&&set.find(s[j1])set.end()){set.insert(s[…...
Android Compose 框架派生状态(derivedStateOf、rememberCoroutineScope)深入剖析(十五)
一、引言 在 Android 开发领域,高效的状态管理对于构建响应式、高性能的应用程序至关重要,在 Jetpack Compose 中,derivedStateOf 和 rememberCoroutineScope 这两个与派生状态相关的特性在状态管理方面发挥着关键作用。派生状态允许我们根据…...
3.25-2request库
request库 一、介绍request库 (1)requests是用python语言编写的简单易用的http库,用来做接口测试的库; (2)接口测试自动化库有哪些? requests、urllib 、urllib2、urllib3、 httplib 等&…...
《破解老龄化的智能密钥:机器人四维战略与未来养老生态》
一、引言:老龄化社会与智能机器人的必然性 全球老龄化趋势与老年人核心需求(健康管理、生活辅助、心理陪伴、安全保障) 全球正面临着严峻的老龄化挑战。根据联合国发布的数据,全球60岁及以上人口数量在过去几十年中持续增长&…...
Docker-Volume数据卷详讲
Docker数据卷-Volume 一:Volume是什么,用来做什么的 当删除docker容器时,容器内部的文件就会跟随容器所销毁,在生产环境中我们需要将数据持久化保存,就催生了将容器内部的数据保存在宿主机的需求,volume …...
SpringMVC 配置
一、MVC 模式简介 在软件开发的广袤天地中,MVC 模式宛如一座明亮的灯塔,指引着开发者构建高效、可维护的应用程序。Spring MVC 作为基于 Spring 框架的重要 web 开发模块,更是将 MVC 模式的优势发挥得淋漓尽致,堪称 Servlet 的强…...
Python 3.8 Requests 爬虫教程(2025最新版)
遵守网站的爬虫规则、避免爬取敏感信息、保护个人隐私! 一、环境配置与基础验证 # 验证 Python 版本(需 ≥3.8) import sys print(sys.version) # 应输出类似 3.8.12 的信息# 安装 requests 库(若未安装) # 命令行执…...
蓝桥杯备考之 最长上升子序列问题(挖地雷)
这道题其实就是正常的最长上升子序列问题,但是我们还要把最优方案输出出来,我们可以用个pre数组来维护就行了,每当我们更新以i为结尾的最长子序列,如果i是接在1到i-1某个点后面的话就把前面的点存到pre里面 最后我们把pre倒着打印…...
华为OD机试2025A卷 - 游戏分组/王者荣耀(Java Python JS C++ C )
最新华为OD机试 真题目录:点击查看目录 华为OD面试真题精选:点击立即查看 题目描述 2020年题: 英雄联盟是一款十分火热的对战类游戏。每一场对战有10位玩家参与,分为两组,每组5人。每位玩家都有一个战斗力,代表着这位玩家的厉害程度。为了对战尽可能精彩,我们需要…...
【Python Cookbook】字符串和文本(二)
字符串和文本(二) 6.字符串忽略大小写的搜索替换7.最短匹配模式8.多行匹配模式9.将 Unicode 文本标准化10.在正则式中使用 Unicode 6.字符串忽略大小写的搜索替换 你需要以忽略大小写的方式搜索与替换文本字符串。 为了在文本操作时忽略大小写…...
Redisson 实现分布式锁简单解析
目录 Redisson 实现分布式锁业务方法:加锁逻辑LockUtil 工具类锁余额方法:工具类代码枚举代码 RedisUtil 工具类tryLock 方法及重载【分布式锁具体实现】Supplier 函数式接口调用分析 Redisson 实现分布式锁 业务方法: 如图,简单…...
六十天Linux从0到项目搭建(第五天)(file、bash 和 shell 的区别、目录权限、默认权限umask、粘滞位、使用系统自带的包管理工具)
1. file [选项] 文件名 用于确定文件类型的实用工具。它会通过分析文件内容(而不仅仅是文件扩展名)来判断文件的实际类型 示例输出解析 $ file /bin/bash /bin/bash: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, i…...
信源的分类及数学模型
信源的分类及数学模型 按照信源发出的时间和消息分布分为离散信源和连续信源 按照信源发出符号之间的关系分为无记忆信源和有记忆信源 单符号离散信源(一维离散信源) 信源输出的消息数有限或可数,且每次只输出符号集的一个消息 样本空间&…...
嵌入式硬件工程师从小白到入门-PCB绘制(二)
PCB绘制从小白到入门:知识点速通与面试指南 一、PCB设计核心流程 需求分析 明确电路功能(如电源、信号处理、通信)。确定关键参数(电压、电流、频率、接口类型)。 原理图设计 元器件选型:匹配封装、电压、…...
抽象工厂设计模式及应用案例
引言 在软件开发中,合理的设计模式可以有效地提高代码的可维护性、可扩展性和可重用性。抽象工厂模式(Abstract Factory Pattern)便是一个重要的创建型设计模式,它允许我们在不指定具体类的情况下,创建一系列相关或相…...
LVS NAT模式实现三台RS的轮询访问
节点规划: 配置RS: RS1-RS3的网关配置均为 192.168.163.8 配置RS1: [rootlocalhost ~]# hostnamectl hostname rs1 [rootlocalhost ~]# nmcli c modify ens160 ipv4.method manual ipv4.addresses 192.168.163.7/24 ipv4.gateway 192.168.163.8 conne…...
LSM-Tree(Log-Structured Merge-Tree)详解
1. 什么是 LSM-Tree? LSM-Tree(Log-Structured Merge-Tree)是一种 针对写优化的存储结构,广泛用于 NoSQL 数据库(如 LevelDB、RocksDB、HBase、Cassandra)等系统。 它的核心思想是: 写入时只追加写(Append-Only),将数据先写入内存缓冲区(MemTable)。内存数据满后…...
uni-app jyf-parser将字符串转化为html 和 rich-text
uni-app jyf-parser将字符串转化为html-CSDN博客 方法二: rich-text | uni-app...
Docker+Ollama+Xinference+RAGFlow+Dify部署及踩坑问题
目录 一、Xinference部署 (一)简介 (二)部署 (三)参数 (四)错误问题 (五)Xinference配置Text-embedding模型 (六)Xinference配…...
CentOS 7 搭建基于匿名用户的 FTP 服务
1. 安装 VSFTPD yum install vsftpd -y 2. 配置 VSFTPD 编辑主配置文 vi /etc/vsftpd/vsftpd.conf 以下配置项存在或修改为对应值 anonymous_enableYES # 启用匿名用户 local_enableNO # 禁用本地用户 write_enableYES # 允许写入(若需要匿名上传࿰…...
【机器学习】什么是线性回归?
什么是线性回归? 线性回归是一种 监督学习算法,它通过拟合一个直线(或平面,高维空间下是超平面)来建立 输入特征 和 输出目标 之间的关系。简单来说,线性回归就是找出一个数学方程(通常是线性方…...
零、ubuntu20.04 安装 anaconda
1.anaconda下载 地址:Index of /anaconda/archive/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror 选择:Anaconda3-2023.07-2-Linux-x86_64.sh 2.anaconda安装 选择下载目录,选在在终端中打开,然后在终端输入安装命…...
