【Redis实战】击穿+雪崩+穿透

news2024/12/22 20:41:36

架构

image.png

短信登录

基于session实现登录

流程图

image.png

代码实现
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    /**
     * session用户key
     */
    public static final String USER_CONSTANT = "user";

    @Override
    public Result sendCode(String phone, HttpSession session) {
        //校验手机号码
        boolean phoneInvalid = RegexUtils.isPhoneInvalid(phone);
        if (phoneInvalid) {
            return Result.fail("手机号码格式错误!");
        }
        //生成6位数的验证码
        String code = RandomUtil.randomNumbers(6);
        session.setAttribute("code", code);
        //发送验证码
        log.info("send code success,code={}", code);
        return Result.ok();
    }

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //校验手机号码
        if (Objects.isNull(loginForm)) {
            return Result.fail("参数为空!");
        }
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号码格式错误!");
        }
        //验证码校验
        String code = (String) session.getAttribute("code");
        if (StringUtils.isBlank(code) || !StringUtils.equals(code, loginForm.getCode())) {
            return Result.fail("验证码错误!");
        }
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getPhone, phone);
        User user = getOne(wrapper);
        if (!Objects.nonNull(user)) {
            //注册新用户
            user = getNewUserByPhone(phone);
            save(user);
        }
        session.setAttribute(USER_CONSTANT, BeanUtil.copyProperties(user, UserDTO.class));
        return Result.ok();
    }

    /**
     * 根据手机号码创建新用户
     *
     * @param phone 手机号码
     * @return
     */
    private User getNewUserByPhone(String phone) {
        User user = new User();
        user.setCreateTime(LocalDateTime.now());
        user.setPhone(phone);
        user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
        user.setUpdateTime(LocalDateTime.now());
        return user;
    }
}
集群session共享问题

image.png

session数据拷贝可以解决这个问题,但是多台tomcat之间存储相同的数据会浪费内存空间,拷贝会有数据延迟。
session每个浏览器有不同的code,tomcat里保存里很多code。

基于Redis实现session登录

验证码流程图

image.png

代码实现
    public Result sendCode(String phone, HttpSession session) {
        //校验手机号码
        boolean phoneInvalid = RegexUtils.isPhoneInvalid(phone);
        if (phoneInvalid) {
            return Result.fail("手机号码格式错误!");
        }
        //生成6位数的验证码
        String code = RandomUtil.randomNumbers(6);
        //保存验证码到redis
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.SECONDS);
        //发送验证码
        log.info("send code success,code={}", code);
        return Result.ok();
    }
校验流程图

image.png

代码实现

登录

    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //校验手机号码
        if (Objects.isNull(loginForm)) {
            return Result.fail("参数为空!");
        }
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号码格式错误!");
        }
        //验证码校验
        String code = (String) session.getAttribute("code");
        if (StringUtils.isBlank(code) || !StringUtils.equals(code, loginForm.getCode())) {
            return Result.fail("验证码错误!");
        }
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getPhone, phone);
        User user = getOne(wrapper);
        if (!Objects.nonNull(user)) {
            //注册新用户
            user = getNewUserByPhone(phone);
            save(user);
        }
        session.setAttribute(USER_CONSTANT, BeanUtil.copyProperties(user, UserDTO.class));
        return Result.ok();
}

登录拦截器

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.http.HttpStatus;
import com.hmdp.dto.UserDTO;
import com.hmdp.utils.UserHolder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

/**
 * 登录拦截器
 *
 * @author zhangzengxiu
 * @date 2023/10/6
 */
@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取请求头的token
        String token = request.getHeader("authorization");
        if (StringUtils.isBlank(token)) {
            response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
            return false;
        }
        //获取redis中的token
        String tokenKey = LOGIN_USER_KEY + token;
        Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(tokenKey);
        if (CollectionUtils.isEmpty(map)) {
            //未授权
            response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
            return false;
        }
        UserDTO userDTO = BeanUtil.fillBeanWithMap(map, new UserDTO(), false);
        //用户信息保存到ThreadLocal中
        UserHolder.saveUser(userDTO);
        //刷新token有效期
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }
}
拦截器的操作方式
方式一

