---------------------------------------------------Controller
@RestController
@RequestMapping("/shop-type")
public class ShopTypeController {
@Resource
private IShopTypeService typeService;
@GetMapping("list")
public Result queryTypeList() {
return Result.ok(typeService.queryShopTypeList());
}
}
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
return shopService.queryById(id);
}
---------------------------------------------------Service
@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public List<ShopType> queryShopTypeList() {
String cache_shop_type = stringRedisTemplate.opsForList().index(CACHE_SHOP_TYPE_KEY,0);
if (StrUtil.isNotBlank(cache_shop_type)) {
List<ShopType> arrayLists = JSONUtil.toList(cache_shop_type, ShopType.class);
return arrayLists;
}
List<ShopType> shopTypeList = query().orderByAsc("sort").list();
if (shopTypeList == null) {
return null;
}
stringRedisTemplate.opsForList().leftPush(CACHE_SHOP_TYPE_KEY,JSONUtil.toJsonStr(shopTypeList));
return shopTypeList;
}
}
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
String shopJson = stringRedisTemplate.opsForValue().get( CACHE_SHOP_KEY + id);
if (StrUtil.isNotBlank(shopJson)) {
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
Shop shop = getById(id);
if (shop == null) {
return Result.fail("店铺不存在");
}
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop));
return Result.ok(shop);
}
}
总结: 前端发送请求过来,先是去Redis
里查询,如果有则直接return,没有再去数据库里查,查到后保存一份在Redis,再return。需要注意的是: 往Redis存的时候选择合适的数据类型,取出来的时候也要转成相应的数据类型,再返回给前端。
---------------------------------------------------Controller
/**
* 更新商铺信息
* @param shop 商铺数据
* @return 无
*/
@PutMapping
public Result updateShop(@RequestBody Shop shop) {
// 写入数据库
return shopService.update(shop);
}
---------------------------------------------------Service
@Override
@Transactional
public Result update(Shop shop) {
Long id = shop.getId();
if (id == null) {
return Result.fail("店铺id不能为空");
}
updateById(shop);
stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
return Result.ok();
}
程序逻辑:先操作数据库,再删除缓存。
我们先到Redis缓存中看看之前的数据(图1),在apifox中发起更新请求,把店铺1的名字更改为1033茶餐厅(图2),在请求成功后,我们去Redis缓存中确认数据是否被删除了(图3)。再去数据库确认数据是否更新了(图4)。在用户下次访问店铺数据,就会去数据库查,再把查到的数据更新到缓存中。这样就达到了缓存与数据库的双写一致。
缓存穿透是指: 客户端请求的数据在缓存和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
解决思路:
缓存空对象:
当客户端请求的数据在缓存和数据库中都不存在,为了让相同请求不再到达数据库,就把空的值缓存到Redis中。(比如说客户端请求了一个店铺id,Redis缓存中没有,再去数据库查,也没有,那么就把这个空值null缓存到Redis中,那么当客户端再发起该请求,Redis就能命中,则该请求就不会达到数据库查询。) 这种操作会造成缓存了大量的垃圾数据,添加了额外的内存消耗,针对该问题,可以在缓存null的时候,设置有效期TTL。这种操作还会造成一个问题,当客户端第一次请求时,发现缓存和数据库都没有,我们则把空值缓存起来了,此时刚好该id有数据更新到数据库了,那么客户端再访问,是读取了Redis中的空值,造成了短期数据不一致的情况,针对该问题,可以把TTL的时间设置短一些,或者在更新数据时主动更新Redis缓存。
布隆过滤器:
当用户请求过来时,先去找布隆过滤,如果数据不存在,直接拒绝该请求,如果数据存在,放行请求,去Redis查询,Redis有则返回数据,没有则查数据库,数据库有则缓存到Redis,并把数据返回。布隆过滤器原理:可以理解为一个byte数组,里面存储二进制位。当我们要去判断数据库中数据是否存在的时候,并不是把真的数据存储在布隆过滤器,而是把数据基于某一种hash算法计算出hash值,再将该hash值转换为二进制位保存到布隆过滤器中。当我们去判断数据是否存在的时候,其实是判断对应的位置是0还是1,以此来判断数据是否存在。这种存在与否是概率统计,并不是百分之百准确,当布隆过滤器返回数据不存在的时候,数据是真的不存在,当它返回数据存在的时候,并不一定是存在的。所以有一定的穿透风险。
---------------------------------------------------Service
@Override
public Result queryById(Long id) {
String shopJson = stringRedisTemplate.opsForValue().get( CACHE_SHOP_KEY + id);
if (StrUtil.isNotBlank(shopJson)) {
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
if(shopJson != null){
return Result.fail("店铺信息不存在");
}
Shop shop = getById(id);
if (shop == null) {
//将空值写进Redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺不存在");
}
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
}
缓存雪崩是指: 同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
缓存击穿(热点key)是指: 一个被高并发访问
并且缓存重建业务较复杂
的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
需求:修改根据id查询商铺的业务。
总结: 该互斥锁是自定义的,使用Redis的setnx命令来代替,因为setnx命令在指定的 key 不存在时,为 key 设置指定的值,这种情况下等同 SET 命令。当 key存在时,什么也不做。
---------------------------------------------------Service
public Result queryById(Long id) {
//互斥锁解决缓存击穿
Shop shop = queryWithMutex(id);
if (shop == null) {
return Result.fail("店铺不存在!");
}
return Result.ok(shop);
}
private boolean tryLock(String key){
//setIfAbsent : Redis的setnx命令
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(aBoolean);
}
private void unLock(String key){
stringRedisTemplate.delete(key);
}
public Shop queryWithMutex(Long id){
String shopJson = stringRedisTemplate.opsForValue().get( CACHE_SHOP_KEY + id);
if (StrUtil.isNotBlank(shopJson)) {
return JSONUtil.toBean(shopJson, Shop.class);
}
//Redis店铺信息为 "" 则为不存在 —— 缓存穿透的问题
if(shopJson != null){
return null;
}
Shop shop = null;
try {
//获取锁 —— 缓存击穿
boolean lock = tryLock(LOCK_SHOP_KEY + id);
if(! lock){
Thread.sleep(50);
//递归调用本身,如果获取到了锁则不会进入该if
return queryWithMutex(id);
}
shop = getById(id);
//数据库中的店铺信息不存在
if (shop == null) {
//将空值写进Redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
unLock(LOCK_SHOP_KEY + id);
}
return shop;
}
需求:修改根据id查询商铺的业务。
总结: 基于逻辑过期方式解决缓存击穿问题是依靠程序员主动去做重建缓存动作,先是前端发起商铺数据查询请求,拿着参数id去Redis去查,如果未命中,直接返回null,说明该商铺id不是热点key,不是做活动的商铺。如果命中了,把json数据反序列化为对象,判断缓存是否过期,未过期直接把数据返回给前端;过期了尝试获取互斥锁,如果未获取到互斥锁,说明已经有线线程在重建缓存数据了,那么我们把查到的旧数据返回给前端;如果获取到互斥锁,开启新线程去数据库查最新的数据,加上逻辑过期时间再写入Redis中。【这里开启新线程去做缓存重建的动作是模拟高并发的情况,并发安全,但会存在数据不一致的情况】
---------------------------------------------------Service
@Override
public Result queryById(Long id) {
Shop shop = null;
//缓存穿透
// Shop shop = queryWithPassThrough(id);
//互斥锁解决缓存击穿
// shop = queryWithMutex(id);
//逻辑过期解决缓存击穿
shop = queryWithLogicalExpire(id);
if (shop == null) {
return Result.fail("店铺不存在!");
}
return Result.ok(shop);
}
public Shop queryWithLogicalExpire(Long id) {
//从Redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//未命中直接返回null,说明不是热点数据,不是做活动的商铺
if (StrUtil.isBlank(shopJson)) {
return null;
}
//命中,把json反序列化为对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
//判断是否过期,未过期直接返回
if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
return shop;
}
//已过期,重建缓存
boolean lock = tryLock(LOCK_SHOP_KEY + id);
//获取互斥锁成功,开启线程,实现缓存重建
if (lock) {
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//重建缓存
this.preheatShopDataToRedis(id, 10L);
} catch (Exception e) {
throw new RuntimeException();
} finally {
unLock(LOCK_SHOP_KEY + id);
}
});
}
//返回过期信息
return shop;
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
//把热点数据提前加载到Redis中
public void preheatShopDataToRedis(Long id, Long expireSeconds) {
Shop shop = getById(id);
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//没有设置TTL,就永远存储在Redis中,所以它的真正过期时间由逻辑时间控制。
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}