黑马点评(二)--商户查询缓存

本文详细介绍了在Java应用中如何处理缓存更新策略,包括内存淘汰、超时剔除和主动更新,以及如何解决缓存穿透、雪崩和击穿问题,通过使用互斥锁和逻辑过期来确保数据一致性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.缓存更新策略

1.1内存淘汰

redis自动进行,当内存达到我们设置的最大内存的时候,redis就会触发淘汰机制,淘汰掉一些不重要的数据

适用情况:对数据一致性要求不高

1.2超时剔除

给redis中的key设置ttl过期时间,当达到过期时间以后会自动剔除过期的数据

适用情况:对数据一致性要求一般

1.3主动更新

在编写业务逻辑的时候,修改了数据库中的数据后,同时更新缓存中的数据

适用情况:对数据一致性要求高

2.实现缓存和数据库的双写一致

在这里插入图片描述

2.1Controller

    /**
     * 更新商铺信息
     *
     * @param shop 商铺数据
     * @return 无
     */
    @PutMapping
    public Result updateShop(@RequestBody Shop shop) {
        // 写入数据库
        return shopService.updateShop(shop);
    }
    
    /**
     * 根据id查询商铺信息
     *
     * @param id 商铺id
     * @return 商铺详情数据
     */
    @GetMapping("/{id}")
    public Result queryShopById(@PathVariable("id") Long id) {
        return shopService.getShopById(id);
    }

2.2Service

    /**
     * 更新商铺信息
     *
     * @author lichuancheng
     * @date 创建时间 2024-04-28
     * @since V1.0
     */
    @Override
    public Result updateShop(Shop shop) {
        // 修改数据
        Long id = shop.getId();
        if (id == null) {
            return Result.fail("店铺id不能为空");
        }
        updateById(shop);
        // 删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
        return Result.ok();
    }

    /**
     * 根据id查询商铺信息
     *
     * @author lichuancheng
     * @date 创建时间 2024-04-28
     * @since V1.0
     */
    @Override
    public Result getShopById(Long id) {
        // 1.从缓存中查询
        String shopStr = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        // 2.缓存中有数据直接返回
        if (!StringUtil.isNullOrEmpty(shopStr)) {
            Shop shop = JSONUtil.toBean(shopStr, Shop.class);
            return Result.ok(shop);
        }
        // 3.缓存中无数据查数据库
        Shop shop = this.getById(id);
        if (shop == null) {
            return Result.fail("店铺不存在!");
        }
        // 4.数据库中数据同步到缓存
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), 30, TimeUnit.MINUTES);
        // 5.返回数据库中数据
        return Result.ok(shop);
    }

2.3思路讲解

实现缓存和数据库双写一致,思路就是主动更新+超时剔除兜底。首先我们先操作数据库,然后删除缓存;查询的时候,先从缓存中查询,如果有值直接返回,如果没有值则去数据库中查询,没有值则返回数据为空,有值则将查到的值存入redis中,并设置过期时间,最后返回查到的数据。

3.解决缓存穿透问题

3.1出现原因

客户端请求的数据在数据库和缓存中都不存在,这样的请求都会打到数据库,因此就出现穿透现象。

3.2解决方案

1、在请求到达redis之前,加一层布隆过滤器(可能会出现误判)
2、在redi中缓存空对象

3.3代码实现

修改service层代码,缓存空对象

/**
     * 根据id查询商铺信息
     *
     * @author lichuancheng
     * @date 创建时间 2024-04-28
     * @since V1.0
     */
    @Override
    public Result getShopById(Long id) {
        // 1.从缓存中查询
        String shopStr = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        // 2.缓存中有数据直接返回
        if (!StringUtil.isNullOrEmpty(shopStr)) {
            Shop shop = JSONUtil.toBean(shopStr, Shop.class);
            return Result.ok(shop);
        }
        // 3.缓存中无数据查数据库
        Shop shop = this.getById(id);
        if (shop == null) {
            // 3.1数据库中不存在数据缓存空对象
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(new Shop()), 3, TimeUnit.MINUTES);
            return Result.fail("店铺不存在!");
        }
        // 4.数据库中数据同步到缓存
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), 30, TimeUnit.MINUTES);
        // 5.返回数据库中数据
        return Result.ok(shop);
    }

4.解决缓存雪崩问题

4.1出现原因

redis中大量的key同一时间内失效或者redis服务器宕机,导致大量的请求直达数据库,给数据库带来巨大的压力。

4.2解决方案

1、给redis中的key设置不同的ttl过期时间,防止大量的key同时失效
2、给redis服务器设置集群,预防某个redis服务器出现宕机情况

4.3代码实现

