百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

利用Redis进行数据缓存(使用redis缓存)

mhr18 2024-11-05 10:22 25 浏览 0 评论

1. 引言

缓存有啥用?

降低对数据库的请求,减轻服务器压力

提高了读写效率

缓存有啥缺点?

如何保证数据库与缓存的数据一致性问题?

维护缓存代码

搭建缓存一般是以集群的形式进行搭建,需要运维的成本

2. 将信息添加到缓存的业务流程

上图可以清晰的了解Redis在项目中所处的位置,是数据库与客户端之间的一个中间件,也是数据库的保护伞。有了Redis可以帮助数据库进行请求的阻挡,阻止请求直接打入数据库,提高响应速率,极大的提升了系统的稳定性。

3. 实现代码

下面将根据查询商铺信息来作为背景进行代码书写,具体的流程图如上所示。

3.1 代码实现(信息添加到缓存中)

public static final String SHOPCACHEPREFIX = "cache:shop:";


@Autowired

private StringRedisTemplate stringRedisTemplate;

// JSON工具

ObjectMapper objectMapper = new ObjectMapper();

@Override

public Result queryById(Long id) {

//从Redis查询商铺缓存

String cacheShop = stringRedisTemplate.opsForValue().get(SHOPCACHEPREFIX + id);

//判断缓存中数据是否存在

if (!StringUtil.isNullOrEmpty(cacheShop)) {

//缓存中存在则直接返回

try {

// 将子字符串转换为对象

Shop shop = objectMapper.readValue(cacheShop, Shop.class);

return Result.ok(shop);

} catch (JsonProcessingException e) {

e.printStackTrace();

}

}

//缓存中不存在,则从数据库里进行数据查询

Shop shop = getById(id);

//数据库里不存在,返回404

if (null==shop){

return Result.fail("信息不存在");

}

//数据库里存在,则将信息写入Redis

try {

String shopJSon = objectMapper.writeValueAsString(shop);

stringRedisTemplate.opsForValue().set(SHOPCACHEPREFIX+id,shopJSon,30,TimeUnit.MINUTES);

} catch (JsonProcessingException e) {

e.printStackTrace();

}

//返回

return Result.ok(shop);

}

3.2 缓存更新策略

数据库与缓存数据一致性问题,当数据库信息修改后,缓存的信息应该如何处理?

内存淘汰 超时剔除 主动更新

说明 不需要自己进行维护,利用Redis的淘汰机制进行数据淘汰 给缓存数据添加TTL 编写业务逻辑,在修改数据库的同时更新缓存

一致性 差劲 一般 好

维护成本 无 低 高

这里其实是需要根据业务场景来进行选择

高一致性:选主动更新

低一致性:内存淘汰和超时剔除

3.3 实现主动更新

此时需要实现数据库与缓存一致性问题,在这个问题之中还有多个问题值得深思

删除缓存还是更新缓存?

当数据库发生变化时,我们如何处理缓存中无效的数据,是删除它还是更新它?

更新缓存:每次更新数据库都更新缓存,无效写操作较多

删除缓存:更新数据库时删除缓存,查询时再添加缓存

由此可见,选择删除缓存是高效的。

如何保证缓存与数据库的操作的同时成功或失败?

单体架构:单体架构中采用事务解决

分布式架构:利用分布式方案进行解决

先删除缓存还是先操作数据库?

在并发情况下,上述情况是极大可能会发生的,这样子会导致缓存与数据库数据库不一致。

先操作数据库,在操作缓存这种情况,在缓存数据TTL刚好过期时,出现一个A线程查询缓存,由于缓存中没有数据,则向数据库中查询,在这期间内有另一个B线程进行数据库更新操作和删除缓存操作,当B的操作在A的两个操作间完成时,也会导致数据库与缓存数据不一致问题。

完蛋!!!两种方案都会造成数据库与缓存一致性问题的发生,那么应该如何来进行选择呢?

虽然两者方案都会造成问题的发生,但是概率上来说还是先操作数据库,再删除缓存发生问题的概率低一些,所以可以选择先操作数据库,再删除缓存的方案。

个人见解:

如果说我们在先操作数据库,再删除缓存方案中线程B删除缓存时,我们利用java来删除缓存会有Boolean返回值,如果是false,则说明缓存已经不存在了,缓存不存在了,则会出现上图的情况,那么我们是否可以根据删除缓存的Boolean值来进行判断是否需要线程B来进行缓存的添加(因为之前是需要查询的线程来添加缓存,这里考虑线程B来添加缓存,线程B是操作数据库的缓存),如果线程B的添加也在线程A的写入缓存之前完成也会造成数据库与缓存的一致性问题发生。那么是否可以延时一段时间(例如5s,10s)再进行数据的添加,这样子虽然最终会统一数据库与缓存的一致性,但是若是在这5s,10s内又有线程C,D等等来进行缓存的访问呢?C,D线程的访问还是访问到了无效的缓存信息。

