Redis缓存简介以及缓存的更新策略
Posted 爱上口袋的天空
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis缓存简介以及缓存的更新策略相关的知识,希望对你有一定的参考价值。
目录
一、什么是缓存
就像自行车,越野车的避震器
举个例子:越野车,山地自行车,都拥有” 避震器”, 防止车体加速后因惯性,在酷似”U” 字母的地形上飞跃,硬着陆导致的损害 , 像个弹簧一样;
同样,实际开发中,系统也需要” 避震器”, 防止过高的数据访问猛冲系统,导致其操作线程无法及时处理信息而瘫痪;
这在实际开发中对企业讲,对产品口碑,用户评价都是致命的;所以企业非常重视缓存技术;
缓存 (Cache), 就是数据交换的缓冲区 , 俗称的缓存就是缓冲区内的数据 , 一般从数据库中获取,存储于本地代码 。
二、为什么要使用缓存
一句话:因为速度快,好用
缓存数据存储于代码中,而代码运行在内存中,内存的读写性能远高于磁盘,缓存可以大大降低用户访问并发量带来的服务器读写压力
实际开发过程中,企业的数据量,少则几十万,多则几千万,这么大数据量,如果没有缓存来作为” 避震器”, 系统是几乎撑不住的,所以企业会大量运用到缓存技术;
但是缓存也会增加代码复杂度和运营的成本:
三、如何使用缓存
实际开发中,会构筑多级缓存来使系统运行速度进一步提升,例如:本地缓存与 redis 中的缓存并发使用
浏览器缓存:主要是存在于浏览器端的缓存
应用层缓存:可以分为 tomcat 本地缓存,比如之前提到的 map,或者是使用 redis 作为缓存
数据库缓存:在数据库中有一片空间是 buffer pool,增改查数据都会先加载到 mysql 的缓存中
CPU 缓存:当代计算机最大的问题是 cpu 性能提升了,但内存读写速度没有跟上,所以为了适应当下的情况,增加了 cpu 的 L1,L2,L3 级的缓存
四、添加商户缓存
在我们查询商户信息时,我们是直接操作从数据库中去进行查询的,大致逻辑是这样,直接查询数据库那肯定慢咯,所以我们需要增加缓存
@GetMapping("/id")
public Result queryShopById(@PathVariable("id") Long id)
//这里是直接查询数据库
return shopService.queryById(id);
1、缓存模型和思路
标准的操作方式就是查询数据库之前先查询缓存,如果缓存数据存在,则直接从缓存中返回,如果缓存数据不存在,再查询数据库,然后将数据存入 redis。
2、代码如下
代码思路:如果缓存有,则直接返回,如果缓存不存在,则查询数据库,然后存入 redis。
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id)
// 1.从redis查询商铺缓存
String key = CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson))
// 3.存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
System.out.println("Redis");
return Result.ok(shop);
// 4.不存在,根据id查询数据库
Shop shop = getById(id);
// 5.不存在,返回错误
if (shop == null)
return Result.fail("店铺不存在!");
// 6.存在,写入Redis
// 把shop转换成为JSON形式写入Redis
System.out.println("MySQL");
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
return Result.ok(shop);
五、缓存更新策略
缓存更新是 redis 为了节约内存而设计出来的一个东西,主要是因为内存数据宝贵,当我们向 redis 插入太多数据,此时就可能会导致缓存中的数据过多,所以 redis 会对部分数据进行更新,或者把他叫为淘汰更合适。
- 内存淘汰:redis 自动进行,当 redis 内存达到咱们设定的 max-memery 的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据 (可以自己设置策略方式)
- 超时剔除:当我们给 redis 设置了过期时间 ttl 之后,redis 会将超时的数据进行删除,方便咱们继续使用缓存
- 主动更新:我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题
- 内存淘汰:redis本身的事情,当redis内存快满了,它自身会部分数据剔除,但是该操作不可控。
- 超时剔除:设置一个TTL过期时间(expire命令),到期就删除,但是一致性的强弱取决于TTL时间设置的长短。
- 主动更新(重):一改数据库,就更新缓存。
- 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
- 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存
1、数据库缓存不一致解决方案:
由于我们的缓存的数据源来自于数据库 , 而数据库的数据是会发生变化的 , 因此,如果当数据库中数据发生变化,而缓存却没有同步 , 此时就会有一致性问题存在 , 其后果是:
用户使用缓存中的过时数据,就会产生类似多线程数据安全问题,从而影响业务,产品口碑等;怎么解决呢?有如下几种方案
- Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案
- Read/Write Through Pattern : 由系统本身完成,数据库与缓存的问题交由系统本身去处理
- Write Behind Caching Pattern :调用者只操作缓存,其他线程去异步处理数据库,实现最终一致
2、数据库和缓存不一致采用什么方案
- 方案一,代码最复杂,但是可靠性高;
- 方案二,将缓存与数据库看作一个整体(服务)来操作,但是这个服务不太好维护,市面上目前也没有这种东西。
- 方案三,最简单,调用者只操作缓存,缓存持久化到数据库的过程交给一个异步的线程,效率高,但是一致性难以保证!如果出现宕机,数据会丢失,所以可靠性也存在问题!
综上所述:首选方案一!
3、Cache Aside Pattern实现
更新缓存 与 删除缓存
- 如何使用 “更新缓存” 那么我每修改一次数据库的数据时,Redis就要更新一次值,数据库更新100次,Redis就要更新100次;
- 而如果是“删除缓存” 那么当我修改数据库的数据时,Redis中的值就会被直接删除,数据库更新100次,Redis只要更新1次!!!
- 而且使用“删除缓存”的方案,当没人来查询该数据时,我们也不用将数据库的值写入缓存redis中。
综上所述:必然时选择 “删除缓存” !相当于延迟加载。
4、先操作数据库还是先操作缓存?
(1)先操作缓存再操作数据库
正常情况下:假设数据库与缓存中的值为10,按下述操作最后缓存与数据库中的值都为20。
异常情况:假设数据库与缓存中的值为10,按下述操作最后缓存的值为10而数据库中的值为20
2)先操作数据库再操作缓存
正常情况下:假设数据库与缓存中的值为10,按下述操作最后缓存与数据库中的值都为20。
异常情况:假设数据库与缓存中的值为10,并且恰好此时缓存失效了(TTL到了,故为空!),先查缓存,未命中,就查询数据库得到值为10;并且恰好此时在更新数据库前,线程2抢夺走CPU资源,更新数据库值为20,再删除缓存(本就为空),然后线程切换,写入缓存(这里写入的值是之前查询的,不是在更新数据库后的新值!!!故写入缓存的值为10)。
综上所述:
上述两种情况都有可能发生!但是第二种情况发生的可能性非常小,在查询数据库后,我们紧接着是要写缓存,而写缓存一般是微秒级别的,几乎不可能在这一瞬间,数据库更新了,且就算数据库更新,写入数据的速度也比不上redis,所以我们选择第二种方案!选择:先操作数据库再操作缓存 的方式来实现!
六、实现商铺和缓存与数据库双写一致
1、加入超时时间 queryById()
@Override
public Result queryById(Long id)
// 1.从redis查询商铺缓存
String key = CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson))
// 3.存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
System.out.println("Redis");
return Result.ok(shop);
// 4.不存在,根据id查询数据库
Shop shop = getById(id);
// 5.不存在,返回错误
if (shop == null)
return Result.fail("店铺不存在!");
// 6.存在,写入Redis
// 把shop转换成为JSON形式写入Redis
System.out.println("MySQL");
// 同时添加超时时间
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
2、修改更新的逻辑
代码分析:通过之前的淘汰,我们确定了采用删除策略,来解决双写问题,当我们修改了数据之后,然后把缓存中的数据进行删除,查询时发现缓存中没有数据,则会从 mysql 中加载最新的数据,从而避免数据库和缓存不一致的问题,之后如果在查询该店铺,再会由上面的方法写入 queryById(id)
@Override
@Transactional
public Result update(Shop shop)
System.out.println("up");
Long id = shop.getId();
if (id == null)
return Result.fail("店铺id不能为空!");
// 1.更新数据库
updateById(shop);
// 2.删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
return Result.ok();
以上是关于Redis缓存简介以及缓存的更新策略的主要内容,如果未能解决你的问题,请参考以下文章