目录
- 1.缓存更新策略
- 1.1内存淘汰
- 1.2超时剔除
- 1.3主动更新
- 2.实现缓存和数据库的双写一致
- 2.1Controller
- 2.2Service
- 2.3思路讲解
- 3.解决缓存穿透问题
- 3.1出现原因
- 3.2解决方案
- 3.3代码实现
- 4.解决缓存雪崩问题
- 4.1出现原因
- 4.2解决方案
- 4.3代码实现
- 5.解决缓存击穿问题
- 5.1出现原因
- 5.2解决方案
- 5.3代码实现
- 5.3.1互斥锁(try Lock + double Check)
- 5.3.2逻辑过期
1.缓存更新策略
1.1内存淘汰
redis自动进行,当内存达到我们设置的最大内存的时候,redis就会触发淘汰机制,淘汰掉一些不重要的数据
适用情况:对数据一致性要求不高
1.2超时剔除
给redis中的key设置ttl过期时间,当达到过期时间以后会自动剔除过期的数据
适用情况:对数据一致性要求一般
1.3主动更新
在编写业务逻辑的时候,修改了数据库中的数据后,同时更新缓存中的数据
适用情况:对数据一致性要求高
2.实现缓存和数据库的双写一致
2.1Controller
/**
* 更新商铺信息
*
* @param shop 商铺数据
* @return 无
*/
@PutMapping
public Result updateShop(@RequestBody Shop shop) {
// 写入数据库
return shopService.updateShop(shop);
}
/**
* 根据id查询商铺信息
*
* @param id 商铺id
* @return 商铺详情数据
*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
return shopService.getShopById(id);
}
2.2Service
/**
* 更新商铺信息
*
* @author lichuancheng
* @date 创建时间 2024-04-28
* @since V1.0
*/
@Override
public Result updateShop(Shop shop) {
// 修改数据
Long id = shop.getId();
if (id == null) {
return Result.fail("店铺id不能为空");
}
updateById(shop);
// 删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
return Result.ok();
}
/**
* 根据id查询商铺信息
*
* @author lichuancheng
* @date 创建时间 2024-04-28
* @since V1.0
*/
@Override
public Result getShopById(Long id) {
// 1.从缓存中查询
String shopStr = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
// 2.缓存中有数据直接返回
if (!StringUtil.isNullOrEmpty(shopStr)) {
Shop shop = JSONUtil.toBean(shopStr, Shop.class);
return Result.ok(shop);
}
// 3.缓存中无数据查数据库
Shop shop = this.getById(id);
if (shop == null) {
return Result.fail("店铺不存在!");
}
// 4.数据库中数据同步到缓存
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), 30, TimeUnit.MINUTES);
// 5.返回数据库中数据
return Result.ok(shop);
}
2.3思路讲解
实现缓存和数据库双写一致,思路就是主动更新+超时剔除兜底。首先我们先操作数据库,然后删除缓存;查询的时候,先从缓存中查询,如果有值直接返回,如果没有值则去数据库中查询,没有值则返回数据为空,有值则将查到的值存入redis中,并设置过期时间,最后返回查到的数据。
3.解决缓存穿透问题
3.1出现原因
客户端请求的数据在数据库和缓存中都不存在,这样的请求都会打到数据库,因此就出现穿透现象。
3.2解决方案
1、在请求到达redis之前,加一层布隆过滤器(可能会出现误判)
2、在redi中缓存空对象
3.3代码实现
修改service层代码,缓存空对象
/**
* 根据id查询商铺信息
*
* @author lichuancheng
* @date 创建时间 2024-04-28
* @since V1.0
*/
@Override
public Result getShopById(Long id) {
// 1.从缓存中查询
String shopStr = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
// 2.缓存中有数据直接返回
if (!StringUtil.isNullOrEmpty(shopStr)) {
Shop shop = JSONUtil.toBean(shopStr, Shop.class);
return Result.ok(shop);
}
// 3.缓存中无数据查数据库
Shop shop = this.getById(id);
if (shop == null) {
// 3.1数据库中不存在数据缓存空对象
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(new Shop()), 3, TimeUnit.MINUTES);
return Result.fail("店铺不存在!");
}
// 4.数据库中数据同步到缓存
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), 30, TimeUnit.MINUTES);
// 5.返回数据库中数据
return Result.ok(shop);
}
4.解决缓存雪崩问题
4.1出现原因
redis中大量的key同一时间内失效或者redis服务器宕机,导致大量的请求直达数据库,给数据库带来巨大的压力。
4.2解决方案
1、给redis中的key设置不同的ttl过期时间,防止大量的key同时失效
2、给redis服务器设置集群,预防某个redis服务器出现宕机情况
4.3代码实现
修改service层代码,给redis中的key设置不同的ttl过期时间
/**
* 根据id查询商铺信息
*
* @author lichuancheng
* @date 创建时间 2024-04-28
* @since V1.0
*/
@Override
public Result getShopById(Long id) {
// 1.从缓存中查询
String shopStr = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
// 2.缓存中有数据直接返回
if (!StringUtil.isNullOrEmpty(shopStr)) {
Shop shop = JSONUtil.toBean(shopStr, Shop.class);
return Result.ok(shop);
}
// 3.缓存中无数据查数据库
Shop shop = this.getById(id);
if (shop == null) {
// 3.1数据库中不存在数据缓存空对象
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(new Shop()), RandomUtil.randomLong(2, 3), TimeUnit.MINUTES);
return Result.fail("店铺不存在!");
}
// 4.数据库中数据同步到缓存
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), RandomUtil.randomLong(20, 30), TimeUnit.MINUTES);
// 5.返回数据库中数据
return Result.ok(shop);
}
5.解决缓存击穿问题
5.1出现原因
在高并发场景下,redis中的热点key失效,导致无数请求直达数据库,给数据库带来巨大的冲击。
5.2解决方案
1、使用互斥锁(try Lock + double Check)
2、逻辑过期
5.3代码实现
5.3.1互斥锁(try Lock + double Check)
首先查询缓存,如果有数据直接返回,如果没有数据则去获取互斥锁(setnx),如果获取到锁,则重建缓存,返回数据库中查到的数据,如果获取不到锁,则休眠等待一段时间,重新去获取锁(获取锁之前再查一遍缓存),直到获取到锁或者从缓存中查到数据为止。
/**
* 根据id查询商铺信息
*
* @author lichuancheng
* @date 创建时间 2024-04-28
* @since V1.0
*/
@Override
public Result getShopById(Long id) {
String lockkey = "lock:shop:" + id;
Shop shop = new Shop();
// 1.从缓存中查询
String shopStr = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
// 2.缓存中有数据直接返回
if (!StringUtil.isNullOrEmpty(shopStr)) {
System.out.println(Thread.currentThread().getName()+"从缓存中获取到了数据");
shop = JSONUtil.toBean(shopStr, Shop.class);
return Result.ok(shop);
}
// 3.缓存中无数据获取互斥锁
Boolean isGetLock = stringRedisTemplate.opsForValue().setIfAbsent(lockkey, "1", 10, TimeUnit.SECONDS);
try {
if (isGetLock) {
System.out.println(Thread.currentThread().getName()+"获取到锁");
// 3.1.获取到锁查询数据库重建缓存
shop = this.getById(id);
if (shop == null) {
// 数据库中不存在数据缓存空对象
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(new Shop()), RandomUtil.randomLong(2, 3), TimeUnit.MINUTES);
return Result.fail("店铺不存在!");
}
// 数据库中数据同步到缓存
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), RandomUtil.randomLong(20, 30), TimeUnit.MINUTES);
System.out.println(Thread.currentThread().getName()+"重建了缓存");
} else {
System.out.println(Thread.currentThread().getName()+"没有获取到锁");
// 3.2.获取不到锁,休眠一段时间,重新获取
while (!isGetLock) {
Thread.sleep(1000);
return this.getShopById(id);
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 释放锁
stringRedisTemplate.delete(lockkey);
}
// 4.返回数据库中数据
System.out.println(Thread.currentThread().getName()+"从数据库中获取到了数据");
return Result.ok(shop);
}
5.3.2逻辑过期
提前缓存好热点key吗,热点key设置为永不过期,在key对应的值中存入数据和过期的时间(当前时间+多久后过期);从缓存中查到数据首先判断是否为空,如果为空则返回数据为空,如果不为空,判断缓存中的数据是否过期,如果没过期直接返回缓存中的数据,如果过期了则去获取锁,获取到锁,新开一个线程重建缓存,(主线程)然后返回过期的数据,如果没有获取到锁,则直接返回过期的数据。
/**
* 利用单元测试提前保存缓存中的热点key
*
* @author lichuancheng
* @date 创建时间 2024-04-29
* @since V1.0
*/
public void saveRedisHotKey(Long shopId, Long expireTime) {
// 1、创建存储数据和过期时间的实体类
RedisData redisData = new RedisData();
redisData.setData(getById(shopId));
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));
// 2、将实体类放入缓存,不设置过期时间
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + shopId, JSONUtil.toJsonStr(redisData));
}
/**
* 根据id查询商铺信息
*
* @author lichuancheng
* @date 创建时间 2024-04-28
* @since V1.0
*/
@Override
public Result getShopById(Long id) {
String lockkey = "lock:shop:" + id;
// 1.从缓存中查询
String shopStr = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
// 2.缓存中没有数据直接返回店铺不存在
if (StringUtil.isNullOrEmpty(shopStr)) {
System.out.println(Thread.currentThread().getName() + "查询了缓存中不存在的店铺");
return Result.fail("店铺不存在!");
}
// 3.缓存中有数据判断是否过期
RedisData redisData = JSONUtil.toBean(shopStr, RedisData.class);
LocalDateTime expireTime = redisData.getExpireTime();
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
if (!LocalDateTime.now().isAfter(expireTime)) {
// 3.1未过期直接返回缓存中的数据
System.out.println(Thread.currentThread().getName() + "返回了缓存中未过期的数据");
return Result.ok(shop);
} else {
// 3.2过期了获取锁
Boolean isGetLock = stringRedisTemplate.opsForValue().setIfAbsent(lockkey, "1", 10, TimeUnit.SECONDS);
if (isGetLock) {
System.out.println(Thread.currentThread().getName() + "获取到了锁");
// 3.2.1获取到锁新开线程重建缓存,返回过期的数据
shopServiceThreadPool.submit(() -> {
// 重建缓存(只能使用lambda表达式调用,因为lambda表达式的this指向当前类,如果使用匿名内部类,则this指向匿名内部类)
try {
this.saveRedisHotKey(id, 10l);
System.out.println(Thread.currentThread().getName() + "重建了缓存");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
stringRedisTemplate.delete(lockkey);
}
}
);
System.out.println(Thread.currentThread().getName() + "获取到锁并返回了缓存中过期的数据");
// 返回过期数据
return Result.ok(shop);
} else {
// 3.2.2没获取到锁,直接返回过期的数据
System.out.println(Thread.currentThread().getName() + "未获取到锁并返回了缓存中过期的数据");
return Result.ok(shop);
}
}
}