Redis缓存简介以及缓存的更新策略

Posted 爱上口袋的天空

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis缓存简介以及缓存的更新策略相关的知识,希望对你有一定的参考价值。

目录

一、什么是缓存

二、为什么要使用缓存

三、如何使用缓存

四、添加商户缓存

1、缓存模型和思路

 2、代码如下

五、缓存更新策略

 1、数据库缓存不一致解决方案:

 2、数据库和缓存不一致采用什么方案

3、Cache Aside Pattern实现

4、先操作数据库还是先操作缓存?

六、实现商铺和缓存与数据库双写一致

 1、加入超时时间 queryById()

2、修改更新的逻辑


一、什么是缓存

就像自行车,越野车的避震器

举个例子:越野车,山地自行车,都拥有” 避震器”, 防止车体加速后因惯性,在酷似”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缓存简介以及缓存的更新策略的主要内容,如果未能解决你的问题,请参考以下文章

iOS web缓存策略以及手动清除缓存

redis订阅发布消息操作本地缓存

redis缓存更新策略

Redis-Sp:刷新缓存策略

Redis7高级之缓存双写一致性之更新策略探讨

redis延迟双删策略