「黑马点评」二、商户查询缓存

2025 年 4 月 21 日 星期一
1

「黑马点评」二、商户查询缓存

引入 Redis 作为缓存

引入 Redis 作为 Client 与 Server 之间的缓存存在,查询先经过 Redis 缓存,没有才会访问数据库,然后回写缓存,方便下一次对该数据的查询

image.png|500

image.png|500

示例

对于 queryShop 而言,先查询缓存中有没有,没有再去查询数据库,如果数据库中有就回写到缓存中,并返回

@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
    return shopService.queryShopById(id);
}

@Override
public Result<Shop> queryShopById(Long id) {
    String key = RedisConstants.CACHE_SHOP_KEY + id;
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    if (StrUtil.isNotBlank(shopJson)) {
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return Result.success(shop);
    }
    Shop shop = shopMapper.selectById(id);
    if (shop == null) {
        return Result.error("店铺不存在");
    }
    stringRedisTemplate.opsForValue()
        .set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
    return Result.success(shop);
}

缓存更新策略

image.png|500

image.png|500

本案例中的商铺类型查询即为低一致要求,使用内存淘汰策略,所以不需要设置 TTL

对于主动更新来说,数据库、Redis 的操作顺序也有讲究,显而易见,对于主动更新会有三种方法,即:更新数据库同时更新缓存、整合数据库与缓存为一个服务保证一致性、只操作缓存等待异步持久化到数据库;第二种复杂,第三种宕机就完蛋了,选择第一种

image.png|500

image.png|500

同时,如果是更新数据库就更新 Redis,无效写的操作可能有些耽误性能,所以使用删除缓存的操作,利用回写写入缓存

image.png|500

image.png|500

那么问题就来了:究竟是先删除缓存再操作数据库、还是先操作数据库再删除缓存?

image.png|500

image.png|500

Redis 操作远比数据库操作要快,结合上图所示,先操作数据库再删除缓存的顺序中,一个请求未命中缓存且回写成功的概率非常低,因为写入缓存大概率无法比更新数据库要慢点

示例

对于 updateShop 来说,先更新数据库再删除缓存,懒加载缓存,并用 Transactional 保证数据一致性

@PutMapping
public Result<String> updateShop(@RequestBody Shop shop) {
    return shopService.updateById(shop);
}

@Override
@Transactional
public Result<String> updateById(Shop shop) {
    if (shop.getId() == null) {
        return Result.error("店铺id不能为空");
    }

    shopMapper.updateById(shop);
    stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + shop.getId());
    return Result.success("修改成功");
}

缓存穿透

问题:大量请求缓存和数据库中不存在的数据,每次都会穿过缓存打入数据库,给数据库造成巨大压力

解决方案

缓存空对象:即便是空的也会缓存,避免全部无效数据打入数据库 布隆过滤器:概率型数据结构,可能存在与绝对不存在的判断

image.png|500

image.png|500

使用方案一,缓存空对象 + TTL,因为无法接受可能 (我意思两个结合不是更好?)

image.png|500

image.png|500

示例

@Override
public Result<Shop> queryShopById(Long id) {
    String key = RedisConstants.CACHE_SHOP_KEY + id;
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    if (StrUtil.isNotBlank(shopJson)) {
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return Result.success(shop);
    }
    if (shopJson != null) {
        return Result.error("店铺不存在");
    }
    Shop shop = shopMapper.selectById(id);
    if (shop == null) {
        stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
        return Result.error("店铺不存在");
    }
    stringRedisTemplate.opsForValue()
        .set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
    return Result.success(shop);
}

缓存雪崩

问题:缓存大量同时失效,即大量缓存数据同时到期和 Redis 宕机

image.png|500

image.png|500

缓存击穿

问题:热点 KEY 问题,高并发访问且缓存重建复杂的 KEY 突然失效,大量请求会瞬间冲击数据库

image.png|500

image.png|500

解决方案

互斥锁(一致性):拿到锁才能访问数据库并重建缓存 逻辑过期(可用性):根据缓存得到的过期时间判断是否过期,过期则争夺重建缓存的线程的锁,并返回过期数据

image.png|500

image.png|500

互斥锁方案示例

image.png|500

image.png|500

高一致,一时卡顿得到新数据

@Override
public Result<Shop> queryShopById(Long id) {
    Shop shop = queryWithMutex(id);
    if (shop == null) {
        return Result.error("店铺不存在");
    }
    return Result.success(shop);
}

private Shop queryWithMutex(Long id) {
    String key = RedisConstants.CACHE_SHOP_KEY + id;
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    if (StrUtil.isNotBlank(shopJson)) {
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return shop;
    }
    if (shopJson != null) {
        return null;
    }
    // 缓存重建
    String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
    Shop shop = null;
    try {
        boolean isLock = tryLock(lockKey);
        if (!isLock) {
            Thread.sleep(50);
            return queryWithMutex(id);
        }
        shop = shopMapper.selectById(id);
        if (shop == null) {
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        stringRedisTemplate.opsForValue()
            .set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return shop;
    }catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        unlock(lockKey);
    }
}

private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(flag);
}

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

逻辑过期方案示例

image.png|500

image.png|500

高可用,数据一直存在

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

private Shop queryWithLogicalExpire(Long id) {
    String key = RedisConstants.CACHE_SHOP_KEY + id;
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    if (StrUtil.isBlank(shopJson)) {
        return null;
    }
    RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
    LocalDateTime expireTime = redisData.getExpireTime();
    Shop shop = JSONUtil.toBean((JSONObject)redisData.getData(), Shop.class);
    if (expireTime.isAfter(LocalDateTime.now())) {
        return shop;
    }
    // 重建缓存
    String lockShopKey = RedisConstants.LOCK_SHOP_KEY + id;
    boolean isLock = tryLock(lockShopKey);
    if (isLock) {
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                this.saveShopToRedis(id, RedisConstants.CACHE_SHOP_TTL);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                unlock(lockShopKey);
            }
        });
    }
    return shop;
}

private void saveShopToRedis(Long id, Long time) {
    Shop shop = shopMapper.selectById(id);
    RedisData redisData = new RedisData();
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(time));
    stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

封装缓存穿透及缓存击穿(逻辑过期)工具类

解决缓存穿透(缓存空对象方案)的 get 和 set 解决缓存击穿(逻辑过期方案)的 setWithLogicalExpire 和 getWithLogicalExpire > 善用泛型以及函数式编程

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}

@Slf4j
@Component
public class RedisClient {

    private final StringRedisTemplate stringRedisTemplate;

    private final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public RedisClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public void set(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    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)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    public <T, ID> T get(String keyPrefix, ID id, Class<T> type, Function<ID, T> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(json)) {
            return JSONUtil.toBean(json, type);
        }
        if (json != null) {
            return null;
        }
        T t = dbFallback.apply(id);
        if (t == null) {
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        this.set(key, t, time, unit);
        return t;
    }

    public <T, ID> T getWithLogicalExpire(String keyPrefix, ID id, Class<T> type, Function<ID, T> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(json)) {
            return null;
        }
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        T t = JSONUtil.toBean((JSONObject)redisData.getData(), type);
        if (expireTime.isAfter(LocalDateTime.now())) {
            return t;
        }
        // 重建缓存
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        if (isLock) {
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    T newT = dbFallback.apply(id);
                    this.setWithLogicalExpire(key, newT, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    unlock(lockKey);
                }
            });
        }
        return t;
    }

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

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

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...