拦截器

public class LoginInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

    /**
     * 这个LoginInterceptor是new出来的,所以不能使用Spring注入Bean
     */
    public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
       this.stringRedisTemplate = stringRedisTemplate;
    }
}

使用拦截器

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    /**
     * 添加拦截器
     *
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        InterceptorRegistration registration = registry.addInterceptor(new LoginInterceptor(stringRedisTemplate));
        registration.excludePathPatterns("/user/code");
        registration.excludePathPatterns("/user/login");
        registration.excludePathPatterns("/blog/hot");
        registration.excludePathPatterns("/shop/**");
        registration.excludePathPatterns("/shop-type/**");
        registration.excludePathPatterns("/voucher/**");
    }
}
方式二

拦截器:配置为Spring的组件

@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
}

注册拦截器:依赖注入使用即可

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    /**
     * 添加拦截器
     *
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        InterceptorRegistration registration = registry.addInterceptor(loginInterceptor);
        registration.excludePathPatterns("/user/code");
        registration.excludePathPatterns("/user/login");
        registration.excludePathPatterns("/blog/hot");
        registration.excludePathPatterns("/shop/**");
        registration.excludePathPatterns("/shop-type/**");
        registration.excludePathPatterns("/voucher/**");
    }
}
Redis实现session共享

image.png

拦截器优化

当前存在的问题:
如果用户访问不需要登录鉴权的接口,token就不会刷新,token可能会过期。
image.png
token刷新拦截器

import cn.hutool.core.bean.BeanUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.utils.UserHolder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;

/**
 * @author zhangzengxiu
 * @date 2023/10/6
 */
@Component
public class RefreshTokenInterceptor implements HandlerInterceptor {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取请求头的token
        String token = request.getHeader("authorization");
        if (StringUtils.isBlank(token)) {
            return true;
        }
        //获取redis中的token
        String tokenKey = LOGIN_USER_KEY + token;
        Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(tokenKey);
        if (CollectionUtils.isEmpty(map)) {
            //未授权
            return true;
        }
        UserDTO userDTO = BeanUtil.fillBeanWithMap(map, new UserDTO(), false);
        //用户信息保存到ThreadLocal中
        UserHolder.saveUser(userDTO);
        //刷新token有效期
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }
}

登录拦截器

import cn.hutool.http.HttpStatus;
import com.hmdp.dto.UserDTO;
import com.hmdp.utils.UserHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;

/**
 * 登录拦截器
 *
 * @author zhangzengxiu
 * @date 2023/10/6
 */
