【黑马点评】(二)缓存

(一) 什么是缓存


(二)添加商户缓存


控制层

java 复制代码
 /**
  * 根据id查询商铺信息
  * @param id 商铺id
  * @return 商铺详情数据
  */
 @GetMapping("/{id}")
 public Result queryShopById(@PathVariable("id") Long id) {
     return shopService.queryById(id);
 }

service层

java 复制代码
public interface IShopService extends IService<Shop> {

    Result queryById(Long id);
}


@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService{

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryById(Long id) {
        String key = CACHE_SHOP_KEY + id;
        // 1.从redis查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            // 3.存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        // 4.不存在,查询数据库
        Shop shop = getById(id);
        if(shop == null){
            return Result.fail("店铺不存在:!");
        }
        // 5.存在, 存入redis
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));

        return Result.ok(shop);
    }
}

测试效果:

(三)缓存练习题分析

自行实现即可,不难

(四)缓存更新策略


先删除缓存,在操作数据库的正常情况(缓存 数据库 一开始都是10)

产生不一致情况:

先操作数据库,在删除缓存的正常情况:

产生不一致情况:

方案二先操作数据库,在删除缓存 比方案一概率更低,因为需要线程1恰好查询缓存的时候缓存是失效的,同时在准备写入缓存的很短的时间需要有线程二进来更新数据库,删除缓存,需要这两个条件同时成立。

(五)实现商铺缓存与数据库的双写一致

这里更新接口字段需要去掉updatetime和createtime,因为会报错,后续在找办法解决,子需要自定义配置时间就行,多个配置类。

org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: raw timestamp (1642066339000) not allowed for java.time.LocalDateTime: need additional information such as an offset or time-zone (see class Javadocs); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: raw timestamp (1642066339000) not allowed for java.time.LocalDateTime: need additional information such as an offset or time-zone (see class Javadocs)

at [Source: (PushbackInputStream); line: 7, column: 19] (through reference chain: com.hmdp.entity.Shop["updateTime"])

当执行更新店铺时,会更新数据库,在删除缓存

当再次查询时数据库时,会自动更新缓存

修改代码如下,添加缓存时候设置过期时间,然后在更新数据库时删除缓存。

java 复制代码
    @Override
    public Result queryById(Long id) {
        String key = CACHE_SHOP_KEY + id;
        // 1.从redis查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            // 3.存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        // 4.不存在,查询数据库
        Shop shop = getById(id);
        if(shop == null){
            return Result.fail("店铺不存在:!");
        }
        // 5.存在, 存入redis
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);

        return Result.ok(shop);
    }

    @Override
    @Transactional
    public Result update(Shop shop) {
        Long id = shop.getId();
        if(id == null){
            return Result.fail("店铺id不能为空");
        }
        // 1. 更新数据库
        updateById(shop);
        // 2. 删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
        return Result.ok();
    }

(六)缓存穿透的解决思路

(七)编码解决商品查询的缓存穿透问题

java 复制代码
    @Override
    public Result queryById(Long id) {
        String key = CACHE_SHOP_KEY + id;
        // 1.从redis查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            // 3.存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        // 判断命中的是否是空值
        if(shopJson != null){
            // 返回错误信息
            return Result.fail("店铺不存在");
        }
        // 4.不存在,查询数据库
        Shop shop = getById(id);
        if(shop == null){
            // 空值写入redis 2分钟ttl
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);

            return Result.fail("店铺信息不存在!");
        }
        // 5.存在, 存入redis
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);

        return Result.ok(shop);
    }

第一次客户端发起请求查询店铺为0的数据时,应该返回店铺信息不存在,同时将空值写入缓存

此时redis中应该存储0的空值

当再次查询时,不会在请求到数据库当中,会查询缓存返回。

(八)缓存雪崩问题以及解决思路

(九)缓存击穿问题以及解决方案

(十)利用互斥锁解决缓存击穿问题


封装保存缓存穿透的代码以及封装缓存击穿的代码,实现setnx方法以及释放锁

java 复制代码
         // 缓存穿透
