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

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);
            }
        }
    }
  • 14
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

java登云楼

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

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

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

打赏作者

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

抵扣说明:

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

余额充值