@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        UserDTO userDTO = UserHolder.getUser();
        if (Objects.isNull(userDTO)) {
            response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
            return false;
        }
        return true;
    }

    /**
     * 后置拦截器
     * 销毁用户信息,防止内存泄露
     *
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

商户查询

缓存

缓存就是数据交换的缓冲区称作Cache,是存储数据的临时地方,一般读写性能比较高。

CPU缓存

计算机构造:CPU+内存+磁盘
CPU要做数据计算必须先从内存或者硬盘读取到数据,然后放到寄存器才可以运算。计算机性能受限
CPU会把经常需要读写的数据放到CPU缓存中,这样做高速运算的时候,就不需要每次从内存或者磁盘中进行数据读取,再进行运算,而是直接从缓存中获取数据进行运算。
这样可以充分释放CPU的运算能力。CPU缓存越大,可存储的数据越多,处理的性能越高。

web应用开发过程中的缓存

image.png

优缺点

image.png

缓存作用模型

image.png

优化商户缓存流程

image.png
代码实现

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryShopById(Long id) {
        if (Objects.isNull(id) || id < 0) {
            return Result.fail("非法商户!");
        }
        //从redis中查询缓存信息
        String shopCacheKey = CACHE_SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(shopCacheKey);
        if (StringUtils.isNotBlank(shopJson)) {
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //未命中缓存查询数据库
        Shop shop = getById(id);
        if (Objects.isNull(shop)) {
            return Result.fail("商户不存在!");
        }
        //缓存商户信息
        stringRedisTemplate.opsForValue().set(shopCacheKey, JSONUtil.toJsonStr(shop));
        return Result.ok(shop);
    }
}

缓存更新策略

缓存一致性问题
image.png
image.png

业务场景

  • 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
  • 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存

主动更新策略

image.png
image.png

  • 01
    • 维护成本高,需要手动编写代码实现
  • 02
    • 可能没现成的,需要单独维护
  • 03
    • 一致性差:还没异步去更新DB,其他线程去查询了数据库
    • 可靠性差:还没将数据更新到DB,Redis服务挂了,数据丢失了
手动维护

image.png

先删除缓存再操作DB
正常情况

image.png

异常情况

数据不一致情况
image.png

先操作DB再删除缓存(使用)
正常情况

image.png

异常情况

出现的可能性相对较低,加超时时间作为兜底!!!
出现的条件:

  • 两条线程并行执行
  • 线程1执行时,缓存刚好失效
  • 查询数据库后写缓存是微秒级别的
    • 这时刚好另一条线程来进更新了数据库并且删除了缓存,可能性很低

image.png

总结

image.png
image.png

最佳实践方案

image.png
业务代码实现
image.png

设置超时时间

image.png
image.png

缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求会全部打到数据库中。

解决方案

image.png

缓存空对象

image.png

image.png

布隆过滤器

并不是100%准确,有风险
image.png
image.png

业务代码

image.png
解决方案:
image.png

总结

image.png

缓存雪崩

缓存雪崩是指同意时段大量的缓存key同时失效或者redis宕机,导致大量请求到达数数据库,带来巨大压力。
image.png

未命中

image.png

服务宕机

image.png

解决方案

  • 给不同的key的TTL添加随机值。(缓存预热过期都时间一样)
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加服务降级、限流(如:快速失败,拒绝服务等)
  • 给业务添加多级缓存

缓存击穿(热点key)

缓存击穿问题也叫热点key问题,就是一个被高并发访问并且缓存重建业务复杂的key突然失效,无数的请求在瞬间给业务数据库带来巨大的冲击。
image.png
image.png

image.png

解决方案

互斥锁

image.png
性能差,阻塞

逻辑过期

不设置过期时间,永不过期,做活动的时候才会去添加
image.png

VS

image.png

互斥锁牺牲了可用性,保证了一致性:CP
逻辑过期牺牲了一致性,保证了可用性:AP

互斥锁解决缓存击穿问题

image.png

setnx

setnx只有第一个可以操作成功,其他的都会失败。
可以设置有效期作为兜底
有效期设置为业务执行时间的10-20倍
image.png

代码实现
获取锁
    /**
     * 尝试获取锁
     *
     * @param key
     * @return
     */
    private boolean tryLock(String key) {
        //setnx
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        //自动拆箱 防止NPE
        return BooleanUtil.isTrue(flag);
    }
释放锁
    /**
     * 释放锁
     *
     * @param key
     */
    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }
业务代码

