黑马点评----记录2
仅作学习记录
1. 商品缓存
@Override
public Result queryById(Long id) {
//1. 从redis中查看有没有商品的缓存信息
String key = CACHE_SHOP_KEY+id;
String shopJSon = stringRedisTemplate.opsForValue().get(key);
if ( !StrUtil.isBlank(shopJSon)){
//2. 查到了,直接返回
return Result.ok(JSONUtil.toBean(shopJSon,Shop.class));
}
//3. 查不到,去数据库查
Shop shop = getById(id);
if (shop == null){
return Result.fail("该商店不存在!");
}
//4. 将查到的商品信息缓存到redis中
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
return Result.ok(shop);
}
2. 店铺类型查询缓存
这种在页面上短期内不会有改变的内容,可以直接缓存到redis中,避免高频率地访问数据库获取
// 获取店铺类型数据
@Override
public Result getTypeList() {
//1. 从redis中获取店铺类型信息--todo
String key = CACHE_SHOP_TYPE_KEY;
String shopListJSon = stringRedisTemplate.opsForValue().get(key);
//2. 判断是否能找到
if (StrUtil.isNotBlank(shopListJSon)){
//2.1 将字符串转回列表
List<ShopType> shopTypes = JSONUtil.toList(shopListJSon, ShopType.class);
return Result.ok(shopTypes);
}
//3. 没找到,从数据库找
List<ShopType> shopTypes = list();
if (shopTypes == null || shopTypes.isEmpty()){
return Result.fail("店铺类型不存在!");
}
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shopTypes),20L,TimeUnit.MINUTES);
return Result.ok(shopTypes);
}
3. 缓存更新策略
3.1 内存淘汰—redis内存淘汰机制,当内存不足时自动执行
3.2 超时剔除—在存入数据时,设置超时时间TTL
3.3 主动更新—自己编写业务逻辑,在数据库更新时,更新缓存
主动更新三个问题
- 删除缓存还是更新缓存?
更新缓存----每次更新数据库,都去更新缓存,无效更新太多,不建议
删除缓存----每次更新数据库,直接将相应的缓存都删除,只有在查询数据时,才添加缓存 - 如何保证缓存和数据库的一致性
- 如果是选择删除缓存------那是先删除缓存,后更新数据库?还是先更新数据库,再删除缓存?
建议先更新数据库,再删除缓存。
如果第一个线程是先删除了缓存A,然后去更新数据库A数据,在更新数据库数据A的同时,第二线程在redis中找不到缓存A,就会去数据库查旧数据A,并将旧数据A写入redis中,后续线程就会在redis中读取这个旧数据A,而不是第一个线程写入的新数据A
4. 给查询店铺的缓存添加超时剔除和主动更新策略
4.1 更新数据库后,再删除缓存
@Override
@Transactional
// 缓存更新策略----先更新数据库 在删除缓存 ---在下一次查询店铺信息时,会发现没有缓存而查询数据库 并写入缓存中
public Result update(Shop shop) {
Long id = shop.getId();
if (id == null){
return Result.fail("店铺id不能为空");
}
log.info("店铺信息:"+shop.toString());
// 1. 先更新数据库
updateById(shop);
// 2. 再删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
return Result.ok();
}
4.2 查询数据时,先从缓存中查,查不到再去数据库查,并将查到的数据缓存到Redis中(设置TTL)
/**
* 根据商品id查询商品信息(先从redis中查,没查到再去数据库)
* @param id
* @return
*/
@Override
public Result queryById(Long id) {
//1. 从redis中查看有没有商品的缓存信息
String key = CACHE_SHOP_KEY+id;
String shopJSon = stringRedisTemplate.opsForValue().get(key);
if ( !StrUtil.isBlank(shopJSon)){
//2. 查到了,直接返回
return Result.ok(JSONUtil.toBean(shopJSon,Shop.class));
}
//3. 查不到,去数据库查
Shop shop = getById(id);
if (shop == null){
return Result.fail("该商店不存在!");
}
//4. 将查到的商品信息缓存到redis中
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),20L,TimeUnit.MINUTES);
return Result.ok(shop);
}
总结:每当数据库的店铺A信息更新时,redis中相应的店铺A缓存就会被删除。当用户第一次请求店铺A信息时,会先去Redis中查询,没查到再去数据库查,并将查询结果缓存到Redis中,这样就实现了缓存更新。
4.3 商品缓存的缓存穿透
缓存穿透问题:假设用户请求的数据是不存在的,这个请求会先从redis中访问数据,显然redis中不可能会有该数据,因此缓存未能命中,请求就会打到数据库,由于数据库也不存在该数据,所以只会返回错误信息;假设有大量的请求都是请求不存在的数据,缓存永远不会生效,这些请求都会打到数据库,对数据库带来压力,以此来攻击数据库。
解决方法:
- 空值法:给Redis传入空值,客户端就会获取redis中的空值(同时设置TTL,否则浪费内存资源),而不会访问数据库了。
@Override
public Result queryById(Long id) {
//1. 从redis中查看有没有商品的缓存信息
String key = CACHE_SHOP_KEY+id;
String shopJSon = stringRedisTemplate.opsForValue().get(key);
if ( !StrUtil.isBlank(shopJSon)){
//2. 查到了,直接返回
return Result.ok(JSONUtil.toBean(shopJSon,Shop.class));
}
// 2.1判断查到的是不是空值,如果是就返回
if (shopJSon != null){
return Result.fail("店铺不存在!");
}
//3. 查不到,去数据库查
Shop shop = getById(id);
if (shop == null){
// 数据库也查不到,说明缓存穿透了
stringRedisTemplate.opsForValue().set(key,"", CACHE_NULL_TTL,TimeUnit.MINUTES);
return Result.fail("该商店不存在!");
}
//4. 将查到的商品信息缓存到redis中
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),20L,TimeUnit.MINUTES);
return Result.ok(shop);
}
4.4 缓存雪崩
缓存雪崩问题:是指同一时间大量的key失效或者Redis服务器宕机,导致大量请求到达数据库,给数据库带来巨大压力
4.5 缓存击穿
缓存击穿问题:也叫热点Key问题,就是一个被高并发访问且缓存重建业务比较复杂的key突然失效了,导致大量的请求会瞬间访问数据库 -----常见两种解决方法:1. 加互斥锁 2. 设置逻辑过期时间
4.5.1 互斥锁解决缓存击穿
当大量请求不能命中缓存时,这些请求都会去数据库访问数据,这个时候加锁就可以保证第一个获取锁的线程单独访问数据库相应的数据,然后将数据缓存到redis中,后续的线程也就能在redis中查到数据库,避免了对数据库带来压力。
@Override
//互斥锁解缓存击问题: 通过在请求访问数据库的时候加锁来 避免大量请求一瞬间访问数据库。
// 第一个访问完数据库的线程会将数据缓存到redis中,后续的线程就能直接在redis中获取数据了
public Result queryById(Long id) {
Shop shop = toDoqueryWithMutex(id);
if (shop == null){
return Result.fail("店铺不存在");
}
return Result.ok(shop);
}
private Shop toDoqueryWithMutex(Long id){
//1. 从redis中查看有没有商品的缓存信息
String key = CACHE_SHOP_KEY+id;
String shopJSon = stringRedisTemplate.opsForValue().get(key);
if ( !StrUtil.isBlank(shopJSon)){
//2. 查到了,直接返回
return JSONUtil.toBean(shopJSon,Shop.class);
}
// 2.1判断查到的是不是空值,如果是就返回
if (shopJSon != null){
return null;
}
//3. 查不到
//3.1 尝试获取互斥锁,来去访问数据库,避免大量请求同一时间访问数据库
String lockKey = LOCK_SHOP_KEY + id;
Shop shop = null;
try {
boolean tryLock = tryLock(lockKey);
if (!tryLock){
//3.2 获取不成功,重新获取,递归调用
Thread.sleep(50);
return toDoqueryWithMutex(id);
}
//3.3 获取锁成功---还要做一次判断,因为可能其它线程已经访问数据库,并缓存到Redis
//1. 从redis中查看有没有商品的缓存信息
if ( !StrUtil.isBlank(shopJSon)){
//2. 查到了,直接返回
return JSONUtil.toBean(shopJSon,Shop.class);
}
// 2.1判断查到的是不是空值,如果是就返回
if (shopJSon != null){
return null;
}
shop = getById(id);
if (shop == null){
// 数据库也查不到,说明缓存穿透了,给redis传入空值
stringRedisTemplate.opsForValue().set(key,"", CACHE_NULL_TTL,TimeUnit.MINUTES);
return null;
}
//4. 将查到的商品信息缓存到redis中
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),20L,TimeUnit.MINUTES);
// 模拟缓存重建的耗时
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 5. 释放锁
unLock(lockKey);
}
return shop;
}
private boolean tryLock(String key){
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, "lock", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(success);
}
private void unLock(String key){
stringRedisTemplate.delete(key);
}
4.5.2 利用逻辑过期时间解决缓存击穿问题
// 利用逻辑过期时间是解决缓存击穿问题:
// 前面的互斥锁解决方案虽然缓解了数据库的压力,但其余线程都在递归调用中,都在互相等待获取数据,没有做到高可用
// 既然缓存击穿的问题是由于TTL到期了缓存失效,那直接不设TTL,那缓存数据就永远在Redis中,但是可以设置逻辑过期时间,表明数据是否已经过期
@Override
// 解决缓存击穿问题
public Result queryById(Long id) {
//Shop shop = toDoqueryWithMutex(id);
Shop shop = toDowueryWithLogicalExpire(id);
if (shop == null){
return Result.fail("店铺不存在");
}
return Result.ok(shop);
}
// 利用逻辑过期时间是解决缓存击穿问题:
// 前面的互斥锁解决方案虽然缓解了数据库的压力,但其余线程都在递归调用中,都在互相等待获取数据,没有做到高可用
// 既然缓存击穿的问题是由于TTL到期了缓存失效,那直接不设TTL,那缓存数据就永远在Redis中,但是可以设置逻辑过期时间,表明数据是否已经过期
private Shop toDowueryWithLogicalExpire(Long id){
//1. 从redis中查看有没有商品的缓存信息
String key = CACHE_SHOP_KEY+id;
String shopJSonWithLogical = stringRedisTemplate.opsForValue().get(key);
if ( StrUtil.isBlank(shopJSonWithLogical)){
//2. 查不到(一般会查到,热点key是会一直存在Redis中的,查不到说明真的不存在)
return null;
}
//3. 查到了
//3.1 判断是否过期
RedisData redisData = JSONUtil.toBean(shopJSonWithLogical, RedisData.class);
JSONObject shopJSON = (JSONObject) redisData.getData();
Shop shop = JSONUtil.toBean(shopJSON, Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
if (LocalDateTime.now().isBefore(expireTime)){
//3.2 现在时间在逻辑过期时间之前,说明还有效,直接返回
return shop;
}
//3.3 已经过期,尝试获取锁,开启对立线程实现缓存重建
String lockKey = LOCK_SHOP_KEY + id;
boolean success = tryLock(lockKey);
if (success){
// 获取锁成功,开启线程异步执行缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
this.saveShop2Redis(id, 20L);//一般30分钟,这里设20s是为了方便测试,缓存重建
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
unLock(lockKey);
}
});
}
//3.4 无论有没有获取锁成功,也返回redis中的过期数据
return shop;
}