java——redis随笔——实战——商户查询缓存
把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(); } }