赞
踩
目录
缓存就是数据交换的缓冲区(称作Cache ),是存储数据的临时地方,一般读写性能较高
缓存的作用:
降低后端负载 ---直接访问缓存,返回数据
提高读写效率,降低响应时间---基于内存存储
缓存的成本:
数据一致性成本
代码维护成本----解决一致性问题代码复杂
运维成本-- 要保证高可用搭建集群
redis命中直接返回数据,未命中数据库查询返回数据,并且将数据缓存到redis中
- public Result selectShopInfoById(Long id) {
- String key = CACHE_SHOP_KEY + id;
- //1.判断redis中是否存在该id的数据
- String str = stringRedisTemplate.opsForValue().get(key);
- if (StrUtil.isNotBlank(str)) {
- //2.存在 直接返回数据
- return Result.ok(JSONUtil.toBean(str,Shop.class));
- }
- //3.不存在 查询数据库是否存在
- Shop shop = baseMapper.selectById(id);
- if (StringUtils.isEmpty(shop)) {
- //4.不存在直接返回 404
- return Result.fail("店铺不存在");
- }
- //5.存在将数据存储在redis,然后返回
- String shopJsonStr = JSONUtil.toJsonStr(shop);
- stringRedisTemplate.opsForValue().set(key,shopJsonStr);
- return Result.ok(shop);
- }
给店铺类型业务添加缓存
- public Result queryOrderByAscList() {
- //1.判断redis是否存在 商铺类型的缓存
- Long size = stringRedisTemplate.opsForList().size(CACHE_SHOP_TYPE_KEY);
- if (size > 0){
- //2.存在 直接取出返回
- List<String> range = stringRedisTemplate.opsForList().range(CACHE_SHOP_TYPE_KEY, 0, size);
- List<ShopType> shopTypes = range.stream().map(item -> {
- return JSONUtil.toBean(item,ShopType.class);
- }).collect(Collectors.toList());
- //.sorted(Comparator.comparing(ShopType::getSort).reversed())
- return Result.ok(shopTypes);
- }
- //3.不存在,查询数据库
- QueryWrapper<ShopType> queryWrapper = new QueryWrapper<>();
- queryWrapper.orderByAsc("sort");
- List<ShopType> shopTypes = baseMapper.selectList(queryWrapper);
- if (shopTypes.size() <= 0){
- //4.数据库不存在 直接返回错误
- return Result.fail("数据不存在");
- }
- //5.数据库存在,将数据存储在缓存中然后返回
- List<String> collect = shopTypes.stream().map(item -> {
- return JSONUtil.toJsonStr(item);
- }).collect(Collectors.toList());
- stringRedisTemplate.opsForList().rightPushAll(CACHE_SHOP_TYPE_KEY,collect);
- //设置过期时间是1天
- stringRedisTemplate.expire(CACHE_SHOP_TYPE_KEY,CACHE_SHOP_TYPE_TTL, TimeUnit.MINUTES);
-
- return Result.ok(shopTypes);
- }
内存淘汰 | 超时剔除 | 主动更新 | |
---|---|---|---|
说明 | 不用自己维护,利用Redis的内存淘汰机制,当内存不足时自动淘汰部分数据,下次查询时更新缓存 | 给缓存数据添加TTL过期时间,到期后自动删除缓存。下次查询时更新缓存 | 编写业务逻辑,在修改数据的同时,更新缓存 |
一致性 | 差 | 一般 | 好 |
维护成本 | 无 | 低 | 高 |
业务场景:
低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
高一致性需求:主动更新,并以超时时间作为兜底方案。例如店铺详情查询的缓存
Cache Aside Pattern 有缓存的调用缓存,在更新数据库的同时更新缓存
Read/Write Through Pattern 缓存和数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无序关心缓存一致性问题
Write Behind Caching Pattern 调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,保证最终一致
Redis的主动更新有三种常见的方案,包括: Cache Aside Pattern(旁路缓存模式):应用程序先从缓存中获取数据,如果缓存中不存在要访问的数据,则从数据库获取,再将数据写入缓存中。 优点:高效性能,减少数据库访问次数和负载,适合于对数据实时性要求不高的应用。 缺点:存在缓存和数据库数据不一致的问题,当读写并发量大时,可能会出现脏数据。 Read/Write Through Pattern(读写穿透模式):数据缓存和数据库相连,应用程序从缓存中获取数据,如缓存中没有相应数据,会通过缓存访问层查找数据。该层在未命中数据后,查询数据库。若命中则返回数据,并同步写入缓存中;否则返回空值或默认值。 优点:保证缓存、数据库数据一致性,并且在缓存失效的情况下,也可以避免因读操作而引起的数据库压力过大,同时也可以防止缓存数据与数据库之间的数据不一致。 缺点:每次访问数据都必须通过缓存去访问数据库,增加了结构的复杂性并降低了系统的效率。 Write Behind Caching Pattern(写回缓存模式):在进行写操作时,不直接将数据写入到数据库中,而是先将数据写入缓存中,待缓存达到一定条件后再批量同步到数据库中。 优点:提高了写操作的性能,并且降低了数据库负载,可以适用于写入比较频繁但读取全量较少的应用场景,同时也减少了与数据库的交互次数和延迟。 缺点:由于只在达到缓存阈值之后才进行同步,因此可能会存在缓存中未及时更新的数据,从而引起数据不一致性问题,同时当缓存重新启动时还需要从磁盘上读取数据进行恢复,增加了复杂度。 需要针对具体应用场景选择合适的主动更新方案,并结合Redis中提供的其他功能一起使用。
这里比较常用的:Cache Aside Pattern(旁路缓存模式)
操作缓存和数据库有三个问题需要考虑:
删除缓存还是更新缓存?
更新缓存:每次更新数据库都更新缓存,无效写操作较多
删除缓存:更新数据库时让缓存失败,查询时再更新缓存(比较符合)
如何保障缓存与数据库的操作同时成功或失败?
单体系统,将缓存与数据库操作放在一个事务
分布式系统,利用TCC等分布式事务方案
先操作缓存还是先操作数据库?
先删除缓存,再操作数据库(不推荐,更新数据库时间长,出现概率很大)
第一个线程删除缓存后,在更新数据库的时候,还没更新成功的时候, 第二个线程访问了,发现缓存没有,查询数据库的数据,这是数据库的数据的旧的,将旧的数据更新到缓存中出现了不一致性
可以使用延时双删的策略,即先删除缓存,在更新数据库,然后休眠500毫秒在删除缓存,但是因为第二次延时时间,不确定性很大,一般不推荐使用
先操作数据库,再删除缓存(推荐,相较于上一种出现概率很低)
因为某种原因,缓存找中数据没了,线程1访问的时候发现没有缓存,查询数据库得到旧数据,要进行写入缓存操作时 线程2进行了更新数据库,删除缓存,然后线程1更新了缓存为旧数据
缓存更新策略的最佳实践方案:
低一致性需求:使用Redis自带的内存淘汰机制
高一致性需求:主动更新,并以超时剔除作为兜底方案
读操作 Cache Aside Pattern(旁路缓存模式):
缓存未命则直接返回
缓存未命中则查询数据库,并写入缓存,设定超时时间
写操作:
先写数据库,然后再删除缓存
要确保数据库与缓存操作的原子性
修改ShopController中的业务逻辑,满足下面的需求:
根据id查询商铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间
- @Override
- public Result selectShopInfoById(Long id) {
- String key = CACHE_SHOP_KEY + id;
- //1.判断redis中是否存在该id的数据
- String str = stringRedisTemplate.opsForValue().get(key);
- if (StrUtil.isNotBlank(str)) {
- //2.存在 直接返回数据
- return Result.ok(JSONUtil.toBean(str,Shop.class));
- }
- //3.不存在 查询数据库是否存在
- Shop shop = baseMapper.selectById(id);
- if (StringUtils.isEmpty(shop)) {
- //4.不存在直接返回 404
- return Result.fail("店铺不存在");
- }
- //5.存在将数据存储在redis,然后返回
- String shopJsonStr = JSONUtil.toJsonStr(shop);
- stringRedisTemplate.opsForValue().set(key,shopJsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
- return Result.ok(shop);
- }
根据id修改店铺时,先修改数据库,再删除缓存
- @Override
- @Transactional
- public Result updateShopById(Shop shop) {
- Long id = shop.getId();
- if (id == null) {
- return Result.fail("店铺id不能为空");
- }
- //修改数据库
- baseMapper.updateById(shop);
- //删除缓存
- stringRedisTemplate.delete(CACHE_SHOP_KEY+shop.getId());
- return Result.ok();
- }
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
常见的解决方案有两种:
缓存空对象
优点:实现简单,维护方便
缺点:
额外的内存消耗 ---设置ttl过期时间
可能造成短期的不一致 ----插入数据的时候,更新缓存将null的覆盖
布隆过滤
优点:内存占用较少,没有多余key
缺点:
实现复杂
存在误判可能---布隆过滤器是居于hash算法,存在哈希碰撞问题
判断不存在的肯定不存在,判断存在的时候,可能不存在
- public Result selectShopInfoById(Long id) {
- String key = CACHE_SHOP_KEY + id;
- //1.判断redis中是否存在该id的数据
- String str = stringRedisTemplate.opsForValue().get(key);
- if (StrUtil.isNotBlank(str)) {
- //2.存在 直接返回数据
- return Result.ok(JSONUtil.toBean(str,Shop.class));
- }
- //上面判断后 执行到这句的时候,只能是null或者空字符串
- if (str != null) {
- return Result.fail("店铺不存在");
- }
-
- //3.不存在 查询数据库是否存在
- Shop shop = baseMapper.selectById(id);
- if (StringUtils.isEmpty(shop)) {
- //将null存入到redis中
- stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
- //4.不存在直接返回 404
- return Result.fail("店铺不存在");
- }
- //5.存在将数据存储在redis,然后返回
- String shopJsonStr = JSONUtil.toJsonStr(shop);
- stringRedisTemplate.opsForValue().set(key,shopJsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
- return Result.ok(shop);
- }
缓存穿透产生的原因是什么?
用户请求的数据在缓存和数据库汇总都不存在,不断发起这样的请求给数据库带来巨大压力
缓存穿透的解决方案有那些?
缓存null值
布隆过滤器
增强id的复杂度,避免被猜测id规律,然后做好数据的基础格式校验
加强用户权限校验
做好热点参数的限流
缓存雪崩是指同一时段大量的缓存key同时失效或者Redis服务五宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
给不同的key的TTL添加随机值
利用Redis集群提高服务的可用性
给缓存业务添加降级限流策略
给业务添加多级缓存
缓存击穿问题也叫作热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大冲击。
互斥锁
逻辑过期
比较
解决方案 | 优点 | 缺点 |
---|---|---|
互斥锁 | 没有额外的内存消耗 保证了一致性 实现简单 | 线程需要等待,性能受影响 可能有死锁的情况 |
逻辑过期 | 线程无序等待,性能好 | 不保证一致性 存在内存消耗 实现复杂 |
需求:根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题
- private Result cacheShopWithMutex(Long id) {
- String key = CACHE_SHOP_KEY + id;
- Shop shop = null;
- //1.判断redis中是否存在该id的数据
- String str = stringRedisTemplate.opsForValue().get(key);
- if (StrUtil.isNotBlank(str)) {
- //2.存在 直接返回数据
- return Result.ok(JSONUtil.toBean(str,Shop.class));
- }
- //上面判断后 执行到这句的时候,只能是null或者空字符串
- if (str != null) {
- return Result.fail("店铺不存在");
- }
- String lockKey = LOCK_SHOP_KEY + id;
- try {
-
- //3.不存在 先尝试获取互斥锁 利用redis中string字符串中set
- Boolean flagBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
- boolean flag = BooleanUtil.isTrue(flagBoolean);
- //4.获取锁失败
- if (!flag) {
- //获取锁失败休眠一会
- Thread.sleep(100);
- //然后进行重试 ---递归
- return selectShopInfoById(id);
- }
- //5.如果获取锁成功 查询数据库
- shop = baseMapper.selectById(id);
- //模拟重建延迟
- Thread.sleep(200);
- //如果数据库中没有数据
- if (StringUtils.isEmpty(shop)) {
- //将null存入到redis中
- stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
- //不存在直接返回 404
- return Result.fail("店铺不存在");
- }
- //.存在将数据存储在redis,然后返回
- String shopJsonStr = JSONUtil.toJsonStr(shop);
- stringRedisTemplate.opsForValue().set(key,shopJsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }finally {
- //6.释放锁
- stringRedisTemplate.delete(lockKey);
- }
- return Result.ok(shop);
- }
需求:根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题
- //弄一个线程池
- private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
- //逻辑过期
- private Result cacheShopWithLogicTTL(Long id) {
- String key = CACHE_SHOP_KEY + id;
- //1.判断redis中是否存在该id的数据
- String str = stringRedisTemplate.opsForValue().get(key);
- if (StrUtil.isBlank(str)) {
- //2.不存在 直接返回空
- return Result.fail("商铺信息为空");
- }
-
-
- //3.存在 判断缓存是否过期 逻辑时间
- RedisData redisData = JSONUtil.toBean(str, RedisData.class);
- LocalDateTime expireTime = redisData.getExpireTime();
- JSONObject data = (JSONObject)redisData.getData();
- Shop shop = JSONUtil.toBean(data, Shop.class);
- if (expireTime.isAfter(LocalDateTime.now())) {
- //4.未过期 直接返回商铺信息
- return Result.ok(shop);
- }
- //5.过期了尝试获取互斥锁
- String lockKey = LOCK_SHOP_KEY + id;
- Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
- boolean flag = BooleanUtil.isTrue(aBoolean);
- if (!flag) {
- //6.如果未获取到锁 直接返回旧数据
- return Result.ok(shop);
- }
- //7.成功获取到锁 开启一个独立线程
- CACHE_REBUILD_EXECUTOR.submit(() -> {
- //重构缓存
- try {
- saveShopToRedis(id,30L);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }finally {
- stringRedisTemplate.delete(lockKey);
- }
- //释放锁
- });
- //8.返回旧的数据
- return Result.ok(shop);
- }
参考
1.下载
2.解压
3.设置环境变量
4.path中设置
5.启动
双击打开bin中的jemter.bat
就自动启动了
6.设置中文
7.进行配置
8、输入参数,测试
基于StingRedisTemplate封装一个缓存工具类,满足下列需求:
方法1:将任意Java对象序列化为json并存储在String类型的key中,并且可设置TTL过期时间
方法2:将任意Java对象序列化为json并存储在String类型的key中,并在可以设计逻辑过期时间,用于处理缓存击穿问题
方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
封装类:CacheClient
- package com.hmdp.utils;
-
- import cn.hutool.core.util.BooleanUtil;
- import cn.hutool.core.util.StrUtil;
- import cn.hutool.json.JSONObject;
- import cn.hutool.json.JSONUtil;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.stereotype.Component;
- import org.springframework.util.StringUtils;
-
- import javax.annotation.Resource;
- import java.time.LocalDateTime;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.TimeUnit;
- import java.util.function.Function;
-
- import static com.hmdp.utils.RedisConstants.*;
-
- /**
- * @packageName: com.hmdp.utils
- * @author: winter
- * @date: 2023/4/25 8:55
- * @version: 1.0
- * @email 1660420659@qq.com
- * @description: 封装Redis工具类
- */
- @Slf4j
- @Component
- public class CacheClient {
-
- @Resource
- private StringRedisTemplate stringRedisTemplate;
-
- /**
- * 将任意Java对象序列化为json并存储在String类型的key中,
- * 并且可设置TTL过期时间
- * @param key key
- * @param obj 存储对象
- * @param timeTTL 过期时间
- * @param timeUnit 单位
- */
- public void set(String key, Object obj, Long timeTTL, TimeUnit timeUnit) {
- stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(obj),timeTTL,timeUnit);
- }
-
- /**
- * 将任意Java对象序列化为json并存储在String类型的key中,
- * 并在可以设计逻辑过期时间,用于处理缓存击穿问题
- * @param key key
- * @param obj 存储对象
- * @param timeTTL 过期时间
- * @param timeUnit 单位
- */
- public void setWithLogicalExpire(String key,Object obj,Long timeTTL, TimeUnit timeUnit) {
- RedisData redisData = new RedisData();
- redisData.setData(obj);
- redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(timeTTL)));
- stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
- }
-
- /**
- * 通过key获取字符串
- * @param key
- * @return
- */
- public String get(String key) {
- String str = stringRedisTemplate.opsForValue().get(key);
- return str;
- }
-
- /**
- * 根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
- * @param key key值
- * @param id id值
- * @param tClass 类型
- * @param function 手写方法
- * @param timeTTL 过期时间
- * @param timeUnit 时间单位
- * @param <T> 对象类型
- * @param <ID> id类型
- * @return 对象
- */
- public <T,ID> T getWithPassThrough(String key,ID id, Class<T> tClass, Function<ID,T> function,Long timeTTL, TimeUnit timeUnit) {
- //1.判断redis中是否存在该id的数据
- String str = get(key);
- if (StrUtil.isNotBlank(str)) {
- //2.存在 直接返回数据
- return JSONUtil.toBean(str,tClass);
- }
- //上面判断后 执行到这句的时候,只能是null或者空字符串
- if (str != null) {
- return null;
- }
-
- //3.不存在 查询数据库是否存在
- T shop = function.apply(id);
- if (StringUtils.isEmpty(shop)) {
- //将null存入到redis中
- set(key,"",CACHE_NULL_TTL,timeUnit);
- //4.不存在直接返回 404
- return null;
- }
- //5.存在将数据存储在redis,然后返回
- set(key,shop,timeTTL,timeUnit);
- return shop;
- }
-
-
- //弄一个线程池
- private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
-
- /**
- * 根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
- * @param key key
- * @param id id
- * @param tClass RedisDate中存储对象类型
- * @param function 方法
- * @param timeTTL 过期时间
- * @param timeUnit 过期类型
- * @param <T> 对象类型
- * @param <ID> id类型
- * @return 对象
- */
- public <T,ID> T getWithLogicalExpire(String key,ID id, Class<T> tClass, Function<ID,T> function,Long timeTTL, TimeUnit timeUnit) {
- //1.判断redis中是否存在该id的数据
- String str = get(key);
- if (StrUtil.isBlank(str)) {
- //2.不存在 直接返回空
- return null;
- }
-
-
- //3.存在 判断缓存是否过期 逻辑时间
- RedisData redisData = JSONUtil.toBean(str, RedisData.class);
- LocalDateTime expireTime = redisData.getExpireTime();
- JSONObject data = (JSONObject)redisData.getData();
- T shop = JSONUtil.toBean(data, tClass);
- if (expireTime.isAfter(LocalDateTime.now())) {
- //4.未过期 直接返回商铺信息
- return shop;
- }
- //5.过期了尝试获取互斥锁
- String lockKey = LOCK_SHOP_KEY + id;
- Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
- boolean flag = BooleanUtil.isTrue(aBoolean);
- if (!flag) {
- //6.如果未获取到锁 直接返回旧数据
- return shop;
- }
- //7.成功获取到锁 开启一个独立线程
- CACHE_REBUILD_EXECUTOR.submit(() -> {
- //重构缓存
- try {
- //查询店铺数据
- T tshop = function.apply(id);
- //模拟
- Thread.sleep(200);
- //封装逻辑过期时间
- setWithLogicalExpire(key,tshop,timeTTL,timeUnit);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }finally {
- stringRedisTemplate.delete(lockKey);
- }
- //释放锁
- });
- //8.返回旧的数据
- return shop;
- }
- }
-
测试:ShopServiceImpl
- @Override
- public Result selectShopInfoById(Long id) throws InterruptedException {
- //1.缓存穿透 存储null值解决方案
- // return cacheShopWithPassThrough(id);
- //2.缓存击穿 --互斥锁解决方案
- // return cacheShopWithMutex(id);
- //3.缓存击穿 ---逻辑过期解决方案
- // return cacheShopWithLogicTTL(id);
-
- //4.使用封装类中解决缓存穿透 存储null值办法
- // Shop shop = cacheClient.getWithPassThrough(CACHE_SHOP_KEY + id, id, Shop.class
- // ,this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
- // return Result.ok(shop);
-
- //5.使用封装类中解决缓存击穿 逻辑过期方式
- //为了测试 将逻辑过期时间设置短一点
- Shop shop = cacheClient.getWithLogicalExpire(CACHE_SHOP_KEY + id, id, Shop.class
- , this::getById, 10L, TimeUnit.SECONDS);
- return Result.ok(shop);
- }
具体代码
redis实战篇-hmdp-短信登录-商铺缓存: 存放黑马点评中redis进行短信登录、商铺查询的代码 ,包括前端后后端
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。