所以在数据库与缓存的一致性问题上,除非在写入正确缓存之前拒绝相关请求进行服务器来进行访问才能避免用户访问到错误信息,但是拒绝请求对用户来说是致命的,极大可能会导致用户直接放弃使用应用,所以我们只能尽可能的减少问题可能性的发生。(个人理解,有问题可以在评论区留言赐教)

@Override

@Transactional

public Result updateShop(Shop shop) {

Long id = shop.getId();

if (null==id){

return Result.fail("店铺id不能为空");

}

//更新数据库

boolean b = updateById(shop);

//删除缓存

stringRedisTemplate.delete(SHOPCACHEPREFIX+shop.getId());

return Result.ok();

}

4. 缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

解决方案:

缓存空对象

缺点:

空间浪费

如果缓存了空对象,在空对象的有效期内,我们后台在数据库新增了和空对象相同id的数据,这样子就会造成数据库与缓存一致性问题

布隆过滤器

优点:

内存占用少

缺点:

实现复杂

存在误判的可能(存在的数据一定会判断成功,但是不存在的数据也有可能会放行进来,有几率造成缓存穿透)

4.1 解决缓存穿透(使用空对象进行解决)

public static final String SHOPCACHEPREFIX = "cache:shop:";

@Autowired

private StringRedisTemplate stringRedisTemplate;

// JSON工具

ObjectMapper objectMapper = new ObjectMapper();

@Override

public Result queryById(Long id) {

//从Redis查询商铺缓存

String cacheShop = stringRedisTemplate.opsForValue().get(SHOPCACHEPREFIX + id);

//判断缓存中数据是否存在

if (!StringUtil.isNullOrEmpty(cacheShop)) {

//缓存中存在则直接返回

try {

// 将子字符串转换为对象

Shop shop = objectMapper.readValue(cacheShop, Shop.class);

return Result.ok(shop);

} catch (JsonProcessingException e) {

e.printStackTrace();

}

}

// 因为上面判断了cacheShop是否为空,如果进到这个方法里面则一定是空,直接过滤,不打到数据库

if (null != cacheShop){

return Result.fail("信息不存在");

}

//缓存中不存在,则从数据库里进行数据查询

Shop shop = getById(id);

//数据库里不存在,返回404

if (null==shop){

// 缓存空对象

stringRedisTemplate.opsForValue().set(SHOPCACHEPREFIX+id,"",2,TimeUnit.MINUTES);

return Result.fail("信息不存在");

}

//数据库里存在,则将信息写入Redis

try {

String shopJSon = objectMapper.writeValueAsString(shop);

stringRedisTemplate.opsForValue().set(SHOPCACHEPREFIX+id,shopJSon,30,TimeUnit.MINUTES);

} catch (JsonProcessingException e) {

e.printStackTrace();

}

//返回

return Result.ok(shop);

}

上述方案终究是被动方案,我们可以采取一些主动方案,例如

给id加复杂度

权限

热点参数的限流

5. 缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

给不同的Key的TTL添加随机值

大量的Key同时失效,极大可能是TTL相同,我们可以随机给TTL

利用Redis集群提高服务的可用性

给缓存业务添加降级限流策略

给业务添加多级缓存

6. 缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

常见的解决方案:

互斥锁

逻辑过期

互斥锁:

即采用锁的方式来保证只有一个线程去重建缓存数据,其余拿不到锁的线程休眠一段时间再重新重头去执行查询缓存的步骤

优点:

没有额外的内存消耗(针对下面的逻辑过期方案)

保证了一致性

缺点:

线程需要等待,性能受到了影响

可能会产生死锁

逻辑过期:

逻辑过期是在缓存数据中额外添加一个属性,这个属性就是逻辑过期的属性,为什么要使用这个来判断是否过期而不使用TTL呢?因为使用TTL的话,一旦过期,就获取不到缓存中的数据了,没有拿到锁的线程就没有旧的数据可以返回。

它与互斥锁最大的区别就是没有线程的等待了,谁先获取到锁就去重建缓存,其余线程没有获取到锁就返回旧数据,不去做休眠,轮询去获取锁。

重建缓存会新开一个线程去执行重建缓存,目的是减少抢到锁的线程的响应时间。

优点:

线程无需等待,性能好

缺点:

不能保证一致性

缓存中有额外的内存消耗

实现复杂

两个方案各有优缺点:一个保证了一致性,一个保证了可用性,选择与否主要看业务的需求是什么,侧重于可用性还是一致性。

6.1 互斥锁代码

互斥锁的锁用什么?

使用Redis命令的setnx命令。

首先实现获取锁和释放锁的代码