修改service层代码,给redis中的key设置不同的ttl过期时间

    /**
     * 根据id查询商铺信息
     *
     * @author lichuancheng
     * @date 创建时间 2024-04-28
     * @since V1.0
     */
    @Override
    public Result getShopById(Long id) {
        // 1.从缓存中查询
        String shopStr = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        // 2.缓存中有数据直接返回
        if (!StringUtil.isNullOrEmpty(shopStr)) {
            Shop shop = JSONUtil.toBean(shopStr, Shop.class);
            return Result.ok(shop);
        }
        // 3.缓存中无数据查数据库
        Shop shop = this.getById(id);
        if (shop == null) {
            // 3.1数据库中不存在数据缓存空对象
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(new Shop()), RandomUtil.randomLong(2, 3), TimeUnit.MINUTES);
            return Result.fail("店铺不存在!");
        }
        // 4.数据库中数据同步到缓存
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), RandomUtil.randomLong(20, 30), TimeUnit.MINUTES);
        // 5.返回数据库中数据
        return Result.ok(shop);
    }

5.解决缓存击穿问题

5.1出现原因

在高并发场景下,redis中的热点key失效,导致无数请求直达数据库,给数据库带来巨大的冲击。

5.2解决方案

1、使用互斥锁(try Lock + double Check)
2、逻辑过期

5.3代码实现

5.3.1互斥锁(try Lock + double Check)

首先查询缓存,如果有数据直接返回,如果没有数据则去获取互斥锁(setnx),如果获取到锁,则重建缓存,返回数据库中查到的数据,如果获取不到锁,则休眠等待一段时间,重新去获取锁(获取锁之前再查一遍缓存),直到获取到锁或者从缓存中查到数据为止。

/**
     * 根据id查询商铺信息
     *
     * @author lichuancheng
     * @date 创建时间 2024-04-28
     * @since V1.0
     */
    @Override
    public Result getShopById(Long id) {
        String lockkey = "lock:shop:" + id;
        Shop shop = new Shop();
        // 1.从缓存中查询
        String shopStr = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);

        // 2.缓存中有数据直接返回
        if (!StringUtil.isNullOrEmpty(shopStr)) {
            System.out.println(Thread.currentThread().getName()+"从缓存中获取到了数据");
            shop = JSONUtil.toBean(shopStr, Shop.class);
            return Result.ok(shop);
        }

        // 3.缓存中无数据获取互斥锁
        Boolean isGetLock = stringRedisTemplate.opsForValue().setIfAbsent(lockkey, "1", 10, TimeUnit.SECONDS);
        try {
            if (isGetLock) {
                System.out.println(Thread.currentThread().getName()+"获取到锁");
                // 3.1.获取到锁查询数据库重建缓存
                shop = this.getById(id);
                if (shop == null) {
                    // 数据库中不存在数据缓存空对象
                    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(new Shop()), RandomUtil.randomLong(2, 3), TimeUnit.MINUTES);
                    return Result.fail("店铺不存在!");
                }
                // 数据库中数据同步到缓存
                stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), RandomUtil.randomLong(20, 30), TimeUnit.MINUTES);
                System.out.println(Thread.currentThread().getName()+"重建了缓存");
            } else {
                System.out.println(Thread.currentThread().getName()+"没有获取到锁");
                // 3.2.获取不到锁,休眠一段时间,重新获取
                while (!isGetLock) {
                    Thread.sleep(1000);
                    return this.getShopById(id);
                }
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 释放锁
            stringRedisTemplate.delete(lockkey);
        }

        // 4.返回数据库中数据
        System.out.println(Thread.currentThread().getName()+"从数据库中获取到了数据");
        return Result.ok(shop);
    }
5.3.2逻辑过期

提前缓存好热点key吗,热点key设置为永不过期,在key对应的值中存入数据和过期的时间(当前时间+多久后过期);从缓存中查到数据首先判断是否为空,如果为空则返回数据为空,如果不为空,判断缓存中的数据是否过期,如果没过期直接返回缓存中的数据,如果过期了则去获取锁,获取到锁,新开一个线程重建缓存,(主线程)然后返回过期的数据,如果没有获取到锁,则直接返回过期的数据。

    /**
     * 利用单元测试提前保存缓存中的热点key
     *
     * @author lichuancheng
     * @date 创建时间 2024-04-29
     * @since V1.0
     */
    public void saveRedisHotKey(Long shopId, Long expireTime) {
        // 1、创建存储数据和过期时间的实体类
        RedisData redisData = new RedisData();

        redisData.setData(getById(shopId));
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));
        // 2、将实体类放入缓存,不设置过期时间
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + shopId, JSONUtil.toJsonStr(redisData));
    }

