本章导学:
- 了解什么是缓存
- 了解缓存更新的几种策略
- 内存淘汰
- 过期剔除
- 主动更新(重点掌握)
- 代码实现
一、什么是缓存?
缓存就是数据交换的缓冲区,是存贮数据的临时地方,一般读写速度性能较高
当今电脑的CPU发展已经非常强大了,它的运算能力十分之高,已经远远超出内存、磁盘的读写数据的能力。但是,CPU所做的任何运算,都要先从磁盘或者内存读出数据,放到CPU寄存器里后才可以进行运算。所以就出现了一个问题: 数据读写能力远远低于CPU运算能力,计算机性能收到了限制
那么为了解决这个问题,人们就在CPU内部添加了一块缓存区,CPU会把经常需要读写的数据,放到CPU缓存区,当我们做高速运算的时候,就不用从内存或者磁盘里读出数据,而是直接从CPU内部的缓存区取出数据,大大提高了读取速度。
但是凡事有利就有弊,缓存也是如此。如果我们从磁盘里读出了数据并添加到缓存,然后磁盘里的数据进行了变更,但我们缓存区的数据没有随之更新,就出现了数据一致性问题。
优势:
- 降低了后端负载
- 提高了读写效率
- 降低相应时间
弊端:
- 数据一致性问题
- 代码维护成本提高
二、缓存更新的策略
为了解决数据一致性问题,我们需要更新缓存。这里介绍几种更新缓存的策略。
2.1、内存淘汰策略
说明:开发人员无需自己维护,利用Redis自带的缓存淘汰机制,当内存不足时自动淘汰部分缓存,下次查询时再更新缓存。
一致性:低。因为该机制只会在内存不足时触发。当且仅当内存不足时,才会删除掉旧的缓存,待下次查询时进行缓存更新。
维护成本:无。使用Redis缓存淘汰机制,开发人员无需任何操作。
2.2、超时剔除策略
说明:在添加缓存的时候,主动为缓存设置过期时间(TTL),待设置时间结束后缓存自动删除,等待下次查询后更新缓存。
一致性:一般。取决于设置的过期时间长短。
维护成本:低。只需要开发人员在添加缓存时设置TTL即可。
2.3、主动更新策略
说明:在开发人员编写业务逻辑时,只要遇到数据库的修改,同步更新缓存。
一致性:高。数据库一变更,缓存直接
维护成本:高。需要开发人员编码实现数据库与缓存同步。
具体选择哪种方案来解决数据一致性问题,我们需要结合业务场景来看。
低一致性需求:采用内存淘汰策略
高一致性需求:采用主动更新策略,并使用过期淘汰策略作为兜底。
在采用主动更新策略的时候,我们还需要考虑三个问题:
1、是更新缓存还是删除缓存?
2、如何保证数据库与缓存同成功或同失败?
3、是先操作数据库还是先操作缓存?
问题一:先删除缓存较好。假设我们每次更新数据库都对应的更新缓存,那么如果该数据修改了一万次,我们也要更新一万次缓存,而用户只访问该数据一次,无效的<写>操作太多了!
而如果我们先删除缓存,不论数据库修改了多少次,只有当用户访问了这条数据,我们才把数据添加到缓存里,那么对于缓存的操作只有一次。
问题二:把缓存与数据库操作放在同一个事务里即可。
问题三:我们看下面的情况演示
先删除缓存,再更新数据库(正常情况),数据库原始数据V1=10
先删除缓存,再更新数据库(异常情况),数据库原始数据V1=10
接下来我们看先更新数据库,再删除缓存的两种情况
先更新数据库,再删除(正常情况),数据库原始数据V1=10
先更新数据库,再删除(异常情况),数据库原始数据V1=10
我们主要分析一下这种情况:首先,要达成上述情况我们需要满足两个前提:
1,再用户查询的时候,缓存恰好过期了
2,再查询完数据库写入缓存的这微秒时间(缓存写入是非常快的),被线程B抢占更新了数据库,而数据库的更新往往是比较慢的。只有非常非常小的概率会出现,在缓存写入过程中的微秒时间内,数据库完成了更新并删除了缓存两个步骤
总结:
基于以上四种分析,我们可以得出之前提过三个问题的答案了。
1、是更新缓存还是删除缓存?
答:删除缓存
2、如何保证数据库与缓存同成功或同失败?
答:把数据库与缓存操作放在同一个事务
3、是先操作数据库还是先操作缓存?
答:先操作数据库
所以更新缓存的最佳策略就是:
低一致性需求:使用Redis内存淘汰策略
高一致性需求:使用主动更新策略,并用过期淘汰策略兜底
读操作:
- 缓存命中直接返回
- 缓存未命中,查询数据库后,写入缓存,并设置过期时间
写操作:
- 先修改数据库,再删除缓存
三、代码实现
需求:通过ID查询店铺信息,并添加到缓存。需要保证数据一致性
操作:先从Redis查询缓存,命中了直接返回,没命中去数据库查询,查询后添加缓存并设置过期时间。
Controller层方法如下:
具体实现:
//需求,通过ID查询店铺信息,并添加到缓存。需要保证数据一致性
@Override
public Result queryById(Long id) {
//定义个cacheID作为key,后续方便使用
String cacheId = CACHE_SHOP_KEY + id;
//1、从Redis中查询商户信息
String shopJson = stringRedisTemplate.opsForValue().get(cacheId);
//2、击中了,转成对象直接返回
if(StrUtil.isNotBlank(shopJson)){
return Result.ok(JSONUtil.toBean(shopJson,Shop.class));
}
//3、没击中,去数据库查
//Shop shop = getById(id);
Shop shop = query().eq("id", id).one();
//4、数据库里也没查到,返回异常
if(shop == null){
return Result.fail(SHOP_ERROR);
}
//5、添加数据到redis缓存
stringRedisTemplate.opsForValue().set(cacheId,JSONUtil.toJsonStr(shop));
//6、设置过期时间,30分钟
stringRedisTemplate.expire(cacheId,CACHE_SHOP_TTL, TimeUnit.SECONDS);
//6、返回数据
return Result.ok(shop);
}
需求:通过ID修改店铺信息,并添加到缓存。需要保证数据一致性
操作:先更新数据库信息,再删除缓存(更新里只用做这两步),待下一次查询后,根据查询到的数据库信息添加缓存,并设置过期时间
Controller层方法如下:
具体实现:
//数据库与缓存的操作放在同一个事务中处理
@Override
@Transactional
public Result updateByShopId(Shop shop) {
//判断用户输入的ID是否存在
Long shopId = shop.getId();
if(shopId == null){
return Result.fail("商铺ID不存在!");
}
//1、更新数据库
boolean update = updateById(shop);
//2、删除缓存
//key的话我们在查询方法里有定义过的 String cacheId = CACHE_SHOP_KEY + id;
stringRedisTemplate.delete(CACHE_SHOP_KEY + shop.getId());
return Result.ok();
}
接下来我们访问一下商铺信息,观察Redis缓存
可以发现,缓存已经成功添加了。
我们再测试一下执行修改操作后,会不会把缓存成功删除
用POSTMAN发个请求
去Redis看看缓存删掉没
成功删除!
我们重新访问店铺信息