

@RestController
@RequestMapping("/shop")
public class ShopController {
@Resource
public IShopService shopService;
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
return shopService.queryById(id);
}
}
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
// 1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在,则返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 4.不存在,则根据id查数据库
Shop shop = getById(id);
if (shop == null) {
// 5.商户不存在,返回错误
return Result.fail("店铺不存在!");
}
// 6.商户存在,写入redis
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop));
//7.返回shop
return Result.ok(shop);
}
}




修改ShopController、ShopServiceImpl中的业务逻辑,满足下面的需求:
@RestController
@RequestMapping("/shop")
public class ShopController {
@Resource
public IShopService shopService;
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
return shopService.queryById(id);
}
/**
* 更新商铺信息
* @param shop 商铺数据
* @return 无
*/
@PutMapping
public Result updateShop(@RequestBody Shop shop) {
// 更新商铺信息,并写入数据库
return shopService.update(shop);
}
}
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
// 1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在,则返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 4.不存在,则根据id查数据库
Shop shop = getById(id);
if (shop == null) {
// 5.商户不存在,返回错误
return Result.fail("店铺不存在!");
}
// 6.商户存在,写入redis
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
//7.返回shop
return Result.ok(shop);
}
@Override
@Transactional
public Result update(Shop shop) {
Long id = shop.getId();
if (id == null) {
return Result.fail("店铺id不能为空");
}
// 1.更新数据库
updateById(shop);
// 2.删除缓存
stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + id);
return Result.ok();
}
}
缓存更新策略的最佳实践方案:
(1)低一致性需求:使用Redis自带的内存淘汰机制
(2)高一致性需求:主动更新,并以超时剔除作为兜底方案



修改ShopServiceImpl中queryById方法,解决缓存穿透问题:
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
// 1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在,则返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 如果命中的是空值,则返回错误信息,防止缓存穿透
if ("".equals(shopJson)) {
return Result.fail("店铺不存在!");
}
// 4.不存在,则根据id查数据库
Shop shop = getById(id);
if (shop == null) {
// 5.商户不存在,返回错误
// 将空值写入redis,并设置短暂过期时间,解决缓存穿透问题
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺不存在!");
}
// 6.商户存在,写入redis
// 设置过期时间
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
//7.返回shop
return Result.ok(shop);
}
}

解决方案:





修改ShopServiceImpl中queryById方法,获取互斥锁、释放互斥锁,解决缓存击穿问题:
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
// 互斥锁解决缓存击穿
Shop shop = queryWithMutex(id);
if (shop == null) {
return Result.fail("店铺不存在!");
}
return Result.ok(shop);
}
public Shop queryWithMutex(Long id) {
String key = RedisConstants.CACHE_SHOP_KEY + id;
// 1、从redis中查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2、判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3、存在,直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
// 如果命中的是空值,则返回错误信息,防止缓存穿透
if ("".equals(shopJson)) {
return null;
}
// 4.实现缓存重构
// 4.1 获取互斥锁
String lockKey = "lock:shop:" + id;
Shop shop = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2 判断否获取成功
if (!isLock){
// 4.3 失败,则休眠重试
Thread.sleep(50);
// 递归重试
return queryWithMutex(id);
}
// 4.4 成功,根据id查询数据库
shop = getById(id);
// 模拟业务延时
Thread.sleep(200);
// 5.不存在,返回错误
if(shop == null){
// 将空值写入redis,并设置短暂过期时间,解决缓存穿透问题
stringRedisTemplate.opsForValue().set(key,"", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.写入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
}catch (Exception e){
throw new RuntimeException(e);
}finally {
// 7.释放互斥锁
unlock(lockKey);
}
return shop;
}
/**
* 自定义互斥锁,利用redis的setnx方法来表示获取锁
* @param key
* @return
*/
private boolean tryLock(String key) {
return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS));
}
/**
* 释放互斥锁
* @param key
*/
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
}
使用Jmeter工具压测:
压测时需要注意:



@Data
public class RedisData {
// 逻辑过期时间
private LocalDateTime expireTime;
// 缓存预热对象
private Object data;
}
在ShopServiceImpl中新增该方法:
/**
* 重建缓存
* @param id
* @param expireSeconds
*/
public void saveShop2Redis(Long id, Long expireSeconds)throws InterruptedException {
// 1.查询店铺数据
Shop shop = getById(id);
// 模拟业务延时
Thread.sleep(200);
// 2.封装缓存预热对象
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
// 3.写入redis
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
测试方法:
@SpringBootTest
class HmDianPingApplicationTests {
@Autowired
private ShopServiceImpl shopService;
@Test
void testSaveShop() throws InterruptedException {
// 将id为1的对象作为热点数据,逻辑过期时间设置20s
shopService.saveShop2Redis(1L, 20L);
}
}
热点数据存入成功:

修改ShopServiceImpl中queryById方法:
@Override
public Result queryById(Long id) {
// 互斥锁解决缓存击穿
// Shop shop = queryWithMutex(id);
// 逻辑过期解决缓存击穿
Shop shop = queryWithLogicalExpire(id);
if (shop == null) {
return Result.fail("店铺不存在!");
}
return Result.ok(shop);
}
/**
* 逻辑过期解决缓存击穿
* @param id
* @return
*/
public Shop queryWithLogicalExpire(Long id) {
String key = RedisConstants.CACHE_SHOP_KEY + id;
// 1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isBlank(json)) {
// 3.存在,直接返回
return null;
}
// 4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
// 先转化为JSONObject,再将JSONObject转化为需要的对象
JSONObject JSONData = (JSONObject) redisData.getData();
Shop shop = JSONUtil.toBean(JSONData, Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判断是否过期
if(expireTime.isAfter(LocalDateTime.now())) {
// 5.1 未过期,直接返回店铺信息
return shop;
}
// 5.2 已过期,需要缓存重建
// 6. 缓存重建
// 6.1 获取互斥锁
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2 判断是否获取锁成功
if (isLock){
// 6.3 成功,开启新线程,重建缓存
CACHE_REBUILD_EXECUTOR.submit(() -> {
try{
// 重建缓存
this.saveShop2Redis(id,20L);
}catch (Exception e){
throw new RuntimeException(e);
}finally {
unlock(lockKey);
}
});
}
// 6.4 返回过期的商铺信息
return shop;
}


测试结果:

