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

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
本案例中的商铺类型查询即为低一致要求,使用内存淘汰策略,所以不需要设置 TTL
对于主动更新来说,数据库、Redis 的操作顺序也有讲究,显而易见,对于主动更新会有三种方法,即:更新数据库同时更新缓存、整合数据库与缓存为一个服务保证一致性、只操作缓存等待异步持久化到数据库;第二种复杂,第三种宕机就完蛋了,选择第一种

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

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

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
缓存击穿
问题:热点 KEY 问题,高并发访问且缓存重建复杂的 KEY 突然失效,大量请求会瞬间冲击数据库

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
高可用,数据一直存在
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);
}
}