image.png

    /**
     * 互斥锁解决缓存缓存击穿问题
     *
     * @param id
     * @return
     */
    private Shop queryShopByMutex(Long id) {
        //从redis中查询缓存信息
        String shopCacheKey = CACHE_SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(shopCacheKey);
        if (StringUtils.isNotBlank(shopJson)) {
            return getShopFromCache(shopJson);
        }
        Shop shop = null;
        String lockKey = "lock:shop:" + id;
        try {
            //获取互斥锁
            boolean isLock = tryLock(lockKey);
            if (!isLock) {
                //获取锁失败,休眠 重试
                TimeUnit.MILLISECONDS.sleep(50);
                //一直重试 会有性能问题
                return queryShopByMutex(id);
            }
            //获取锁成功,再次查询缓存是否存在,Double Check
            shopJson = stringRedisTemplate.opsForValue().get(shopCacheKey);
            if (StringUtils.isNotBlank(shopJson)) {
                return getShopFromCache(shopJson);
            }
            //未命中缓存查询数据库
            shop = getById(id);
            //模拟重建延时200ms
            TimeUnit.MILLISECONDS.sleep(200);
            if (Objects.isNull(shop)) {
                //缓存空值 缓存2min
                stringRedisTemplate.opsForValue().set(shopCacheKey, NULL_VAL, CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            //缓存商户信息,添加过期时间 30分钟
            stringRedisTemplate.opsForValue().set(shopCacheKey, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException();
        } finally {
            //释放互斥锁
            unLock(lockKey);
        }
        return shop;
    }

    /**
     * 从缓存中获取shop信息
     *
     * @param shopJson
     * @return
     */
    private Shop getShopFromCache(String shopJson) {
        if (StringUtils.equals(NULL_VAL, shopJson)) {
            //空值
            return null;
        }
        return JSONUtil.toBean(shopJson, Shop.class);
    }

模拟并发请求

image.png
线程组 QPS=200
image.png

逻辑过期解决缓存击穿问题

业务流程图

image.png

缓存预热
    /**
     * 模拟缓存预热
     *
     * @param id
     * @param expireSeconds 过期时间
     */
    public void saveShopToRedis(Long id, long expireSeconds) {
        if (Objects.isNull(id)) {
            return;
        }
        Shop shop = getById(id);
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        //未设置过期时间
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }

    @Autowired
    private ShopServiceImpl shopService;

    /**
     * 单测:缓存预热
     */
    @Test
    public void saveShopToRedis() {
        shopService.saveShopToRedis(1L, 10L);
    }

逻辑过期时间
image.png
业务代码实现

    /**
     * 缓存重建线程池
     */
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

	/**
     * 查询商户信息
     * 逻辑过期解决缓存击穿问题
     *
     * @param id
     
     * @return
     */
    public Shop queryShopByLogicExpire(Long id) {
        if (Objects.isNull(id) || id < 0) {
            return null;
        }
        RedisData redisData = getRedisDataFromCache(id);
        JSONObject data = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        //是否过期
        if (!cacheIsExpire(redisData)) {
            //未过期
            return shop;
        }
        //过期
        String lockKey = LOCK_SHOP_KEY + id;
        //获取互斥锁
        if (!tryLock(lockKey)) {
            //获取互斥锁失败,返回已经过期的商户信息
            return shop;
        }
        //获取锁成功
        redisData = getRedisDataFromCache(id);
        //Double Check 再次查看缓存是否过期
        if (!cacheIsExpire(redisData)) {
            //没过期,无需重建缓存
            return JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        }
        //开启独立线程进行缓存重建
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                this.saveShopToRedis(id, 20L);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                //释放锁
                unLock(lockKey);
            }
        });
        return shop;
    }

    /**
     * 缓存是否过期
     *
     * @param redisData
     * @return
     */
    private boolean cacheIsExpire(RedisData redisData) {
        //是否过期 Double check
        if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
            //未过期
            return false;
        }
        return true;
    }

    private RedisData getRedisDataFromCache(Long id) {
        String shopCacheKey = CACHE_SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(shopCacheKey);
        if (StringUtils.isBlank(shopJson)) {
            //不存在 直接返回
            return null;
        }
        return JSONUtil.toBean(shopJson, RedisData.class);
    }
压测

jmeter压测100QPS
查看运行结果
前面会返回旧数据
image.png
后面会返回新数据
image.png
数据会有短暂不一致的问题,但是保证了可用性。
image.png

封装缓存工具

image.png
代码

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;

/**
 * @author zhangzengxiu
 * @date 2023/10/7
 */
@Slf4j
@Component
public class CacheClient {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 缓存不存在的数据
     */
    public static final String NULL_VAL = "-1";

