黑马点评学习笔记(一)

黑马点评学习笔记

一、项目简介

  • redis的企业应用实战,几乎涵盖所有的redis开发

二、项目架构和模块

项目架构

在这里插入图片描述

项目模块

在这里插入图片描述

三、模块开发

项目部署

  1. 导入半成品项目
  2. 创建数据库hmdp,然后使用sql文件创建数据库表
  3. 修改mysql、redis地址
  4. 启动后端项目、前端项目(部署在nginx上)

短信登录

1.基于Session实现登录

在这里插入图片描述

1.发送验证码
    // 发送手机验证码
    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1. 验证手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2. 手机号不合法,返回错误信息
            return Result.fail("手机号非法,请重新输入!");
        }
        // 3. 手机号合法,生成验证码
        String code = RandomUtil.randomNumbers(6);
        // 4. 保存验证码到session中
        session.setAttribute(SESSION_PHONE_CODE, code);
        // 5. 发送验证码(模拟)
        log.debug("手机验证码发送成功!验证码是:{}", code);
        // 6.返回成功结果
        return Result.ok();
    }
2.短信验证码登录、注册
    // 登录、注册一体
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1. 校验手机号
        if (RegexUtils.isPhoneInvalid(loginForm.getPhone())) {
            // 2. 手机号不合法,返回错误信息
            return Result.fail("手机号非法,请重新输入!");
        }
        // 2. 校验验证码
        Object code = session.getAttribute(SESSION_PHONE_CODE);
        String code1 = loginForm.getCode();
        if (code == null || !code.toString().equals(code1)) {
            // 3. session中没有验证码,或者验证码不正确,返回错误信息
            return Result.fail("验证码错误!");
        }
        // 4. 验证码正确,查询手机号用户
        User user = lambdaQuery().eq(User::getPhone, loginForm.getPhone()).one();
        // 5. 手机号用户不存在,注册
        if (user == null) {
            user = createUserByPhone(loginForm.getPhone());
        }
        // 6. 将用户保存到session中,要转成UserDTO保护信息安全(不管是否查询到用户,最终user一定有用户,需要保存到session)
        session.setAttribute(SESSION_USERDTO, BeanUtil.copyProperties(user, UserDTO.class));
        // 7. 返回成功结果(不需要返回登录凭证,因为每次用户访问都自带cookie,cookie中有sessionid就可以判断登录状态了)
        return Result.ok();
    }
3.校验登录状态

创建拦截器

// mvc拦截器
public class LoginInterceptor implements HandlerInterceptor {
    // 前置拦截(主要是校验登录状态)
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 从session中获取用户
        HttpSession session = request.getSession();
        Object user = session.getAttribute(SESSION_USERDTO);
        // 2. 用户不存在,拦截
        if (user == null) {
            response.setStatus(401);//设置状态码
            return false;//拦截
        }
        // 3. 用户存在,保存到TreadLocal线程中
        UserHolder.saveUser((UserDTO) user);
        // 4. 放行
        return true;
    }

    // 后置拦截(移除线程中的用户,防止内存泄露)
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

创建mvc配置类,添加拦截器并配置拦截路径

// webmvc配置类
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    // 添加自定义拦截器,并配置拦截规则
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(
                "/shop/**",
                "/voucher/**",
                "/shop-type/**",
                "/upload/**",
                "/blog/hot",
                "/user/code",
                "/user/login"
        );
    }
}

校验接口返回用户信息

    /**
     * 获取当前登录的用户并返回
     * @return
     */
    @GetMapping("/me")
    public Result me(){
        UserDTO user = UserHolder.getUser();
        return Result.ok(user);
    }
2.集群的session共享问题
  1. session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。

  2. session的替代方案(使用redis替代)应该满足:

    • 数据共享

    • 内存存储

    • key、value结构

  3. Redis代替session需要考虑的问题:

    • 选择合适的数据结构

    • 选择合适的key

    • 选择合适的存储粒度

3.基于Redis实现登录

使用redis存储数据考虑两个点

  • 使用哪种数据结构,如验证码使用字符串存储,用户对象可以使用字符串或者hash存储,字符串存储对象比较直观但是仅限数据量不大,推荐使用hash方式,修改数据啥的更方便。。
  • 键的设计,键的设计需要满足唯一性、可传递性,因为多台tomcat访问同一个redis,必须要保证唯一。后续我们需要在代码中获取redis的值,所以我们需要携带键来获取值,这里就需要传递性。如验证码使用手机号作为键,用户对象使用用户登录后的token作为键。

