首页 > 其他分享 >黑马点评(2)- 商户查询缓存

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

时间:2023-06-21 19:55:48浏览次数:27  
标签:shop Shop 缓存 return 商户 黑马 key id

02 商户查询缓存

(0)前期准备

1、接口

  • 根据id查询商铺信息;
  • 更新商铺信息。
ShopController
@RestController
@RequestMapping("/shop")
public class ShopController {

    @Autowired
    private ShopService shopService;

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

    /**
     * 更新商铺信息
     * @param shop 商铺数据
     */
    @PutMapping()
    public Result updateShop(@RequestBody Shop shop) {
        // 更新数据库,删除redis中缓存
        return shopService.update(shop);
    }
}

2、数据库表

image

(1)缓存是什么

  • 缓存定义:数据交换的缓冲区(Cache),存贮数据的临时地方,一般读写性能高;
  • 缓存使用:如浏览器缓存、应用层缓存、数据库缓存、CPU缓存;
  • 缓存作用:降低后端负载,提高服务读写响应速度;
  • 缓存成本:开发成本、运维成本、一致性问题。

(2)查询商户信息添加Redis缓存

  • 查询商户信息时,先查询Redis缓存,若缓存没有则查询商户信息,若查到则将该数据添加到Redis。

image

1、key的结构

  • 商户信息:key-value:shop缓存标识 + 商户id --- 商户信息;

2、查询商户信息

