黑马redis实战-商户查询缓存

商户查询缓存

本文的主题是商铺缓存。主要包括:添加商铺缓存到 redis,实现缓存和数据库的一致,
redis 缓存面临的三个问题的解决:缓存穿透,缓存雪崩,缓存击穿

实现效果:
在这里插入图片描述

1. 添加商户缓存

需求分析:根据 id 查询商铺,若 redis 中有商铺缓存,直接返回。否则查询数据库并将商铺信息缓存到 redis 中

代码实现:
① Controller 层,获取url中的请求参数 “id”

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

② redis 查询缓存,若缓存未命中,查询数据库,并保存到 redis 中

\\ ShopServiceImpl.java
@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.不存在,根据id查询数据库
        Shop shop = getById(id);
        // 5.不存在,返回错误
        if(shop == null){
   
   
            return Result.fail("店铺不存在!");
        }
        // 6.存在,写入 redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
        // 7.返回
        return Result.ok(shop);
    }
}

问题记录:前端商铺显示 NaN
① return Result.ok() ,里面没有返回 shop

② redis 中有缓存记录,但是给店铺添加了过期时间,然后缓存没有清理掉,所以过期不显示。删除缓存刷新即可

2. 缓存与数据库双写一致

缓存更新策略:
① 内存淘汰:一致性差,利用 redis 的内存淘汰机制,当内存不足时自动淘汰部分数据,下次查询时更新缓存

② 超时剔除:给缓存数据添加 TTL 时间,到期后自动删除缓存,下次查询时更新缓存

③ 主动更新:编写业务逻辑,在修改数据库的同时,更新缓存

采用主动更新方式:修改数据库并更新缓存

问题:先修改数据库再更新缓存,还是先更新缓存再修改数据库?
在这里插入图片描述
根据上面添加商铺缓存的逻辑,若线程查询redis 缓存未命中,即执行 查询数据库并写入 redis 缓存的逻辑

方案一:线程1删除缓存之后,导致缓存失效. 由于操作数据库是涉及IO,不如redis直接操作内存,因此大概率情况下线程1在更新数据库期间会有线程查询缓存. 可以看到,线程2在线程1更新数据库的时候,查询缓存并拿到了数据库中的快照版本,并将过期的值写入缓存。最后的结果是 redis 中 a = 10,而 mysql 中 a = 20,缓存和数据库不一致

方案二:线程1 先更新数据库再删除缓存,之后线程2由于缓存未命中,查询到数据库中更新的值并写入缓存. 结果上,数据库和缓存数据库一致

问题:更新数据库和删除缓存之间有线程请求业务呢?
在这里插入图片描述
线程取到过期缓存:在线程 1 更新数据库还未来得及删除缓存前,线程2查询缓存,由于缓存还未删除,线程2拿到旧数据。但之后的线程由于缓存未命中,会查询数据库并更新缓存

多线程操作数据库:这种情况也会发生缓存和数据库不一致。接着左侧线程3缓存未命中去查询数据库的情况,这时有线程更新数据库,则会出现对缓存更新丢失的情况.即线程1 写入缓存的操作覆盖了线程2 写入缓存的操作。这种情况发生的前提是:更新数据库和删除缓存之间有线程查询缓存导致缓存被删除【即左图】,其次是 查询数据库和写入缓存之前有线程查询数据库并写入了缓存。这种情况会发生但是因为有条件限制,所以概率较低

综合来看,方案二虽然也会发生缓存过期以及缓存与数据库不一致的情况,但是出错概率要小一点

因此选择:先更新数据库,再删除缓存

更新方式:利用 postman 修改店铺信息并发送给服务器
在这里插入图片描述
代码实现:

// ShopController.java
/**
 * 更新商铺信息
 * @param shop 商铺数据
 * @return 无
 */
@PutMapping
public Result updateShop(@RequestBody Shop shop) {
   
   
    // 写入数据库
    return shopService.update(shop);
}
### 使用 Redis 实现店铺类型数据的缓存 为了实现店铺类型的高效缓存并提升应用性能,可以采用以下策略: #### 方法设计 定义 `saveShopTypeToRedis` 函数用于保存店铺类型到 Redis 中,并设置合理的逻辑过期时间。此操作不仅能够减少数据库的压力,还能确保即使在高并发场景下也能提供稳定的服务。 ```java public void saveShopTypeToRedis(Long typeId, Long seconds) { // 查询店铺类型数据 ShopType shopTypeById = getShopTypeById(typeId); // 封装要存储的数据结构 RedisData redisData = new RedisData(); redisData.setData(shopTypeById); // 设置逻辑上的过期时间戳 LocalDateTime expireTime = LocalDateTime.now().plusSeconds(seconds); redisData.setExpireTime(expireTime); // 序列化对象为JSON字符串形式存入Redis String jsonStr = JSONUtil.toJsonStr(redisData); stringRedisTemplate.opsForValue().set( RedisConstants.CACHE_SHOP_TYPE_KEY + typeId, jsonStr ); } ``` 上述代码片段展示了如何将特定ID对应的店铺类型信息持久化至Redis中[^1]。 对于读取操作,则需先尝试从Redis获取对应键值;如果命中则解析返回给前端;未命中的情况下再回源查询真实库表记录,并更新最新版本到缓存层以便后续访问能快速响应。 ```java public ShopType queryWithLogicalExpire(Long id){ // 构造缓存Key String key = RedisConstants.CACHE_SHOP_TYPE_KEY + id; // 优先级最高的是直接从本地内存或更靠近用户的节点处取得所需资源 Object resultObj = localCache.getIfPresent(key); if (resultObj != null && !isExpired(resultObj)){ return ((RedisData<ShopType>)resultObj).getData(); } // 获取缓存中的json串 String typeJson = stringRedisTemplate.opsForValue().get(key); if(StrUtil.isBlank(typeJson)) { // 缓存穿透处理... return null; } // 反序列化成对象 RedisData redisData = JSONUtil.toBean(typeJson, RedisData.class); JSONObject data = JSONUtil.parseObj(redisData.getData()); ShopType shopType = BeanUtil.fillBeanWithData(data, new ShopType(), true); // 判断是否已过期 LocalDateTime expireTime = redisData.getExpireTime(); if(expireTime.isAfter(LocalDateTime.now())) { // 如果没过期就正常返回商品详情 return shopType; } else{ // 已经过期了,删除旧版缓存条目 stringRedisTemplate.delete(key); // 背景异步重构新缓存项 rebuildCacheOnBackground(id,key); // 返回null 或者默认值/降级方案 return null; } } private boolean isExpired(Object obj){ try{ RedisData<?> rd = (RedisData<?>)obj; return rd.getExpireTime().isBefore(LocalDateTime.now()); }catch(Exception e){ log.error("判断缓存是否过期失败",e); return false; } } ``` 这段Java程序实现了带有逻辑过期机制的商品详情页缓存功能,在保证高性能的同时也兼顾了一定程度上的一致性和安全性。 当遇到缓存雪崩风险时,可以通过引入随机延迟、限流措施以及分布式锁等方式来缓解突发流量冲击带来的负面影响[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值