很多小技巧

  • StrUtil.isBlank(token)判断token是否为空

  • 对象转map,并配置转换条件,如忽略null值、编辑字段,把值都转换成String类型。

    Map<String, Object> usermap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create()
                                                     .ignoreNullValue().setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
    
  • map转对象,设置转换时不忽略错误,有异常就抛出

    UserDTO userDTO = BeanUtil.fillBeanWithMap(usermap, new UserDTO(), false);//不忽略错误
    
  • 生成简单token,没有横线

    String token = UUID.randomUUID().toString(true);//没有短横线的uuid
    
  • 把静态常量都专门写到一个类里,写成静态常量

  • redis存值时需要设置过期时间

    // 设置有效期,因为hash不能存的时候设置有效期,所以只能后续设置
    stringRedisTemplate.expire(tokenkey,LOGIN_USER_TTL,TimeUnit.MINUTES);//token在用户没有访问时,半个小时就过期
    
  • 使用两个拦截器分工协作,可以达到用户点击任何页面就刷新token,防止token过期而需要重新登录的现象。

  • 当自己创建的对象需要使用spring管理的对象时,不能直接注入,可以通过构造方法传入。

  1. 发送验证码

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    // 发送手机验证码
    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1. 验证手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2. 手机号不合法,返回错误信息
            return Result.fail("手机号非法,请重新输入!");
        }
        // 3. 手机号合法,生成验证码
        String code = RandomUtil.randomNumbers(6);
        // 4. 保存验证码到redis中,手机号为键,验证码为值,两分钟后过期
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
        // 5. 发送验证码(模拟)
        log.debug("手机验证码发送成功!验证码是:{}", code);
        // 6.返回成功结果
        return Result.ok();
    }
    
  2. 短信验证码登录、注册

    // 登录、注册一体
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1. 校验手机号
        if (RegexUtils.isPhoneInvalid(loginForm.getPhone())) {
            // 2. 手机号不合法,返回错误信息
            return Result.fail("手机号非法,请重新输入!");
        }
        // 2. 校验验证码
        String code = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + loginForm.getPhone());
        String code1 = loginForm.getCode();
        if (code == null || !code.equals(code1)) {
            // 3. session中没有验证码,或者验证码不正确,返回错误信息
            return Result.fail("验证码错误!");
        }
        // 4. 验证码正确,查询手机号用户
        User user = lambdaQuery().eq(User::getPhone, loginForm.getPhone()).one();
        // 5. 手机号用户不存在,注册
        if (user == null) {
            user = createUserByPhone(loginForm.getPhone());
        }
        // 6. 生成简单token
        String token = UUID.randomUUID().toString(true);//没有短横线的uuid
        // 7. 将用户转换成map保存到redis中
        //将用户拷贝成dto对象,安全一点
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        //将对象转换成map类型,并把long类型id转换成字符串,方便存入redis
        Map<String, Object> usermap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create()
                                                         .ignoreNullValue().setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        // 存入redis
        String tokenkey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenkey,usermap);
    
        // 8.设置有效期,因为hash不能存的时候设置有效期,所以只能后续设置
        stringRedisTemplate.expire(tokenkey,LOGIN_USER_TTL,TimeUnit.MINUTES);//token在用户没有访问时,半个小时就过期
        // 9. 返回token给前端,后续登录校验每次就看你有没有token
        return Result.ok(token);
    }
    
    // 根据手机号创建用户
    private User createUserByPhone(String phone) {
        // 1. 创建用户,设置手机号、昵称
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
        // 2. 保存用户
        save(user);
        return user;
    }
    
  3. 使用了两个拦截器分别做token的刷新和登录拦截

    token刷新

    // token刷新拦截器
    public class RefreshInterceptor implements HandlerInterceptor {
    
        // 由于LoginInterceptor对象是手动创建,不受spring管理,不能直接注入,所以需要构造方法传入redis对象
        private StringRedisTemplate stringRedisTemplate;
    
        public RefreshInterceptor(StringRedisTemplate stringRedisTemplate) {
            this.stringRedisTemplate = stringRedisTemplate;
        }
    
        // 前置拦截(主要是校验登录状态)
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 1.获取请求头中的token,根据token从redis中获取用户数据map
            String token = request.getHeader("authorization");
            // token为空,无法查到用户,直接放行
            if (StrUtil.isBlank(token)){
                return true;
            }
            String tokenkey = LOGIN_USER_KEY + token;
            Map<Object, Object> usermap = stringRedisTemplate.opsForHash().entries(tokenkey);
            // 2. 如果map为空,无法查到用户,直接放行
            if (usermap.isEmpty()){
                return true;
            }
            // 3. 将map数据转换成userDTO类型
            UserDTO userDTO = BeanUtil.fillBeanWithMap(usermap, new UserDTO(), false);//不忽略错误
            // 4. 直接将用户保存到TreadLocal线程中
            UserHolder.saveUser(userDTO);
            // 5. 用户一直在访问的话,刷新token过期时间,类似session的默认30分钟
            stringRedisTemplate.expire(tokenkey,LOGIN_USER_TTL, TimeUnit.MINUTES);//刷新token过期时间
            // 6. 放行
            return true;
        }
    
        // 后置拦截(移除线程中的用户,防止内存泄露)
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            UserHolder.removeUser();
        }
    }
    

    登录拦截

    // 登录拦截器
    public class LoginInterceptor implements HandlerInterceptor {
    
    
        // 前置拦截(主要是校验登录状态)
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            //线程中没有用户,说明未登录,拦截
            if (UserHolder.getUser() == null){
                response.setStatus(401);
                return false;
            }
            //有用户,直接放行
            return true;
        }
    
        // 后置拦截(移除线程中的用户,防止内存泄露)
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            UserHolder.removeUser();
        }
    }
    

商户查询缓存

1.什么是缓存

在这里插入图片描述

在这里插入图片描述

2.添加redis缓存

根据id查询商铺信息添加redis缓存

@Resource
private StringRedisTemplate stringRedisTemplate;

// 根据id查询商铺信息(添加缓存)
@Override
public Result selectShopById(Long id) {
    // 1. 在redis中查询商铺(使用字符串的形式)
    String key = CACHE_SHOP_KEY + id;
    String shopjson = stringRedisTemplate.opsForValue().get(key);
    if (StrUtil.isNotBlank(shopjson)) {
        // 2. 如果有,直接返回
        Shop shop = JSONUtil.toBean(shopjson, Shop.class);
        return Result.ok(shop);
    }
    // 3. 没有,去数据库查询
    Shop shop = getById(id);
    // 4. 数据库查询对象不存在,返回错误
    if (shop == null) {
        return Result.fail("商铺不存在!");
    }
    // 5. 数据库对象存在,加入缓存
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
    // 6. 返回商铺对象
    return Result.ok(shop);
}

店铺类型查询业务添加缓存


3.缓存更新策略

缓存更新的方式

在这里插入图片描述

选择主动更新好一些

在这里插入图片描述

主动更新我们需要考虑三个问题

在这里插入图片描述

先操作缓存还是先操作数据库

在这里插入图片描述

最佳实践方案

在这里插入图片描述

4.缓存穿透

在这里插入图片描述

缓存穿透产生的原因是什么?

  • 用户请求的数据在缓存中和数据库中都不能存在,不断发起这样的请求,给数据库带来巨大压力。

缓存穿透的解决方案有哪些?

被动:

  • 缓存空值
  • 布隆过滤

主动:

  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的校验格式
  • 加强用户权限校验
  • 做好热点参数的限流

具体代码实现(以查询商铺数据为例)

思路:缓存和数据库都未查询到数据,我们需要缓存一个空值,有效期2分钟,然后查询数据时,如果查询到的商铺数据为空值,说明该数据不存在,直接返回信息。

  1. 查询缓存时
//如果缓存数据是缓存击穿的结果,就返回不存在
//没有查询到商铺会返回Null,如果查到空值,说明结果不存在,是我们手动设置的
if (shopjson != null && shopjson.equals("")) {
    return Result.fail("商铺不存在!");
}
  1. 查询数据库时
// 4. 数据库查询对象不存在,返回错误
if (shop == null) {
    //缓存空值,防止缓存击穿,过期时间为两分钟
    stringRedisTemplate.opsForValue().set(key,"", CACHE_NULL_TTL, TimeUnit.MINUTES);
    return Result.fail("商铺不存在!");
}
5.缓存雪崩

在这里插入图片描述

6.缓存击穿

什么是缓存击穿

高并发+重建缓存业务复杂

在这里插入图片描述

缓存击穿解决方案

在这里插入图片描述

具体代码实现

互斥锁:

获取锁是利用了redis的setnx命令设置值,即如果不存在该键,就设置值,如果存在了就设置失败。

// 使用互斥锁解决缓存击穿
private Result UseMutexesToResolveCacheBreakdowns(Long id) {
    // 1. 在redis中查询商铺(使用字符串的形式)
    String key = CACHE_SHOP_KEY + id;
    String shopjson = stringRedisTemplate.opsForValue().get(key);
    if (StrUtil.isNotBlank(shopjson)) {
        // 2. 如果有,直接返回
        Shop shop = JSONUtil.toBean(shopjson, Shop.class);
        return Result.ok(shop);
    }
    //如果缓存数据是缓存击穿的结果,就返回不存在
    if (shopjson != null && shopjson.equals("")) {
        return Result.fail("商铺不存在!");
    }

    // TODO 没有命中缓存,先获取锁
    String lockkey = LOCK_SHOP_KEY + id;
    Shop shop = null;
    try {
        boolean issuccess = trylock(lockkey);
        // TODO 获取失败,休眠然后重试
        if (!issuccess) {
            Thread.sleep(50);//休眠50毫秒
            return UseMutexesToResolveCacheBreakdowns(id);//重试
        }
        // TODO 3.获取成功,查询数据库,重建缓存
        shop = getById(id);
        //由于缓存击穿出现的条件是高并发访问+缓存重建业务复杂,所以我们这里休眠一下,模拟业务复杂
        //这样jmeter测试的时候就可以模拟了
        Thread.sleep(200);
        // 4. 数据库查询对象不存在,返回错误
        if (shop == null) {
            //缓存空值,防止缓存击穿,过期时间为两分钟
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return Result.fail("商铺不存在!");
        }
        // 5. 数据库对象存在,加入缓存,并设置超时剔除
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        // TODO 缓存重建完成,释放锁
        unlock(lockkey);
    }
    // 6. 返回商铺对象
    return Result.ok(shop);
}

逻辑过期:

一般都会提前把数据导入缓存,因为是热点数据嘛。

// 使用逻辑过期解决缓存击穿
private Result UseLogicalExpirationToResolveCacheBreakdowns(Long id) {
    // 1. 在redis中查询商铺(使用字符串的形式)
    String key = CACHE_SHOP_KEY + id;
    String shopjson = stringRedisTemplate.opsForValue().get(key);
    // 2. 如果没有命中缓存,直接返回(按理说逻辑过期都会命中,这里判断是为了代码健壮性考虑)
    if (StrUtil.isBlank(shopjson)) {
        return null;
    }

    // 3.获取数据
    // 这个地方json转成redisdata对象,获取data时需要强转为JSONObject,才能获取到data数据
    RedisData redisData = JSONUtil.toBean(shopjson, RedisData.class);
    Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);//获取商铺对象
    LocalDateTime expireTime = redisData.getExpireTime();//获取逻辑过期时间

    // 4.缓存命中,需要判断缓存是否过期
    if (expireTime.isAfter(LocalDateTime.now())){
        // 4.1没有过期,返回缓存
        return Result.ok(shop);
    }
    // 4.2如果过期,获取一个锁
    String lockkey = LOCK_SHOP_KEY + id;
    boolean issuccess = trylock(lockkey);
    if (!issuccess) {
        // 4.2.1获取失败,返回过期缓存
        return Result.ok(shop);
    }

    // 4.2.2获取成功,新开一个线程重建缓存,重建之后释放锁
    CACHE_REBUILD.submit(() ->{
        try {
            this.cacheRebuild(id,100L);//重建缓存,逻辑过期时间20秒
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            unlock(lockkey);//释放锁
        }
    });

    //  5.获取锁成功后,当前线程就返回之前的缓存
    return Result.ok(shop);
}

上面解决方案用到的方法

重建缓存,用了一个实体类封装数据和逻辑时间,这样比在实体类上添加字段好一点。

这里获取锁和释放锁的原理,利用了redis的setnx的特性,只能设置不存在的键,如果键存在就会设置失败,相当于每次只能设置一次。

//传入商铺id和逻辑过期时间重建缓存
public void cacheRebuild(Long id,Long expireTime){
    // 1.查询商铺
    Shop shop = getById(id);
    // 模拟复杂业务
    try {
        Thread.sleep(200);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    // 2.封装逻辑过期时间
    RedisData<Shop> redisData = new RedisData<>();
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));//添加逻辑过期秒
    // 3.写入缓存
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

//获取锁
public boolean trylock(String key) {
    // 利用setnx只能设置一次,来达到锁的效果
    Boolean islock = stringRedisTemplate.opsForValue().setIfAbsent(key, "1");
    // 不能直接返回islock,因为拆箱时有可能发生空指针我们这里使用工具类直接判断
    return BooleanUtil.isTrue(islock);
}