    /**
     * 锁key前缀
     */
    private static final String LOCK_KEY = "lock:";
    /**
     * 缓存重建线程池
     */
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 设置缓存
     *
     * @param key
     * @param value
     * @param expireTime
     * @param timeUnit
     */
    public void set(String key, Object value, long expireTime, TimeUnit timeUnit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), expireTime, timeUnit);
    }

    /**
     * 逻辑过期时间
     *
     * @param key
     * @param value
     * @param expireTime
     * @param timeUnit
     */
    public void setLogicExpire(String key, Object value, long expireTime, TimeUnit timeUnit) {
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(expireTime)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 解决缓存穿透问题
     *
     * @param id
     * @param keyPrefix
     * @param type
     * @param function
     * @param expireTime
     * @param timeUnit
     * @param <R>
     * @param <ID>
     * @return
     */
    public <R, ID> R queryWithPassThrough(ID id, String keyPrefix, Class<R> type, Function<ID, R> function, long expireTime, TimeUnit timeUnit) {
        if (Objects.isNull(id)) {
            return null;
        }
        //从redis中查询缓存信息
        String cacheKey = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(cacheKey);
        if (StringUtils.isNotBlank(json)) {
            if (StringUtils.equals(NULL_VAL, json)) {
                //空值
                return null;
            }
            return JSONUtil.toBean(json, type);
        }
        //未命中缓存查询数据库
        R res = function.apply(id);
        if (Objects.isNull(res)) {
            //缓存空值 缓存2min
            this.set(cacheKey, NULL_VAL, 2L, TimeUnit.MINUTES);
            return null;
        }
        //缓存添加过期时间
        this.set(cacheKey, JSONUtil.toJsonStr(res), expireTime, timeUnit);
        return res;
    }

    /**
     * 逻辑过期解决缓存击穿问题
     *
     * @param id
     * @return
     */
    public <R, ID> R queryByLogicExpire(String keyPrefix, ID id, Class<R> type, long expireTime, TimeUnit unit, Function<ID, R> function) {
        if (Objects.isNull(id)) {
            return null;
        }
        String cacheKey = keyPrefix + id;
        RedisData redisData = getRedisDataFromCache(cacheKey);
        JSONObject data = (JSONObject) redisData.getData();
        R res = JSONUtil.toBean(data, type);
        //是否过期
        if (!cacheIsExpire(redisData)) {
            //未过期
            return res;
        }
        //过期
        String lockKey = LOCK_KEY + id;
        //获取互斥锁
        if (!tryLock(lockKey)) {
            //获取互斥锁失败,返回已经过期的信息
            return res;
        }
        //获取锁成功
        redisData = getRedisDataFromCache(cacheKey);
        //Double Check 再次查看缓存是否过期
        if (!cacheIsExpire(redisData)) {
            //没过期,无需重建缓存
            return JSONUtil.toBean((JSONObject) redisData.getData(), type);
        }
        //开启独立线程进行缓存重建
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                //查询DB
                R r = function.apply(id);
                //写入redis
                this.setLogicExpire(lockKey, r, expireTime, unit);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                //释放锁
                unLock(lockKey);
            }
        });
        return res;
    }

    /**
     * 缓存是否过期
     *
     * @param redisData
     * @return
     */
    private boolean cacheIsExpire(RedisData redisData) {
        //是否过期 Double check
        if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
            //未过期
            return false;
        }
        return true;
    }

    /**
     * 从缓存中获取RedisData
     *
     * @param cacheKey
     * @return
     */
    private RedisData getRedisDataFromCache(String cacheKey) {
        String shopJson = stringRedisTemplate.opsForValue().get(cacheKey);
        if (StringUtils.isBlank(shopJson)) {
            //不存在 直接返回
            return null;
        }
        return JSONUtil.toBean(shopJson, RedisData.class);
    }

    /**
     * 尝试获取锁
     *
     * @param key
     * @return
     */
    private boolean tryLock(String key) {
        //setnx
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        //自动拆箱 防止NPE
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     *
     * @param key
     */
    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }

}

