学习目标:
提示:学习如何利用Redis逻辑过期实现添加缓存功能解决缓存击穿
学习产出:
缓存击穿讲解图
:
解决方案:
- 采用互斥锁
- 采用逻辑过期
1. 准备pom环境
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope><version>5.1.47</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.3</version></dependency><!--hutool--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.7.17</version></dependency>
2. 配置ThreadLocal和过滤器
public class UserHolder {private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();public static void saveUser(UserDTO user){tl.set(user);}public static UserDTO getUser(){return tl.get();}public static void removeUser(){tl.remove();}
}
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Autowiredprivate StringRedisTemplate redis;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/code","/user/login","/blog/hot","/shop/**","/shop-type/**","/voucher/**").order(2);registry.addInterceptor(new RefreshTokenInterceptor(redis)).addPathPatterns("/**").order(1);}
}
---------------------------------------------
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {//controller执行之前@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.判断是否需要拦截ThreadLocalif (UserHolder.getUser()==null) {response.setStatus(401);return false;}//7.放行return true;}//渲染后返回给前台数据前@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//移除用户,避免内存泄露UserHolder.removeUser();}
}
---------------------------------------------------
@Slf4j
public class RefreshTokenInterceptor implements HandlerInterceptor {//这个对象不是由spring管理的所以不能用注解自动注入private StringRedisTemplate redis;public RefreshTokenInterceptor(StringRedisTemplate redis) {this.redis = redis;}//controller执行之前@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.获取请求头中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {return true;}//2.基于token获取redis中的用户//通过key取到hash中的map集合数据Map<Object, Object> userMap = redis.opsForHash().entries("login:token:" + token);//3.判断用户是否存在if (userMap.isEmpty()) {return true;}//5.将查询到的hash数据转为userDto对象UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);//6.存在,保存用户信息到ThreadLocal中UserHolder.saveUser(userDTO);//7.刷新token有效期redis.expire(LOGIN_USER_KEY + token, 30, TimeUnit.MINUTES);log.info("我是第一个拦截器当前拦截所有请求的用户为,线程为{},{}",UserHolder.getUser(),Thread.currentThread());//8.放行return true;}
3. RedisData接收数据
@Data
public class RedisData {private LocalDateTime expireTime;private Object data;
}
3. Controller层:负责接收请求和向下分配
@RestController
@RequestMapping("/shop")
public class ShopController {@Resourcepublic IShopService shopService;/*** 根据id查询商铺信息* @param id 商铺id* @return 商铺详情数据*/@GetMapping("/{id}")public Result queryShopById(@PathVariable("id") Long id) {return Result.ok(shopService.queryShopById(id));}
}
4. Service层:负责业务的处理逻辑
@Service
@Slf4j
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {@Resourceprivate StringRedisTemplate redis;private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(10);private boolean tryLock(String key) {Boolean setnx = redis.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MINUTES);return BooleanUtil.isTrue(setnx);}private void unlock(String key) {redis.delete(key);}public Result queryShopById(Long id) {Shop shop = queryShopWithLogicExpire(id);if (shop == null) {return Result.fail("店铺不存在");}return Result.ok(shop);}//利用逻辑过期解决缓存击穿问题private Shop queryShopWithLogicExpire(Long id) {//1.从Redis查询商品缓存String cacheShop = redis.opsForValue().get("cache:shop:" + id);//2.未命中if (ObjectUtil.isEmpty(cacheShop)) {return null;}//3.命中RedisData redisDataWithShop = JSONUtil.toBean(cacheShop, RedisData.class);LocalDateTime expireTime = redisDataWithShop.getExpireTime();JSONObject shopData = (JSONObject) redisDataWithShop.getData();Shop shop = JSONUtil.toBean(shopData, Shop.class);//3.1判断缓存是否过期if (expireTime.isAfter(LocalDateTime.now())) {//3.2未过期,返回return shop;}//4.已过期,需要重建缓存//5.缓存重建//5.1获取互斥锁String lock = "lock:shop:" + id;boolean isLock = tryLock(lock);//5.2判断互斥锁是否成功if (isLock) {// TODO: 2023/8/9 //5.3成功,开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {//缓存重构try {Shop shopItem = getById(id);RedisData redisData = new RedisData();redisData.setData(shopItem);redisData.setExpireTime(LocalDateTime.now().plusSeconds(180L));redis.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(redisData));} finally {unlock(lock);}});}//5.4返回过期商铺信息return shop;}
}