/**
     * 根据id查询商铺信息
     *
     * @author lichuancheng
     * @date 创建时间 2024-04-28
     * @since V1.0
     */
    @Override
    public Result getShopById(Long id) {
        String lockkey = "lock:shop:" + id;
        // 1.从缓存中查询
        String shopStr = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);

        // 2.缓存中没有数据直接返回店铺不存在
        if (StringUtil.isNullOrEmpty(shopStr)) {
            System.out.println(Thread.currentThread().getName() + "查询了缓存中不存在的店铺");
            return Result.fail("店铺不存在!");
        }

        // 3.缓存中有数据判断是否过期
        RedisData redisData = JSONUtil.toBean(shopStr, RedisData.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        if (!LocalDateTime.now().isAfter(expireTime)) {
            // 3.1未过期直接返回缓存中的数据
            System.out.println(Thread.currentThread().getName() + "返回了缓存中未过期的数据");
            return Result.ok(shop);
        } else {
            // 3.2过期了获取锁
            Boolean isGetLock = stringRedisTemplate.opsForValue().setIfAbsent(lockkey, "1", 10, TimeUnit.SECONDS);
            if (isGetLock) {
                System.out.println(Thread.currentThread().getName() + "获取到了锁");
                // 3.2.1获取到锁新开线程重建缓存,返回过期的数据
                shopServiceThreadPool.submit(() -> {
                            // 重建缓存(只能使用lambda表达式调用,因为lambda表达式的this指向当前类,如果使用匿名内部类,则this指向匿名内部类)
                            try {
                                this.saveRedisHotKey(id, 10l);
                                System.out.println(Thread.currentThread().getName() + "重建了缓存");
                            } catch (Exception e) {
                                throw new RuntimeException(e);
                            } finally {
                                stringRedisTemplate.delete(lockkey);
                            }
                        }
                );

                System.out.println(Thread.currentThread().getName() + "获取到锁并返回了缓存中过期的数据");
                // 返回过期数据
                return Result.ok(shop);
            } else {
                // 3.2.2没获取到锁,直接返回过期的数据
                System.out.println(Thread.currentThread().getName() + "未获取到锁并返回了缓存中过期的数据");
                return Result.ok(shop);
            }
        }
    }
### 使用 Redis 实现黑马点评商户系统缓存的最佳实践 #### 解决缓存雪崩问题 为了防止大量请求同时到达导致缓存失效而引发的数据库压力骤增现象,可以采用多层缓存架构。通过引入本地缓存(如 Caffeine)与分布式缓存(如 Redis),能够有效缓解这一状况。当发生大规模并发访问时,即使部分节点上的 Redis 缓存过期,仍然可以从其他存活实例获取所需资源;而对于完全缺失的情况,则借助于近端快速响应的小型内存存储提供临时服务[^1]。 #### 数据库与缓存的一致性维护 针对读取操作而言,优先尝试从缓存中检索目标记录。一旦发现对应条目为空缺状态,则转向底层持久化设施进行实际查询工作,并随后将获得的结果同步保存至高速缓冲区以便后续重复利用。对于更新事务来说,遵循先完成对关系型数据库内实体属性变更处理后再移除关联键值的原则,以此保障两者间尽可能高的匹配程度[^2]。 ```java // Java代码片段展示如何安全地执行上述流程 public class CacheService { private final StringRedisTemplate stringRedisTemplate; /** * 根据ID查询店铺信息 */ public Shop queryById(Long id) { // 尝试从Caffeine缓存中加载数据 ValueOperations<String, String> valueOps = stringRedisTemplate.opsForValue(); // 如果Caffeine未命中,则继续向Redis发起请求... Object resultFromCache = caffeineCache.getIfPresent(id.toString()); if (resultFromCache != null){ return JSON.parseObject(resultFromCache.toString(), Shop.class); } // ...若仍无果,则最终回退到DB层面做最后的努力 String dbResult = valueOps.get("shop:" + id); if(dbResult == null || "".equals(dbResult)){ // 查询数据库并将结果放入两处缓存之中 Shop shop = shopRepository.findById(id).orElseThrow(() -> new RuntimeException("找不到该商店")); // 设置合理的TTL以控制缓存的有效期限 int ttlInSeconds = calculateProperExpirationTimeBasedOnBusinessLogic(); valueOps.set("shop:"+id ,JSON.toJSONString(shop),ttlInSeconds, TimeUnit.SECONDS); caffeineCache.put(id.toString(), JSON.toJSONString(shop)); return shop; }else{ // 更新Caffeine缓存中的副本 caffeineCache.put(id.toString(),dbResult); return JSON.parseObject(dbResult, Shop.class); } } } ``` #### 处理缓存穿透及击穿风险 为了避免恶意攻击者故意构造不存在的商品编号造成频繁不必要的磁盘I/O开销,建议采取预填充策略预先植入一些默认占位符对象进入缓存体系内部。与此同时,还可以考虑部署布隆过滤器来初步筛选掉那些明显不合逻辑的输入参数组合,从而减少不必要的计算成本消耗。另外,在面对突发流量高峰期间可能出现单个热点商品成为瓶颈的问题上,可以通过设置带有生存周期限制的时间戳标记或是基于版本号机制来进行细粒度调控管理[^4]。 #### 锁定机制下的高效协作模式 考虑到多个客户端可能几乎同时触发相同类型的业务场景进而引起竞争条件的发生概率增加,有必要引入轻量级锁定协议确保每次只有一个进程有权修改特定范围内的共享变量内容。这里推荐使用 `SETNX` 原语配合定时自毁特性构建简易可靠的互斥锁结构体,既简单又实用[^5]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

java登云楼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值