//释放锁
public void unlock(String key) {
    //直接删除setnx设置的锁
    stringRedisTemplate.delete(key);
}
7.缓存工具封装

注意点:

  • service层的返回值不要写成controller中的返回值Result(不规范),一般数据直接返回给controller层,然后controller层封装后返回Result对象。
  • 使用泛型之前先定义泛型
  • 缓存工具类,可以序列化任何java对象,封装了解决缓存穿透(缓存空值)和缓存击穿(逻辑过期)的解决方案。
//缓存工具类(避免解决缓存穿透和缓存击穿重复写代码)
@Slf4j
@Component
public class CacheClient {

    //新建一个线程池,初始为10个线程
    private static final ExecutorService CACHE_REBUILD = Executors.newFixedThreadPool(10);

    private StringRedisTemplate stringRedisTemplate;//构造注入

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    //1.将任意java对象序列化成json并存储在string类型的key中,并且可以设置TTL过期时间(普通redis存值并设置过期时间)
    public void set(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    //2.将任意java对象序列化成json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题。
    public void setWithLogical(String key, Object value, Long time, TimeUnit unit) {
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));//逻辑过期在redis层面不设置过期时间
    }

    //3.根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题。
    public <R, ID> Result ResolveCacheBreakdowns(
            String prefix, ID id, Class<R> clazz, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        // 1. 在redis中查询商铺(使用字符串的形式)
        String key = prefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(json)) {
            // 2. 如果有,直接返回
            R r = JSONUtil.toBean(json, clazz);
            return Result.ok(r);
        }
        //如果缓存数据是缓存穿透的结果,就返回不存在
        if (json != null && json.equals("")) {
            return Result.fail("数据不存在!");
        }

        // 3. 没有,去数据库查询
        R r = dbFallback.apply(id);
        // 4. 数据库查询对象不存在,返回错误
        if (r == null) {
            //缓存空值,防止缓存击穿,过期时间为两分钟
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return Result.fail("数据不存在!");
        }
        // 5. 数据库对象存在,加入缓存,并设置超时剔除
        this.set(key, r, time, unit);
        // 6. 返回商铺对象
        return Result.ok(r);
    }


    //4.根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题。
    // 使用逻辑过期解决缓存击穿(数据一开始会导入缓存,因为是热点数据)
    public  <R, ID> Result UseLogicalExpirationToResolveCacheBreakdowns(
            String prefix, ID id, Class<R> clazz, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        // 1. 在redis中查询商铺(使用字符串的形式)
        String key = prefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2. 如果没有命中缓存,直接返回(按理说逻辑过期都会命中,这里判断是为了代码健壮性考虑)
        if (StrUtil.isBlank(json)) {
            return null;
        }

        // 3.获取数据
        // 这个地方json转成redisdata对象,获取data时需要强转为JSONObject,才能获取到data数据
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), clazz);//获取商铺对象
        LocalDateTime expireTime = redisData.getExpireTime();//获取逻辑过期时间

        // 4.缓存命中,需要判断缓存是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {//数据时间是未来时间,没有过期
            // 4.1没有过期,返回缓存
            return Result.ok(r);
        }
        // 4.2如果过期,获取一个锁
        String lockkey = LOCK_SHOP_KEY + id;
        boolean issuccess = trylock(lockkey);
        if (!issuccess) {
            // 4.2.1获取失败,返回过期缓存
            return Result.ok(r);
        }

        // 4.2.2获取成功,新开一个线程重建缓存,重建之后释放锁
        CACHE_REBUILD.submit(() -> {
            try {
                //1.从数据库查询数据
                R r1 = dbFallback.apply(id);
                //2.写入缓存
                this.setWithLogical(key, r1, time, unit);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                unlock(lockkey);//释放锁
            }
        });

        //  5.获取锁成功后,当前线程就返回之前的缓存
        return Result.ok(r);
    }

    //获取锁
    public boolean trylock(String key) {
        // 利用setnx只能设置一次,来达到锁的效果
        Boolean islock = stringRedisTemplate.opsForValue().setIfAbsent(key, "1");
        // 不能直接返回islock,因为拆箱时有可能发生空指针我们这里使用工具类直接判断
        return BooleanUtil.isTrue(islock);
    }

    //释放锁
    public void unlock(String key) {
        //直接删除setnx设置的锁
        stringRedisTemplate.delete(key);
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一码一上午

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值