把key做了下代码优化:
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@Override
public Result queryById(Long id) {
// key要唯一 就用id
String key = CACHE_SHOP_KEY + id;
// 1 从redis查询商铺缓存 以店铺ID为key
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2 判断是否存在
// null "" "\t\n" 都会被认为是false
if (StrUtil.isNotBlank(shopJson)) {
// 3 存在直接返回 JSON格式变回类对象
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 4 shop不存在 根据id查询数据库
Shop shopById = iShopService.getById(id);
// 5 不存在 返回错误
if (shopById == null){
return Result.fail("该商铺不存在");
}
// 6 存在 写入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shopById));
// 7 返回
return Result.ok(shopById);
}
重点:
首先修改查询业务:
- com/hmdp/service/impl/ShopServiceImpl.java
- 重写shopController的更新方法
/**
* 更新商铺信息
* @param shop 商铺数据
* @return 无
*/
@Override
// 保证原子性
@Transactional
public Result updateShop(Shop shop) {
// 获取店铺id
Long id = shop.getId();
if (id == null){
return Result.fail("店铺ID不能为空");
}
// 1 更新数据库
iShopService.updateById(shop);
// 2 删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
return Result.ok();
}
- com/hmdp/service/impl/ShopServiceImpl.java
package com.hmdp.service.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.*;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private IShopService iShopService;
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@Override
public Result queryById(Long id) {
// key要唯一 就用id
String key = CACHE_SHOP_KEY + id;
// 1 从redis查询商铺缓存 以店铺ID为key
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2 判断是否存在
// null "" "\t\n" 都会被认为是false
if (StrUtil.isNotBlank(shopJson)) {
// 3 存在直接返回 JSON格式变回类对象
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 判断是否是空值 是空值的话 就说明店铺不存在
if (shopJson == ""){
// 返回一个错误信息
return Result.fail("该店铺不存在");
}
// 4 shop不存在 根据id查询数据库 shopJson == null
Shop shopById = iShopService.getById(id);
// 5 不存在 返回错误
if (shopById == null){
// 5.1 将空值写入redis
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
// 5.2 返回错误信息
return Result.fail("该商铺不存在");
}
// 6 存在 写入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shopById)
,CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 7 返回
return Result.ok(shopById);
}
/**
* 更新商铺信息
* @param shop 商铺数据
* @return 无
*/
@Override
// 保证原子性
@Transactional
public Result updateShop(Shop shop) {
// 获取店铺id
Long id = shop.getId();
if (id == null){
return Result.fail("店铺ID不能为空");
}
// 1 更新数据库
iShopService.updateById(shop);
// 2 删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
return Result.ok();
}
}
利用互斥锁解决缓存击穿问题
操作锁的代码:
核心思路就是利用redis的setnx方法来表示获取锁,该方法含义是redis中如果没有这个key,则插入成功,返回1,
在stringRedisTemplate中返回true, 如果有这个key则插入失败,则返回0,在stringRedisTemplate返回false;
利用逻辑过期解决缓存击穿问题
代码汇总:
package com.hmdp.service.impl;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.*;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private IShopService iShopService;
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@Override
public Result queryById(Long id) {
// 缓存穿透
// Shop shop = queyWithPassThrough(id);
// 互斥锁解决缓存击穿
Shop shop = queyWithMutex(id);
// 使用逻辑过期解决缓存击穿
// 7 返回
if (shop == null){
return Result.fail("该店铺不存在");
}
return Result.ok(shop);
}
// 线程池
private static final ExecutorService CACHE_RREBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queyWithLogicalExpie(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1 从redis查询商铺缓存 以店铺ID为key
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2 判断是否存在
// null "" "\t\n" 都会被认为是false
if (StrUtil.isBlank(shopJson)) {
// 3 不存在直接返回 JSON格式变回类对象
return null;
}
// 4 shopJson不为空 则需要把json反序列化为对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
// 先强行转化为JSONObject
JSONObject data = (JSONObject)redisData.getData();
Shop shop = JSONUtil.toBean(data, Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
// 5 判断逻辑时间是否过期
if(expireTime.isAfter(LocalDateTime.now())){
// 6 未过期 则直接返回商铺信息
return shop;
}
// 7 过期 需要重建缓存
// 7.1 尝试获取互斥锁
String lockKey = LOCK_SHOP_KEY+id;
boolean flag = tryLock(lockKey);
if (flag){
// 7.3 获取互斥锁成功 开启独立线程 实现缓存重建
CACHE_RREBUILD_EXECUTOR.submit(()->{
try {
// 重建缓存
this.saveShop2Redis(id,20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 释放锁
unLock(lockKey);
}
});
}
// 7.2 获取互斥锁失败 返回店铺信息
// 不管成功还是失败 最后都是要返回shop
return shop;
}
/**
* 存储逻辑过期时间
*
* @param id
*/
public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {
// 1 查询商铺信息
Shop shop = iShopService.getById(id);
Thread.sleep(200);
// 2 封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
// 3 写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
/**
* 互斥锁解决缓存击穿
*
* @param id
* @return
*/
public Shop queyWithMutex(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1 从redis查询商铺缓存 以店铺ID为key
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2 判断是否存在
// null "" "\t\n" 都会被认为是false
if (StrUtil.isNotBlank(shopJson)) {
// 3 存在直接返回 JSON格式变回类对象
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
// 是一个空值
if (shopJson.equals("")) {
return null;
}
// 既没有数据 也没有空值 是null
// 4 实现缓存重建
// 4.1 获取互斥锁
// 锁的key和缓存的key不一样
String lockKey = "lock:shop:" + id;
Shop shop = null;
try {
boolean flag = tryLock(lockKey);
// 4.2 判断是否获取成功
if (!flag) {
// 4.3 失败 则休眠并且重试
Thread.sleep(50);
// 进行递归 要返回
return queyWithMutex(id);
}
// 4.4 成功 根据id查询数据库
shop = iShopService.getById(id);
// 模拟重建缓存 在本地查询太快了 休眠一下
Thread.sleep(200);
// 查询数据库结果 不存在 返回错误
if (shop == null) {
// 将空值写入redis 这里写入的是空值 而不是Null
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 4.5 将商铺数据写入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop)
, CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 5 最终必须释放互斥锁
unLock(lockKey);
}
// 6 返回数据
return shop;
}
/**
* 获取锁
*/
public boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
// 不能直接返回 有可能会出现空指针
// 上面是引用类型 转换为基本类型
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
* @param key
*/
public void unLock(String key) {
stringRedisTemplate.delete(key);
}
/**
* 缓存穿透代码
* 返回空或者数据本身
* @param id
* @return
*/
public Shop queyWithPassThrough(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1 从redis查询商铺缓存 以店铺ID为key
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2 判断是否存在
// null "" "\t\n" 都会被认为是false
if (StrUtil.isNotBlank(shopJson)) {
// 3 存在直接返回 JSON格式变回类对象
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
// 是一个空值
if (shopJson.equals("")) {
return null;
}
// 4 不存在 根据id查询数据库
Shop shopById = iShopService.getById(id);
// 5 查询数据库结果 不存在 返回错误
if (shopById == null) {
// 将空值写入redis 这里写入的是空值 而不是Null
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 6 存在 写入redis 设置过期时间
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shopById)
, CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 7 返回
return shopById;
}
/**
* 更新商铺信息
* @param shop 商铺数据
* @return 无
*/
@Override
// 保证原子性
@Transactional
public Result updateShop(Shop shop) {
// 获取店铺id
Long id = shop.getId();
if (id == null){
return Result.fail("店铺ID不能为空");
}
// 1 更新数据库
iShopService.updateById(shop);
// 2 删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
return Result.ok();
}
}