/**

* 尝试获取锁

*

* @param key

* @return

*/

private boolean tryLock(String key) {

Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);

return BooleanUtil.isTrue(flag);

}

/**

* 删除锁

*

* @param key

*/

private void unLock(String key) {

stringRedisTemplate.delete(key);

}

代码实现

public Shop queryWithMutex(Long id) throws InterruptedException {

//从Redis查询商铺缓存

String cacheShop = stringRedisTemplate.opsForValue().get(SHOPCACHEPREFIX + id);

//判断缓存中数据是否存在

if (!StringUtil.isNullOrEmpty(cacheShop)) {

//缓存中存在则直接返回

try {

// 将子字符串转换为对象

Shop shop = objectMapper.readValue(cacheShop, Shop.class);

return shop;

} catch (JsonProcessingException e) {

e.printStackTrace();

}

}

// 因为上面判断了cacheShop是否为空,如果进到这个方法里面则一定是空,直接过滤,不打到数据库

if (null != cacheShop) {

return null;

}

Shop shop = new Shop();

// 缓存击穿,获取锁

String lockKey = "lock:shop:" + id;

try{

boolean b = tryLock(lockKey);

if (!b) {

// 获取锁失败了

Thread.sleep(50);

return queryWithMutex(id);

}

//缓存中不存在,则从数据库里进行数据查询

shop = getById(id);

//数据库里不存在,返回404

if (null == shop) {

// 缓存空对象

stringRedisTemplate.opsForValue().set(SHOPCACHEPREFIX + id, "", 2, TimeUnit.MINUTES);

return null;

}

//数据库里存在,则将信息写入Redis

try {

String shopJSon = objectMapper.writeValueAsString(shop);

stringRedisTemplate.opsForValue().set(SHOPCACHEPREFIX + id, shopJSon, 30, TimeUnit.MINUTES);

} catch (JsonProcessingException e) {

e.printStackTrace();

}

}catch (Exception e){

}finally {

// 释放互斥锁

unLock(lockKey);

}

//返回

return shop;

}

6.2 逻辑过期实现

逻辑过期不设置TTL

代码实现

@Data

public class RedisData {

private LocalDateTime expireTime;

private Object data;

}

由于是热点key,所以key基本都是手动导入到缓存,代码如下

/**

* 逻辑过期时间对象写入缓存

* @param id

* @param expireSeconds

*/

public void saveShopToRedis(Long id,Long expireSeconds){

// 查询店铺数据

Shop shop = getById(id);

// 封装为逻辑过期

RedisData redisData = new RedisData();

redisData.setData(shop);

redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));

// 写入Redis

stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id, JSONUtil.toJsonStr(redisData));

}

逻辑过期代码实现

/**

* 缓存击穿:逻辑过期解决

* @param id

* @return

* @throws InterruptedException

*/

public Shop queryWithPassLogicalExpire(Long id) throws InterruptedException {

//1. 从Redis查询商铺缓存

String cacheShop = stringRedisTemplate.opsForValue().get(SHOPCACHEPREFIX + id);

//2. 判断缓存中数据是否存在

if (StringUtil.isNullOrEmpty(cacheShop)) {

// 3. 不存在

return null;

}

// 4. 存在,判断是否过期

RedisData redisData = JSONUtil.toBean(cacheShop, RedisData.class);

JSONObject jsonObject = (JSONObject) redisData.getData();

Shop shop = JSONUtil.toBean(jsonObject, Shop.class);

LocalDateTime expireTime = redisData.getExpireTime();

// 5. 判断是否过期

if (expireTime.isAfter(LocalDateTime.now())){

// 5.1 未过期

return shop;

}

// 5.2 已过期

String lockKey = "lock:shop:"+id;

boolean flag = tryLock(lockKey);

if (flag){

// TODO 获取锁成功,开启独立线程,实现缓存重建,建议使用线程池去做

CACHE_REBUILD_EXECUTOR.submit(()->{

try {

// 重建缓存

this.saveShopToRedis(id,1800L);

}catch (Exception e){


}finally {

// 释放锁

unLock(lockKey);

}


});

}

// 获取锁失败,返回过期的信息

return shop;

}

/**

* 线程池

*/

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

相关推荐

【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...

Pure Storage推出统一数据管理云平台及新闪存阵列

PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...

对Java学习的10条建议(对java课程的建议)

不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...

SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!

官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...

JDK21有没有什么稳定、简单又强势的特性?

佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...

「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了

在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...

Java面试题及答案最全总结(2025版)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...

数据库日常运维工作内容(数据库日常运维 工作内容)

#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...

分布式之系统底层原理(上)(底层分布式技术)

作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...

oracle 死锁了怎么办?kill 进程 直接上干货

1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

取消回复欢迎 发表评论: