• 【Redis】Redis 的缓存使用技巧(商户查询缓存)


    1. 什么是缓存

    缓存(Cache) 就是数据交换的缓冲区,是存贮数据的临时地方,一般读写性能较高

    缓存的作用:

    • 降低后端负载
    • 提高读写效率,降低响应时间

    缓存的成本:

    • 数据一致性成本
    • 代码维护成本
    • 运维成本

    2. 添加 Redis 缓存

    2.1 缓存工作模型

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Jf00gGW-1660318648289)(C:/Users/bbbbbge/Pictures/接单/1660296042631.png)]

    2.2 代码实现

    前端请求说明:

    说明
    请求方式POST
    请求路径/shop/id
    请求参数id
    返回值

    后端接口实现:

    @Service
    public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    
        @Resource
        private StringRedisTemplate stringRedisTemplate;
    
        @Override
        public Result queryShopById(Long id) {
            String key = "cache:shop:" + id;
            // 1. 从 redis 查询商铺缓存
            String shopJson = stringRedisTemplate.opsForValue().get(key);
            // 2. 判断是否存在
            if(!StrUtil.isBlank(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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    3. 缓存更新策略

    3.1 缓存更新策略类型

    缓存更新策略内存淘汰超时剔除主动更新
    说明不用自己维护,利用 Redis 的内存淘汰机制,当内存不足时自动淘汰部分数据,下次查询时更新缓存。给缓存数据添加 TTL 时间,到期后自动删除缓存。下次查询时更新缓存。编写业务逻辑,在修改数据库的同时,更新缓存。
    一致性一般
    维护成本

    业务场景:

    • 低一致性:使用内存淘汰机制。
    • 高一致性:主动更新,并以超时剔除作为兜底方案。

    3.2 主动更新策略

    方式描述
    Cache Aside Pattern由缓存的调用者,在更新数据库的同时更新缓存。
    Read/Write Through Pattern缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题。
    Write Behind Caching Pattern调用者只操作缓存,由其它线程异步的将缓存持久化到数据库,保证最终一致性。

    这里推荐使用 Cache Aside Pattern,但操作缓存和数据库时有三个问题需要考虑:

    1. 删除缓存还是更新缓存?

      • 更新缓存:每次更新数据库都更新缓存,无效写操作较多。
      • 删除缓存(推荐):更新数据库时让缓存失效,查询时再更新缓存。
    2. 如何保证缓存与数据库的操作的同时成功或失败?

      • 单体系统:将缓存与数据库操作放在一个事务。
      • 分布式系统:利用 TTC 等分布式事务方案。
    3. 先操作缓存还是先操作数据库?

      • 先删除缓存,再操作数据库。(问题:当一个线程进行修改操作时,先删除了缓存,然后另一个线程读取,读取不到缓存便读取数据库,然后更新缓存,更新的是旧的数据库的值,最后第一个线程又更新数据库,导致数据库和缓存不一致。这种问题出现的概率比较高。
      • 先操作数据库,再删除缓存。(推荐。问题:当一个线程读取时正好缓存过期,那么将读取到数据库的数据,然后另一个线程进入进行修改操作,修改数据库后,将缓存删除。最后第一个线程将之前读取的数据写入缓存,就会造成数据库和缓存不一致。但读取缓存是微秒级的并又正好碰上缓存过期,因此该问题的概率很小。)

    小结:

    • 读操作:缓存命中直接返回;缓存未命中则查询数据库,并写入缓存,设定超时时间。
    • 写操作:先写数据库,然后再删缓存。要确保数据库与缓存操作的原子性。

    3.3 超时剔除和主动更新缓存实现

    后端接口实现:

    • 通过 id 查询店铺时,如果缓存未命中,则查询数据库,将数据库写入缓存,并设置超时时间。

      @Service
      public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
      
          @Resource
          private StringRedisTemplate stringRedisTemplate;
      
          @Override
          public Result queryShopById(Long id) {
              String key = "cache:shop:" + id;
              // 1. 从 redis 查询商铺缓存
              String shopJson = stringRedisTemplate.opsForValue().get(key);
              // 2. 判断是否存在
              if(!StrUtil.isBlank(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));
              stringRedisTemplate.expire(key, 30, TimeUnit.MINUTES);
              // 7. 返回
              return Result.ok(shop);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
    • 通过 id 修改店铺时,先修改数据库,再删除缓存。

      @Service
      public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
      
          @Resource
          private StringRedisTemplate stringRedisTemplate;
          
      	@Override
          @Transactional
          public Result updateShop(Shop shop) {
              Long id = shop.getId();
              if(id == null){
                  return Result.fail("店铺 id 不能为空!");
              }
              // 1. 更新数据库
              updateById(shop);
              // 2. 删除缓存
              stringRedisTemplate.delete("cache:shop:" + id);
              return Result.ok();
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20

    4. 缓存穿透

    4.1 基本介绍

    缓存穿透 是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不生效,这些请求都会打到数据库。(如果不断发起这样的请求,会给数据库带来巨大压力)

    常见解决方案:

    方案描述优点缺点
    缓存空对象如果请求的数据缓存不存在,并且数据库也不存在,数据库将给缓存更新个空对象。实现简单,维护方便。额外的内存消耗,可能造成短期的不一致。
    布隆过滤器内存占用较少,没有多余 key实现复杂,存在误判可能
    增强 id 的复杂度,避免被猜测 id 规律
    做好数据的基础格式校验
    做好热点参数的限流

    4.2 通过缓存空对象解决缓存穿透问题

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kvGD79KI-1660318648291)(C:/Users/bbbbbge/Pictures/接单/1660295985442.png)]

    代码实现:

    public Shop queryWithPassThrough(Long id) {
        String key = "cache:shop:" + id;
        // 1. 从 redis 查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2. 判断是否存在
        if (!StrUtil.isBlank(shopJson)) {
            // 3. 存在,直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        // 判断命中的是否为空值
        if (shopJson != null) {
            return null;
        }
        // 4. 不存在,根据 id 查询数据库
        Shop shop = getById(id);
        // 5. 不存在,返回错误
        if (shop == null) {
            // 将空值写入 Redis
            stringRedisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES);
            // 返回错误信息
            return null;
        }
        // 6. 存在,写入 redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
        stringRedisTemplate.expire(key, 30, TimeUnit.MINUTES);
        // 7. 返回
        return shop;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    5. 缓存雪崩

    缓存雪崩 是指在同一时段大量的缓存 key 同时失效或者 Redis 服务宕机,导致大量请求到达数据库,带来巨大压力。

    解决方案:

    • 给不同的 key 的 TTL 添加随机值
    • 利用 Redis 集群提高服务的可用性
    • 给缓存业务添加降级限流策略
    • 给业务添加多级缓存

    6. 缓存击穿

    6.1 基本介绍

    缓存击穿 问题也叫热点 key 问题,就是一个被高并发访问并且缓存重建业务较复杂的 key 突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

    常见解决方案:

    解决方案优点缺点
    互斥锁没有额外的内存消耗;保证一致性;实现简单线程需要等待,性能受影响;可能有死锁风险
    逻辑过期线程无需等待,性能较好不保证一致性;有额外内存消耗;实现复杂

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V9IFFFeF-1660318648291)(C:/Users/bbbbbge/Pictures/接单/1660299860737.png)]

    6.2 基于互斥锁方式解决缓存击穿问题

    这里通过 Redis 中的 SETNX 命令去自定义一个互斥锁,通过 del 命令去删除这个 key 来解锁。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j8RXzvPs-1660318648292)(C:/Users/bbbbbge/Pictures/接单/1660304774893.png)]

    自定义尝试获取锁和释放锁实现

    // 尝试获取锁
    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    业务逻辑实现:

    @Override
    public Result queryShopById(Long id) {
        // 互斥锁缓存击穿
        Shop shop = queryWithMutex(id);
        if(shop == null){
            Result.fail("店铺不存在!");
        }
        // 7. 返回
        return Result.ok(shop);
    }
    
    // 互斥锁存击穿
    public Shop queryWithMutex(Long id){
        String key = "cache:shop:" + id;
        // 1. 从 redis 查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2. 判断是否存在
        if(!StrUtil.isBlank(shopJson)) {
            // 3. 存在,直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        // 判断命中的是否为空值
        if(shopJson != null ){
            return null;
        }
    
        // 4. 实现缓存重建
        // 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);
            // 5. 不存在,返回错误
            if(shop == null){
                // 将空值写入 Redis
                stringRedisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES);
                // 返回错误信息
                return null;
            }
            // 6. 存在,写入 redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
            stringRedisTemplate.expire(key, 30, TimeUnit.MINUTES);
            // 7. 释放互斥锁
            unlock(lockKey);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        // 8. 返回
        return shop;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    6.3 基于逻辑过期方式解决缓存击穿问题

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DHyMmVfv-1660318648292)(C:/Users/bbbbbge/Pictures/接单/1660310273909.png)]

    在不修改原有实体类的情况下,可以新定义一个类用来保存原有的数据并添加逻辑过期时间

    @Data
    public class RedisData {
        // 逻辑过期时间
        private LocalDateTime expireTime;
        // 要存储到 Redis 中的数据
        private Object data;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    将店铺数据和逻辑过期时间封装并保存到 Redis 中

    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:" + id, JSONUtil.toJsonStr(redisData));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    业务实现:

    // 定义线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    
    // 基于逻辑过期缓存穿透
    public Shop queryWithLogicalExpire(Long id) {
        String key = "cache:shop:" + id;
        // 1. 从 redis 查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2. 判断是否存在
        if (StrUtil.isBlank(shopJson)) {
            // 3. 不存在,直接返回
            return null;
        }
        // 4. 命中,需要吧 json 反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 因为 data 类型为 Object,并不知道为 Shop,这里会转成 JSONObject
        JSONObject data = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        // 5. 判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 5.1 未过期,直接返回店铺信息
            return shop;
        }
        // 5.2 已过期,需要缓存重建
        // 6. 缓存重建
        // 6.1 获取互斥锁
        String lockKey = "lock:shop:" + id;
        boolean isLock = tryLock(lockKey);
        // 6.2 判断是否获取锁成功
        if (isLock) {
            // 6.3 成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                // 重建缓存
                this.saveShop2Redis(id, 1800L);
                // 释放锁
                unlock(lockKey);
            });
        }
        // 6.4 返回过期的店铺信息
        return shop;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    7. 缓存工具封装

    接下来将对以下四个方法进行封装:

    1. 将任意 Java 对象序列化为 JSON 并存储在 String 类型的 key 中,并可以设置 TTL 过期时间

    2. 将任意 Java 对象序列化为 JSON 并存储在 String 类型的 key 中,并可以设置逻辑过期时间,用于处理缓存击穿问题

    3. 根据指定的 key 查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题

    4. 根据指定的 key 查询缓存,并反序列化为指定类型,利用逻辑过期解决缓存击穿问题

    @Slf4j
    @Component
    public class CacheClient {
    
        private final StringRedisTemplate stringRedisTemplate;
    
        public CacheClient(StringRedisTemplate stringRedisTemplate) {
            this.stringRedisTemplate = stringRedisTemplate;
        }
    
        // 将任意 Java 对象序列化为 JSON 并存储在 String 类型的 key 中,并可以设置 TTL 过期时间
        public void set(String key, Object value, Long time, TimeUnit unit) {
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
        }
    
        // 将任意 Java 对象序列化为 JSON 并存储在 String 类型的 key 中,并可以设置逻辑过期时间,用于处理缓存击穿问题
        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)));
            // 写入 redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
        }
    
        // 根据指定的 key 查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
        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.isBlank(json)) {
                // 3. 存在,直接返回
                return JSONUtil.toBean(json, type);
            }
            // 判断命中的是否为空值
            if (json != null) {
                return null;
            }
            // 4. 不存在,根据 id 查询数据库
            R r = dbFallback.apply(id);
            // 5. 不存在,返回错误
            if (r == null) {
                // 将空值写入 Redis
                stringRedisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES);
                // 返回错误信息
                return null;
            }
            // 6. 存在,写入 redis
            this.set(key, r, time, unit);
            // 7. 返回
            return r;
        }
    
        // 根据指定的 key 查询缓存,并反序列化为指定类型,利用逻辑过期解决缓存击穿问题
        // 定义线程池
        private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    
        // 尝试获取锁
        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);
        }
    
        // 基于逻辑过期缓存穿透
        public <R, ID> R queryWithLogicalExpire(
                String keyPrefix1, String keyPrefix2, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
            String key = keyPrefix1 + id;
            // 1. 从 redis 查询商铺缓存
            String json = stringRedisTemplate.opsForValue().get(key);
            // 2. 判断是否存在
            if (StrUtil.isBlank(json)) {
                // 3. 不存在,直接返回
                return null;
            }
            // 4. 命中,需要吧 json 反序列化为对象
            RedisData redisData = JSONUtil.toBean(json, RedisData.class);
            LocalDateTime expireTime = redisData.getExpireTime();
            // 因为 data 类型为 Object,并不知道为 Shop,这里会转成 JSONObject
            JSONObject data = (JSONObject) redisData.getData();
            R r = JSONUtil.toBean(data, type);
            // 5. 判断是否过期
            if (expireTime.isAfter(LocalDateTime.now())) {
                // 5.1 未过期,直接返回店铺信息
                return r;
            }
            // 5.2 已过期,需要缓存重建
            // 6. 缓存重建
            // 6.1 获取互斥锁
            String lockKey = keyPrefix2 + id;
            boolean isLock = tryLock(lockKey);
            // 6.2 判断是否获取锁成功
            if (isLock) {
                // 6.3 成功,开启独立线程,实现缓存重建
                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);
                    }
                });
            }
            // 6.4 返回过期的店铺信息
            return r;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
  • 相关阅读:
    写着简单跑着又快的数据库语言SPL
    JAVA后端开发面试基础知识(七)——多线程
    手机喊话应用实现思路
    【JavaScript复习十一】数组内置对象方法一
    探索贪心算法:解决优化问题的高效策略
    第八章 时序检查(中)
    一条慢SQL拖死整个系统
    算法题练习——NC93 设计LRU缓存结构
    前缀++与后缀++
    百度Apollo自定义安装第三方库(以libtorch为例)
  • 原文地址:https://blog.csdn.net/weixin_51367845/article/details/126313136