使用方式

    @Override
    public Result queryShopById(Long id) {
        //获取店铺信息 缓存穿透
        //Shop shop = queryShopByPassThrough(id);
        //使用工具类实现
        Shop shop = cacheClient.queryWithPassThrough(id, CACHE_SHOP_KEY, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //互斥锁 缓存击穿
        //Shop shop = queryShopByMutex(id);
        //逻辑过期时间 解决缓存击穿问题
        //Shop shop = queryShopByLogicExpire(id);
        Shop shop = cacheClient.queryByLogicExpire(CACHE_SHOP_KEY, id, Shop.class, CACHE_SHOP_TTL, TimeUnit.MINUTES, this::getById);
        if (Objects.isNull(shop)) {
            return Result.fail("商户不存在!");
        }
        return Result.ok(shop);
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1066982.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Springboot学习笔记——2

Springboot学习笔记——2 一、打包与运行1.1、程序打包与运行&#xff08;windows版&#xff09;1.2、打包插件1.3、Boot工程快速启动&#xff08;Linux版&#xff09; 二、配置高级2.1、临时属性设置2.2、配置程序四级分类2.3、自定义配置文件 三、多环境开发3.1、多环境开发&…

【计算机网络-自顶向下方法】应用层(HTTP、FTP)

1. Principles of network applications 创建一个网络应用 创建一个网络应用的核心&#xff0c;是编写一个分布式程序&#xff0c;使其可以运行在不同的端系统上&#xff0c;并能通过网络相互通信。&#xff08;例如&#xff0c;web服务器软件与浏览器软件&#xff09;   应…

为什么很多编程语言中数组都是从0开始编号?

文章来源于极客时间前google工程师−王争专栏。 如何实现随机访问? 什么是数组&#xff1f; 数组&#xff08;Array&#xff09;是一种线性表数据结构。它用一组连续的内存空间&#xff0c;来存储一组具有相同类型的数据。 线性表&#xff0c;顾名思义&#xff0c;线性表就…

林沛满-TCP之在途字节数

本文整理自&#xff1a;《Wireshark网络分析的艺术 第1版》 作者&#xff1a;林沛满 著 出版时间&#xff1a;2016-02 我一直谨记斯蒂芬霍金的金玉良言—每写一道数学公式就会失去一半读者。不过为了深度分析网络包&#xff0c;有时候是不得不计算的&#xff0c;好在小学一年级…

DirectX12_Windows_GameDevelop_3:Direct3D的初始化

引言 查看龙书时发现&#xff0c;第四章介绍预备知识的代码不太利于学习。因为它不像是LearnOpenGL那样从头开始一步一步教你敲代码&#xff0c;导致你没有一种整体感。如果你把它当作某一块的代码进行学习&#xff0c;你跟着敲会发现&#xff0c;总有几个变量是没有定义的。这…

【C++设计模式之策略模式】分析及示例

描述 策略模式&#xff08;Strategy Pattern&#xff09;是一种行为型设计模式&#xff0c;它允许在运行时根据不同的情况选择算法的行为。该模式将算法的定义封装成一组易于切换和替换的类&#xff0c;使得算法可以独立于其使用者进行变化。 原理 策略模式通过将具体的算法…

FastThreadLocal 快在哪里 ?

FastThreadLocal 快在哪里 &#xff1f; 引言FastThreadLocalset如何获取当前线程私有的InternalThreadLocalMap &#xff1f;如何知道当前线程使用到了哪些FastThreadLocal实例 ? get垃圾回收 小结 引言 FastThreadLocal 是 Netty 中造的一个轮子&#xff0c;那么为什么放着…

前端到底有多卷?可以转行吗?

我前几天招人&#xff0c;前后端各招一个人。 后端一天大概60多个投简历的。 前端岗位发出去&#xff0c;我吃了个饭&#xff0c;1小时回来 收到300多份简历…… 是一位HR回复的前端卷到什么程度的回答&#xff01; 下面我们来看两组官方纰漏的数据&#xff1a; 2023届全国高…

Git 学习笔记 | Git 的简介与历史

Git 学习笔记 | Git 的简介与历史 Git 学习笔记 | Git 的简介与历史Git 简介Git 历史 Git 学习笔记 | Git 的简介与历史 Git 简介 Git是分布式版本控制系统&#xff08;Distributed Version Control System&#xff0c;简称 DVCS&#xff09;&#xff0c;分为两种类型的仓库&…

100M跨境电商服务器能同时容纳多少人访问?

​  随着“出国”“出海”需求的业务量增多&#xff0c;网络的不断发展&#xff0c;服务商开始在带宽资源配备上作出各种改进。无论是纯国际带宽还是优化回国带宽租用&#xff0c;我们都可以独享&#xff0c;并且享受到大带宽。一般&#xff0c;做跨境电商业务的群体&#xf…

黑客都是土豪吗?真实情况是什么?

黑客的利益链条真的这么大这么好么,连最外围的都可以靠信息不对称赚普通人大学毕业上班族想都不敢想的金钱数目,黑客们是不是基本都是土豪 网络技术可以称为黑客程度的技术是不是真的很吃香&#xff1f;如果大部分大学生的智力资源都用在学习网络技术&#xff0c;会不会出现僧…

如何杜绝聊天泄密事件的发生呢(企业如何管理通讯工具,防止员工聊天泄密)

在现代企业中&#xff0c;员工之间的沟通是必不可少的。然而&#xff0c;随着科技的发展&#xff0c;员工聊天泄密的风险也日益增加。企业需要采取一系列措施来防止员工聊天泄密&#xff0c;以保护企业的核心竞争力和商业机密。本文将介绍一些有效的防止员工聊天泄密的方法。 1…

PHP8的匿名类-PHP8知识详解

PHP8支持通过new class 来实例化一个匿名类。所谓匿名类&#xff0c;就是指没有名称的类&#xff0c;只能在创建时使用new语句来声明它们。 匿名类是一种没有命名的即时类&#xff0c;可以用于简单的对象封装和实现接口。 以下是PHP 8中匿名类的基本语法示例&#xff1a; $ob…

Springboot使用Aop保存接口请求日志到mysql(及解决Interceptor拦截器中引用mapper和service为null)

一、Springboot使用Aop保存接口请求日志到mysql 1、添加aop依赖 <!-- aop日志 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency> 2、新建接口保存数据…

VsCode 常见的配置、常用好用插件

1、自动保存&#xff1a;不用装插件&#xff0c;在VsCode中设置一下就行 2、设置ctr滚轮改变字体大小 3、设置选项卡多行展示 这样打开了很多个文件&#xff0c;就不会导致有的打开的文件被隐藏 4、实时刷新网页的插件&#xff1a;LiveServer 5、open in browser 支持快捷键…

FM100/FM101协议系列-快速充电接口芯片

产品描述&#xff1a; FM100/FM101是一款支持Quick Charge 2.0&#xff08;QC 2.0&#xff09;快速充电协议的充电接口控制器 IC&#xff0c;可自动识别快速充电设备类型&#xff0c;并通过QC2.0协议与设备握手&#xff0c;使之获得设备允许的安全最高充电电压&#xff0c;在保…

为什么程序员必须坚持写技术博客?

当你申请一份工作的时候&#xff0c;你的简历通常大概只有两页的篇幅。当你接受面试的时候&#xff0c;你通常会跟面试官聊上一两个小时。以如此简短的简历和如此短暂的面试来评估一名软件开发人员的技能非常困难&#xff0c;所以雇主以此判定某个人是否适合某个工作岗位也颇具…

VB.NET vs. VB6.0:现代化编程语言 VS 经典老旧语言

目录 ​.NET背景&#xff1a; 特点: VB6.0背景&#xff1a; 特点: 两者之间的不同: 总结: 升华: .NET背景&#xff1a; VB.NET一种简单&#xff0c;现代&#xff0c;面向对象计算机编程语言&#xff0c;有微软开发&#xff0c;VB.NET是一种基于.NET Framework的面向对象…

基于Dockerfile搭建LNMP

目录 一、基础环境准备 1、环境前期准备 二、部署nginx&#xff08;容器IP 为 172.18.0.10&#xff09; 1、配置Dockerfile文件 2、配置nginx.conf文件 3、构建镜像、启动镜像 三、部署mysql 1、配置Dockerfile文件 2、配置my.conf文件 3、构建镜像、启动镜像 5、验…

Linux 基本指令(上)

文章内容&#xff1a; 1. ls 指令 语法&#xff1a; ls [选项][目录或文件] 功能&#xff1a;对于目录&#xff0c;该命令列出该目录下的所有子目录与文件。对于文件&#xff0c;将列出文件名以及其他信息。 单个ls显示当前目录下的文件和目录 常用选项&#…