ShopServiceImpl
    /**
     * 根据id查询店铺,第一版:先从redis查缓存,若无再查Mysql
     */
    public Result queryById(Long id) {
        String cacheShopKey = CACHE_SHOP_KEY + id;
        // 1. 从Redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(cacheShopKey);
        // 2. 判断缓存是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 缓存存在
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        // 3. 缓存未命中,根据id查询数据库
        Shop shop = this.getById(id);
        if (shop == null) {
            // 数据库中不存在数据,返回错误
            return Result.fail("商铺不存在");
        }
        // 4. 数据库中存在数据,将商铺信息写入redis,并设置超时时间,避免redis缓存过多数据
        stringRedisTemplate.opsForValue().set(cacheShopKey, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        // 5. 返回商铺信息
        return Result.ok(shop);
    }

(3)缓存更新策略

(4)结合Redis更新商铺信息

(5)缓存三大问题

1、缓存穿透

  • 客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
  • 解决方案:
    • 缓存空对象:对于不存在的数据也在Redis中建立缓存,值为空,并设置一个较短的TTL时间;
      • 优点:实现简单,维护方便;
      • 缺点:额外的内存消耗,可能造成短期不一致。
    • 布隆过滤:利用布隆过滤算法,在请求进入Redis之前判断是否存在,若不存在则直接拒绝请求(类比hash思想,判断某个数据的多个hash是否匹配上,匹配上则放行,存在有hash冲突,即可能放行不存在的数据);
      • 优点:内存占用较少,没有多余的key;
      • 缺点:实现复杂,存在误判可能。(可以放行一些不存在的key)

image

【1】使用空值解决缓存穿透

  • 注意:value为空值的key的过期时间应该设置较短,避免长时间的数据不一致。

image

ShopServiceImpl
    /**
     * 解决缓存击穿问题
     * 根据id查询店铺,第二版:先从redis查缓存,若无再查Mysql,mysql也无则赋空值至redis
     */
    public Result queryById(Long id) {
        Shop shop = queryWithPassThrough(id);
        if (shop == null) {
            return Result.fail("商铺不存在");
        }
        return Result.ok(shop);
    }

    /**
     * 缓存穿透(使用空值)
     */
    private Shop queryWithPassThrough(Long id) {
        String key = CACHE_SHOP_KEY + id;
        // 1. 从redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2. 判断缓存中是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 存在,则返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        // 3. 判断缓存是否是空值
        if (shopJson != null) {
            return null;
        }
        // 4. 不存在,根据id查询数据库
        Shop shop = getById(id);
        if (shop == null) {
            // 不存在,则设置空值到redis中,过期时间比正常数据短
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        // 5. 数据库中存在数据,将商铺信息写入redis,并设置超时时间,避免redis缓存过多数据
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        // 6. 返回
        return shop;
    }

2、缓存击穿

  • 热点Key问题,即热点Key(高并发访问)突然失效,无数请求该key的请求直接打到数据库上。
  • 解决方案:
    • 互斥锁,进行数据库查询从而缓存重构的过程由一个请求进行;
    • 逻辑过期,不设置Redis key的过期时间,而是自己在value中添加一个过期字段,之后若key逻辑过期则让一个请求使用互斥锁去缓存重构,其他请求直接返回旧数据。

image

【1】使用互斥锁解决缓存击穿

image

ShopServiceImpl
    /**
     * 解决缓存击穿问题
     * 根据id查询店铺,第三版:使用互斥锁
     */
    public Result queryById(Long id) {
        Shop shop = queryWithMutex(id);
        if (shop == null) {
            return Result.fail("商铺不存在");
        }
        return Result.ok(shop);
    }

    /**
     * 缓存击穿(使用互斥锁)
     */
    public Shop queryWithMutex(Long id) {
        String key = CACHE_SHOP_KEY + id;
        // 1. 查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(shopJson)) {
            // 2. 缓存命中返回数据
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        // 3. 缓存未命中
        // 3.1 缓存是否是否是空值
        if (shopJson != null) {
            return null;
        }
        // 4. 实现缓存重建
        // 4.1 尝试获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        Shop shop = null;
        // 4.2 判断是否获取成功
        boolean isLock = tryLock(lockKey);
        if (isLock) {
            try {
                // 4.4 成功,根据id查询数据库
                shop = getById(id);
                // 模拟延迟时间
                Thread.sleep(200);
                if (shop == null) {
                    // 不存在,将空值写入redis
                    stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                    return null;
                }
                // 存在,写入redis
                stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                // 7. 释放互斥锁
                unlock(lockKey);
            }
        } else {
            // 4.3 失败,休眠并等待重试
            try {
                Thread.sleep(50);
                return queryWithMutex(id);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
        //  返回
        return shop;
    }

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

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

【2】使用逻辑过期解决缓存击穿

image

  • 前提:对数据进行预热处理,redis中先保存热点商户信息;
  • 封装一个redis value的实体类,其中封装data与过期时间;
  • 缓存重构的操作新开一个线程去异步操作,返回的是缓存的旧数据,再次请求时才获得新数据。
封装逻辑过期时间实体类
/**
 * 缓存数据:添加过期时间字段
 */
@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}
热点数据预热
    /**
     * 作用1:模拟商铺热点数据预热(给使用逻辑过期策略解决缓存击穿问题做前提准备,前提是执行单元测试中的预热方法【见DianPingApplicationTest】)
     * 作用2:缓存重建
     * 该方法在ShopServiceImpl中
     */
    public void saveShopRedis(Long id, Long expireSeconds) throws InterruptedException {
        // 1. 查询店铺数据
        Shop shop = getById(id);
        if (shop == null) {
            return;
        }
        // 模拟缓存重建的时间
        Thread.sleep(200);
        // 2. 封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        // 3. 写入redis,不需要设置过期时间(有逻辑过期时间)
        String key = CACHE_SHOP_KEY + id;
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 数据预热,测试使用逻辑过期解决缓存击穿前执行
     * 该测试方法在DianPingApplicationTest中
     */
    @Test
    public void test() throws InterruptedException {
        shopService.saveShopRedis(1L, 10L);
    }
ShopServiceImpl
    /**
     * 解决缓存击穿问题
     * 根据id查询店铺,第四版本:使用逻辑删除
     */
    @Override
    public Result queryById(Long id) {
        Shop shop = queryWithLogicalExpire(id);
        if (shop == null) {
            return Result.fail("商铺不存在");
        }
        return Result.ok(shop);
    }


    /**
     * 缓存击穿(使用逻辑过期):需提前导入热点数据
     * 思路:如某时间段的热点商品,提前加入热点数据,如数据不存在,直接返回,数据存在,判断是否过期
     * 想法:万一该预热商品更新数据时,得考虑是删除缓存还是对商品信息缓存重建。
     * 此处针对的是热点数据,不是所有数据
     */
    public Shop queryWithLogicalExpire(Long id) {
        String key = CACHE_SHOP_KEY + id;
        // 1. 从redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2. 判断是否存在
        if (StrUtil.isBlank(shopJson)) {
            // redis中不存在,则返回
            return null;
        }
        // 3. 缓存命中,JSON反序列化成对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 4. 判断数据是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            //  数据未过期,返回店铺信息
            return shop;
        }
        // 5. 数据过期,更新缓存数据
        // 6. 尝试获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        // 6.1 获取成功
        if (isLock) {
            // 注意:获取锁成功应该再次检测redis缓存是否过期,做DoubleCheck,如果存在则无需重建缓存。
            // DoubleCheck:请求1刚刚把Redis数据更新为新数据并释放了锁,请求2查询到旧数据后拿到锁,就得需要再次查询Redis看是否过期,减少与数据库交互的可能次数。
            String shopJson1 = stringRedisTemplate.opsForValue().get(key);
            if (StringUtil.isBlank(shopJson1)) {
                return null;
            }
            RedisData redisData1 = JSONUtil.toBean(shopJson1, RedisData.class);
            LocalDateTime expireTime1 = redisData.getExpireTime();
            if (expireTime1.isAfter(LocalDateTime.now())) {
                return JSONUtil.toBean((JSONObject) redisData1.getData(), Shop.class);
            }
            // 7. 开启新线程进行缓存重建(使用线程池)
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 8. 查询数据库数据
                    // 9. 更新缓存数据到redis中
                    saveShopRedis(id, 20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 10. 释放锁
                    unlock(lockKey);
                }
            });
        }
        // 6.2. 获取失败,返回旧数据
        // 返回旧数据
        return shop;
    }

【3】互斥锁和逻辑过期对比

解决方案 优点 缺点
互斥锁 无额外内存消耗;保存一致性;实现简单 线程可能需要等待,性能受影响;可能有死锁风险
逻辑删除 线程无需等待,性能较好(缓存重建可新开一个线程,返回旧数据) 不保证一致性,有额外内存消耗;实现复杂

3、缓存雪崩

  • 同一

(6)缓存工具封装

03 优惠券秒杀

04 好友关注

05 达人探店

06 附近的商户

07 用户签到

08 UV统计

标签:shop,Shop,缓存,return,商户,黑马,key,id
From: https://www.cnblogs.com/hbjiaxin/p/17496973.html

相关文章

  • 【Azure Redis 缓存】应用中出现连接Redis服务错误(production.ERROR: Connection ref
    问题描述在PHP应用中,连接Redis的方法报错  RedisException(code:0):Connectionrefusedat/data/Redis/Connectors/PhpRedisConnector.phpproduction.ERROR:Connectionrefused{"exception":"[object](RedisException(code:0):Connectionrefusedat/data/Redis/......
  • 什么是Redis 雪崩、缓存"鸡"穿、缓存穿透?及出现的原因,如何预防
    Redis雪崩:在某个时间段,Redis的部分节点或者全部节点都挂掉了,导致Redis无法提供服务,请求全部转移到后端数据库,从而压垮数据库的情况。Redis雪崩通常由于某些原因导致缓存中的数据批量失效或者过期,导致后续请求都落到了数据库上,使得数据负载和请求量急剧增大,最终导致数据库的性能急......
  • 商户查询缓存
    什么是缓存缓存就是数据交换的缓冲区,是存数据的临时地方,一般读写性能较高添加Redis缓存ShopServiceImpl@OverridepublicResultqueryById(Longid){StringshopJson=stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+id);Gsongson=newGson(......
  • MySQL查询缓存的优缺点
    1.MySQL查询缓存的优缺点目录1.MySQL查询缓存的优缺点1.1.前言1.2.工作原理1.3.查询缓存对什么样的查询语句,无法缓存其记录集,大致有以下几类:1.4.查询缓存的优缺点:1.5.查询缓存的配置1.6.维护1.6.1.查询缓存区的碎片整理1.6.2.清空查询缓存的数据1.7.性能监控1.8.适合......
  • MySQL 关于缓存的 “杂七杂八”
    开头还是介绍一下群,如果感兴趣polardb,mongodb,mysql,postgresql,redis等有问题,有需求都可以加群群内有各大数据库行业大咖,CTO,可以解决你的问题。你是否可以想象如果MYSQL没有了innodb_buffer_pool是什么样子的情况,本期需要说说MYSQL的缓存,已经如何使用他更加有效用或者说性......
  • SpringBoot整合Cache缓存深入理解
    我们在上一篇的基础上继续学习。SpringBoot整合cache缓存入门一、@Caching注解@Caching注解用于在方法或者类上,同时指定多个Cache相关的注解。属性名描述cacheable用于指定@Cacheable注解put用于指定@CachePut注解evict用于指定@CacheEvict注解示例代码如下:importcom.example.mys......
  • 缓存方案之Redis
    Redis简介  Redis是RemoteDictionaryServer(Redis)的缩写,或许光听名字你就能猜出它大概是做什么的。不错,它是一个由SalvatoreSanfilippo编写的key-value存储系统,是一个使用ANSIC语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型的Key-Value数据库,并提供多种......
  • uniapp-黑马优选学习03
    01.uni数字组件:uni-number-box02.在flex布局中,如果子元素未铺满的处理>>情形:   >>处理方式:为相应的子元素,配置flex=103.商品信息的滑动删除>>组件:uni-swipe-action和 uni-swipe-action-item>>注意:options已经修改为:left-op......
  • 黑马程序员Java教程学习笔记(一)
    文章目录黑马程序员Java学习笔记Java版本Java语言的跨平台原理JRE和JDKJDK的下载和安装HelloWorld案例注释关键字常量数据类型标识符类型转换运算符字符"+"操作字符串"+"操作赋值运算符自增自减运算符关系运算符逻辑运算符三元运算符案例:两只老虎案例:三个和尚数据输入案例:三个和尚......
  • 黑马程序员Java教程学习笔记(五)
    文章目录黑马程序员Java教程学习笔记(五)日期时间:Date、SimpleDateFormat、CalendarJDK8开始新增日期API包装类正则表达式Arrays类选择排序、二分查找Lambda表达式集合概述、Collection集合的体系特点Collection常用API、遍历方式、存储自定义类型对象常见数据结构List系列集合、集......