//        Shop shop = queryWithPassThrough(id);
        // 互斥锁解决缓存击穿
        Shop shop = queryWithMutex(id);
        if(shop == null){
            return Result.fail("店铺不存在");
        }
        // 返回
        return Result.ok(shop);
    }

    public Shop queryWithPassThrough(Long id){
        String key = CACHE_SHOP_KEY + id;
        // 1.从redis查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            // 3.存在,直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        // 判断命中的是否是空值
        if(shopJson != null){
            // 返回错误信息
            return null;
        }
        // 4.不存在,查询数据库
        Shop shop = getById(id);
        if(shop == null){
            // 空值写入redis 2分钟ttl
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        // 5.存在, 存入redis
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return shop;
    }

    public Shop queryWithMutex(Long id){
        String key = CACHE_SHOP_KEY + id;
        // 1.从redis查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            // 3.存在,直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        // 判断命中的是否是空值
        if(shopJson != null){
            // 返回错误信息
            return null;
        }

        // 4实现缓存重建 (ctrl + alt + T 快捷try-catch)
        //  4.1 获取互斥锁
        String lockKey = "lock:shop" + id;
        Shop shop = null;
        try {
            boolean isLock = tryLock(lockKey);
            //  4.2 判断是否成功
            if(!isLock){
                //  4.3 失败则休眠重试
                Thread.sleep(50);
                return queryWithMutex(id);
            }

            //  4.4 成功,根据id查询数据库
            shop = getById(id);
            // 模拟重建延时
            Thread.sleep(200);
            // 5 不存在,返回错误
            if(shop == null){
                // 空值写入redis 2分钟ttl
                stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            // 6.存在, 存入redis
            stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 7.释放互斥锁
            unlock(key);
        }
        // 8.返回
        return shop;
    }

    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }


自己测试的指标,为啥还有一些请求失败的呢,不过确实只查了一次数据库,缓存中也被更新了

(十)利用逻辑过期解决缓存击穿问题

缓存预热

java 复制代码
 public void saveShop2Redis(Long id, Long expireSeconds){
     // 1. 查询店铺数据
     Shop shop = getById(id);
     // 2. 封装逻辑过期时间
     RedisData redisData = new RedisData();
     redisData.setData(shop);
     redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
     // 3. 写入redis
     stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
 }
java 复制代码
@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}

测试插入一条数据到redis

java 复制代码
@SpringBootTest
class HmDianPingApplicationTests {

    @Resource
    private ShopServiceImpl shopService;


    @Test
    void testSaveShop(){
        shopService.saveShop2Redis(1L, 10L);
    }
}

逻辑过期代码

java 复制代码
    private final static ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public Shop queryWithLogicalExpire(Long id){
        String key = CACHE_SHOP_KEY + id;
        // 1.从redis查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if(StrUtil.isBlank(shopJson)){
            // 3.未命中
            return null;
        }
        // 命中需要判断过期时间
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();

        if(expireTime.isAfter(LocalDateTime.now())){
          // 过期时间是否在当前时间之后
            // 未过期,直接返回数据
            return shop;
        }
        // 过期,重建缓存
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        if(isLock){
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                // 重建缓存
                try {
                    this.saveShop2Redis(id, 20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    // 释放锁
                    unlock(lockKey);
                }
            });
        }
        return shop;
    }
java 复制代码
    public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {
        // 1. 查询店铺数据
        Shop shop = getById(id);
        // 模拟延迟
        Thread.sleep(200);
        // 2. 封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        // 3. 写入redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }

将数据库id为1的改为108茶餐厅,redis之前预热的是105茶餐厅。进行压测

(十二)封装Redis工具类

java 复制代码
@Slf4j
@Component
public class CacheUtil {

    private final StringRedisTemplate stringRedisTemplate;

    public CacheUtil(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData), time, unit);
    }

    public <R, ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit unit){
        String key = keyPrefix + id;
        // 1.从redis查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if(StrUtil.isNotBlank(json)){
            // 3.存在,直接返回
            return JSONUtil.toBean(json, type);
        }
        // 判断命中的是否是空值
        if(json != null){
            // 返回错误信息
            return null;
        }
        // 4.不存在,查询数据库
        R r = dbFallback.apply(id);
        if(r == null){
            // 空值写入redis 2分钟ttl
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        // 5.存在, 存入redis
        this.set(key,r,time,unit);
        return r;
    }

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

    public <R, ID> R queryWithLogicalExpire(
            String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit unit){
        String key = keyPrefix + id;
        // 1.从redis查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if(StrUtil.isBlank(json)){
            // 3.未命中
            return null;
        }
        // 命中需要判断过期时间
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        LocalDateTime expireTime = redisData.getExpireTime();

        if(expireTime.isAfter(LocalDateTime.now())){
            // 过期时间是否在当前时间之后
            // 未过期,直接返回数据
            return r;
        }
        // 过期,重建缓存
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        if(isLock){
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                // 重建缓存
                try {
                    // 查询数据库
                    R r1 = dbFallback.apply(id);
                    // 写入redis
                    this.setWithLogicalExpire(key, r1, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    // 释放锁
                    unlock(lockKey);
                }
            });
        }
        return r;
    }

    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }
}
相关推荐
SPC的存折3 小时前
1、Redis数据库基础
linux·运维·服务器·数据库·redis·缓存
身如柳絮随风扬10 小时前
Redis如何实现高效插入大量数据
数据库·redis·缓存
予早11 小时前
Redis 设置库的数量
数据库·redis·缓存
黑金IT11 小时前
vLLM本地缓存实战,重复提交直接复用不浪费算力
人工智能·缓存
Rick199313 小时前
Redis查询为什么快
数据库·redis·缓存
Rick199314 小时前
Redis 底层架构图
数据库·redis·缓存
Arva .15 小时前
Redis 数据类型
数据库·redis·缓存
笑我归无处15 小时前
Redis和数据库的数据一致性问题研究
数据库·redis·缓存
小红的布丁16 小时前
操作系统与高性能 IO:零拷贝、一次读 IO、CPU 缓存与伪共享
缓存
SPC的存折16 小时前
(自用)LNMP-Redis-Discuz5.0部署指南-openEuler24.03-测试环境
linux·运维·服务器·数据库·redis·缓存