当前位置:   article > 正文

黑马 Redis笔记_黑马redis笔记

黑马redis笔记

3. Redis应用

3.1 短信登录

3.1.1 session短信登录

  1. 验证码存储
@Override
public Result sendCode(String phone, HttpSession session) {
    // 1. 校验手机号,不存在返回错误信息
    if (RegexUtils.isPhoneInvalid(phone)){
        // 不符合返回错误信息
        return Result.fail("手机号格式不正确");
    }
    // 2.生成验证码
    String code = RandomUtil.randomNumbers(6);
    // 3.保存验证码存入session
    session.setAttribute("code",code);
    // 4.发送验证码
    log.debug("发送短信验证码成功,验证码为{}",code);
    // 5.返回OK
    return Result.ok();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  1. 用户登录验证
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1. 校验手机号
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)){
        // 不符合返回错误信息
        return Result.fail("手机号格式不正确");
    }
    // 2. 校验验证码
    String cacheCode = (String) session.getAttribute("code");
    String code = loginForm.getCode();
    if (StringUtils.isEmpty(cacheCode) || !cacheCode.equals(code)){
        // 3. 验证码不一致,报错
        return Result.fail("验证码错误");
    }

    // 4. 判断用户是否存在
    User user = query().eq("phone", phone).one();

    // 5. 不存在则创建用户并保存数据库
    if (user == null){
        user =  createUserWithPhone(phone);
    }
    // 6. 把登录信息存入session当中
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    session.setAttribute("user",userDTO);
    return Result.ok();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  1. 拦截器配置

    3.1 编写拦截类

    public class LoginInterceptor implements HandlerInterceptor {
    
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 1. 获取session
            HttpSession session = request.getSession();
            // 2. 根据session获取用户
            Object user = session.getAttribute("user");
            // 3. 判断用户是否存在
            if (user == null){
                // 4. 不存在就拦截
                response.setStatus(401);
                throw new RuntimeException("用户没有登录");
            }
    
            // 5. 存在就保存到ThreadLocal当中
            UserHolder.saveUser((UserDTO) user);
            // 6. 放行
            return true;
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            UserHolder.removeUser();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    3.2 注册拦截器

    @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"
                    );
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

3.1.2 Redis短信登录

session短信登录存在的问题,当多台tomcat部署的时候,每台tomcat都会有自己的session,由于负载均衡会轮询多台tomcat,会导致数据丢失问题。例如:验证码存在第一天tomcat上,而登录是访问的是第二天tomcat上,这样无法找到验证码信息。

  1. 验证码存储
public Result sendCode(String phone, HttpSession session) {
    // 1. 校验手机号,不存在返回错误信息
    if (RegexUtils.isPhoneInvalid(phone)){
        // 不符合返回错误信息
        return Result.fail("手机号格式不正确");
    }
    // 2.生成验证码
    String code = RandomUtil.randomNumbers(6);
    // 3.保存验证码存入redis,要保证key唯一,所以使用phone作为key,并设置过期时间为2分钟
    redisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
    // 4.发送验证码
    log.debug("发送短信验证码成功,验证码为{}",code);
    // 5.返回OK
    return Result.ok();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  1. 用户登录验证
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1. 校验手机号
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)){
        // 不符合返回错误信息
        return Result.fail("手机号格式不正确");
    }
    // 2. 校验验证码,从redis当中取验证码校验
    String cacheCode = redisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);
    String code = loginForm.getCode();
    if (StringUtils.isEmpty(cacheCode) || !cacheCode.equals(code)){
        // 3. 验证码不一致,报错
        return Result.fail("验证码错误");
    }

    // 4. 判断用户是否存在
    User user = query().eq("phone", phone).one();

    // 5. 不存在则创建用户并保存数据库
    if (user == null){
        user =  createUserWithPhone(phone);
    }
    // 6.将登录信息保存到Redis当中
    //      6.1 随机生成 token作为key
    String token = UUID.randomUUID().toString(true);
    //      6.2 将user对象转为HashMap存储
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                                                     CopyOptions.create().ignoreNullValue().setFieldValueEditor((fieldName, fieldValue)->fieldValue.toString()));//为了解决Inter->String 的问题
    //      6.3 存入Redis并设置过期时间
    String key = LOGIN_USER_KEY + token;
    redisTemplate.opsForHash().putAll(key,userMap);
    redisTemplate.expire(key,LOGIN_USER_TTL,TimeUnit.MINUTES);
    // 7.返回token给前端
    return Result.ok(token);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  1. 配置拦截器

    3.1 配置刷新缓存时间拦截器

    private  StringRedisTemplate  redisTemplate;
    
    public RefreshInterceptor(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 获取请求头中的token
        String token = request.getHeader("authorization");
        if (StringUtils.isEmpty(token)){
            // 2. 不存在就交给登录拦截器
            return true;
        }
    
        // 3. 基于token获取用户
        String key = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = redisTemplate.opsForHash().entries(key);
        if (userMap.isEmpty()){
            // 4. 不存在就交给登录拦截器
            return true;
        }
    
    
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
    
        // 5. 存在就保存到ThreadLocal当中
        UserHolder.saveUser(userDTO);
    
        // 6. 刷新token有效期
        redisTemplate.expire(key,LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 7. 放行
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    3.2 配置登录拦截器

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 从ThreadLocal获取用户
        UserDTO userDTO = UserHolder.getUser();
        if (userDTO == null){
            // 2. 如果ThreadLocal中没有用户,那就拦截
            throw new RuntimeException("用户未登录");
        }
    
        return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

3.2 缓存应用

在这里插入图片描述

在这里插入图片描述

3.2.1 添加缓存

@Override
public Result queryById(Long id) {
     // 1. 从redis中查询商铺缓存
    String key = CACHE_SHOP_KEY + id;
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2. 判断是否存在
    if (!StringUtils.isEmpty(shopJson)) {
        // 3. 存在,直接返回
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return Result.ok(shop);
    }
    // 4. 不存在,根据id查询数据库
    Shop shop = getById(id);
    // 5. 不存在,返回错误
    if (shop == null) {
        return Result.fail("商品不存在");
    }
    // 6. 存在,存入redis
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
    return Result.ok(shop);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

待完成 对商品分类列表进行缓存处理

3.2.2 缓存更新策略

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3.2.3 实现缓存读写一致

查询数据

@Override
public Result queryById(Long id) {
    // 1. 从redis中查询商铺缓存
    String key = CACHE_SHOP_KEY + id;
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2. 判断是否存在
    if (!StringUtils.isEmpty(shopJson)) {
        // 3. 存在,直接返回
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return Result.ok(shop);
    }
    // 4. 不存在,根据id查询数据库
    Shop shop = getById(id);
    // 5. 不存在,返回错误
    if (shop == null) {
        return Result.fail("商品不存在");
    }
    // 6. 存在,存入redis
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
    return Result.ok(shop);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

更新数据

@Override
@Transactional  // 事务是为了保证Redis和MySQL的原子性
public Result updateShop(Shop shop) {
    Long id = shop.getId();
    if (id == null){
        return Result.fail("店铺不存在");
    }
    // 1. 更新数据库
    updateById(shop);
    // 2. 删除缓存
    stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
    return Result.ok();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

3.2.4 缓存穿透

缓存穿透是指:大量请求缓存和数据库中不存在的数据,导致数据库压力过大。

在这里插入图片描述

在这里插入图片描述

使用空对象实现,防止缓存穿透

@Override
public Result queryById(Long id) {
    // 1. 从redis中查询商铺缓存
    String key = CACHE_SHOP_KEY + id;
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2. 判断是否存在
    if (StringUtils.hasText(shopJson)) {
        // 3. 存在,直接返回
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return Result.ok(shop);
    }
    // 4. 判断是否为空值
    if (shopJson != null){
        return Result.fail("系统繁忙,请重试");
    }
    // 5. 不存在,根据id查询数据库
    Shop shop = getById(id);
    // 6. 不存在,返回错误
    if (shop == null) {
        stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
        return Result.fail("商品不存在");
    }
    // 7. 存在,存入redis
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
    return Result.ok(shop);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

3.2.5 缓存雪崩

在这里插入图片描述

3.2.6 缓存击穿(热点key问题)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

互斥锁解决缓存击穿

public Shop queryWithMutex(Long id) {
    // 1. 从redis中查询商铺缓存
    String key = CACHE_SHOP_KEY + id;
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2. 判断是否存在
    if (StringUtils.hasText(shopJson)) {
        // 3. 存在,直接返回
        return JSONUtil.toBean(shopJson, Shop.class);
    }
    // 4. 判断是否为空值
    if (shopJson != null) {
        return null;
    }
    // 5. 实现缓存重建
    String lockKey = "lock:shop:" + id;
    Shop shop = null;
    try {
        // 5.1 获取互斥锁
        boolean isLock = tryLock(lockKey);
        // 5.2 判断是否获取成功
        if (!isLock) {
            // 5.3 失败,休眠并重试
            Thread.sleep(50);
            return queryWithMutex(id);
        }
        // 5.4 成功,再次查询缓存,有可能在加锁的时候,别的线程已经重建缓存了
        shopJson = stringRedisTemplate.opsForValue().get(key);
        if (StringUtils.hasText(shopJson)) {
            // 3. 存在,直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        // 5.5 别的线程没有重建缓存,查询数据库
        Thread.sleep(100);
        shop = getById(id);

        // 6. 不存在,返回错误
        if (shop == null) {
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        // 7. 存在,存入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        // 8. 释放互斥锁
        unLock(lockKey);
    }

    return shop;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

逻辑删除实现缓存击穿

public Shop queryWithLogicalExpire(Long id) {
    // 1. 从redis中查询商铺缓存
    String key = CACHE_SHOP_KEY + id;
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2. 判断是否存在
    if (StringUtils.isEmpty(shopJson)) {
        // 3. 不存在,直接返回
        return null;
    }
    // 4. 命中,把json反序列化成对象
    RedisData redisData;
    redisData = JSONUtil.toBean(shopJson, RedisData.class);
    JSONObject jsonObject = (JSONObject) redisData.getData();
    Shop shop = JSONUtil.toBean(jsonObject, Shop.class);
    LocalDateTime expireTime = redisData.getExpireTime();
    // 5. 判断是否过期
    if (expireTime.isAfter(LocalDateTime.now())) {
        // 5.1 未过期,直接返回店铺信息
        return shop;
    }
    // 5.2 已过期,需要缓存重建

    // 6. 进行缓存重建
    // 6.1 获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    // 6.2 判断是否获取锁成功
    boolean isLock = tryLock(lockKey);
    if (isLock) {
        // 再次检测

        shopJson = stringRedisTemplate.opsForValue().get(key);
        redisData = JSONUtil.toBean(shopJson, RedisData.class);
        jsonObject = (JSONObject) redisData.getData();
        shop = JSONUtil.toBean(jsonObject, Shop.class);
        expireTime = redisData.getExpireTime();
        //  判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            //  未过期,直接返回店铺信息
            return shop;
        }

        // 6.3 成功开启新线程执行缓存重建
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            // 重建缓存
            try {
                this.saveShop2Redis(id, 20L);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                // 释放所
                unLock(lockKey);
            }
        });
    }

    return shop;
}
public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {
    // 1. 查询店铺信息
    Shop shop = getById(id);
    Thread.sleep(50);
    // 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));
}
private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}

private void unLock(String key) {
    stringRedisTemplate.delete(key);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76

3.2.7 缓存工具类

public class CacheClientUtils {

    private final StringRedisTemplate stringRedisTemplate;

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

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

    /**
     *  存储任意对象至redis,解决了缓存穿透问题
     * @param key redis中的键
     * @param value redis当中的值
     * @param time 过期时间
     * @param unit 过期时间的单位
     */
    public void setWithPassThrough(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }


    /**
     * 存储任意对象至redis,解决了缓存击穿问题
     * @param key redis中的键
     * @param value redis当中的值
     * @param time 过期时间
     * @param unit 过期时间的单位
     */
    public void setWithLogicalExpire(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));
    }

    /**
     *  在缓存穿透的情况下,查询数据
     * @param keyPrefix key的前缀
     * @param id 查询的id
     * @param type 查询对象的类型
     * @param dbFallback  查询方法
     * @param time 过期时间
     * @param unit 过期时间单位
     * @return
     */
    public <R,ID> R queryWithPassThough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
        // 1. 从redis中查询商铺缓存
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2. 判断是否存在
        if (StrUtil.isNotBlank(json)) {// 3. 存在,直接返回
            return JSONUtil.toBean(json, type);
        }
        // 4. 判断是否为空值
        if (json != null) {
            return null;
        }
        // 5. 不存在,根据id查询数据库
        R r = dbFallback.apply(id);
        // 6. 不存在,返回错误
        if (r == null) {
            stringRedisTemplate.opsForValue().set(key, "", time, unit);
            return null;
        }
        // 7. 存在,存入redis
        this.setWithPassThrough(key,r,time,unit);
        return r;
    }

    /**
     *  热点key问题,在缓存击穿的情况下,查询数据
     * @param keyPrefix key的前缀
     * @param id 查询的id
     * @param type 查询对象的类型
     * @param dbFallback  查询方法
     * @param time 过期时间
     * @param unit 过期时间单位
     */
    public<R,ID> R queryWithLogicalExpire(String keyPrefix,ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        // 1. 从redis中查询商铺缓存
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2. 判断是否存在
        if (StrUtil.isBlank(json)) {
            // 3. 不存在,直接返回
            return null;
        }
        // 4. 命中,把json反序列化成对象
        RedisData redisData;
        redisData = JSONUtil.toBean(json, RedisData.class);
        JSONObject jsonObject = (JSONObject) redisData.getData();
        R r = JSONUtil.toBean(jsonObject, type);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5. 判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 5.1 未过期,直接返回店铺信息
            return r;
        }
        // 5.2 已过期,需要缓存重建

        // 6. 进行缓存重建
        // 6.1 获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        // 6.2 判断是否获取锁成功
        boolean isLock = tryLock(lockKey);
        if (isLock) {
            // 再次检测

            json = stringRedisTemplate.opsForValue().get(key);
            redisData = JSONUtil.toBean(json, RedisData.class);
            jsonObject = (JSONObject) redisData.getData();
            r = JSONUtil.toBean(jsonObject, type);
            expireTime = redisData.getExpireTime();
            //  判断是否过期
            if (expireTime.isAfter(LocalDateTime.now())) {
                //  未过期,直接返回店铺信息
                return r;
            }

            // 6.3 成功开启新线程执行缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                // 重建缓存
                try {
                    R r1 = dbFallback.apply(id);
                    this.setWithLogicalExpire(key, r1, time,unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放所
                    unLock(lockKey);
                }
            });
        }

        return r;
    }


    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148

3.3 秒杀应用

3.3.1 全局ID

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

@Component
public class RedisIdWorker {

    private static final  long BEGIN_TIMESTAMP = 1640995200L;

    private final StringRedisTemplate stringRedisTemplate;

    private final  static Long COUNT_BITS = 32L;

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

    /**
     *  生成全局ID
     * @param keyPrefix 键前缀
     * @return
     */
    public long nextId(String keyPrefix){
        // 1. 生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;

        // 2. 生成序列号

        // 2.1 获取当前日期,精确到天
        String date = now.format(DateTimeFormatter.ofPattern("yyyyy:MM:dd"));
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        return timestamp<<COUNT_BITS|count;
    }
    
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

3.3.2 秒杀优惠券

@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
    // 1. 查询优惠券
    SeckillVoucher voucher = voucherService.getById(voucherId);
    // 2. 判断秒杀是否开始
    if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
        // 秒杀尚未开始
        return Result.fail("秒杀尚未开始");
    }
    // 3. 判断秒杀是否结束
    if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
        // 秒杀已经结束
        return Result.fail("秒杀尚未开始");
    }
    // 4. 判断库存是否充足
    if (voucher.getStock()<1) {
        // 库存不足
        return Result.fail("秒杀券已抢完");
    }
    // 5. 扣除库存
    boolean success = voucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).update();
    if (! success){
        // 扣除失败
        return Result.fail("秒杀券已抢完");
    }
    // 6. 创建订单

    // 6.1 订单id
    VoucherOrder voucherOrder = new VoucherOrder();
    long orderId = redisIdWorker.nextId("order");
    voucherOrder.setId(orderId);
    // 6.2 用户id
    Long userId = UserHolder.getUser().getId();
    voucherOrder.setUserId(userId);
    // 6.3 代金券id
    voucherOrder.setVoucherId(voucherId);
    save(voucherOrder);
    // 7. 返回订单id

    return Result.ok();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

如果只是采用普通的方式,会有超卖的问题发生,总共只有100个票,但是有200个线程去并发访问。按照道理来说,异常率应该是50%,而此时小于了50%,所以出现了超卖现象。

在这里插入图片描述

3.3.3 超卖问题解决(乐观锁实现)

乐观锁只能针对与更新操作,对于插入操作无法使用乐观锁。

在这里插入图片描述

 @Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {
        // 1. 查询优惠券
        SeckillVoucher voucher = voucherService.getById(voucherId);
        // 2. 判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 秒杀尚未开始
            return Result.fail("秒杀尚未开始");
        }
        // 3. 判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 秒杀已经结束
            return Result.fail("秒杀尚未开始");
        }
        // 4. 判断库存是否充足
        if (voucher.getStock()<1) {
            // 库存不足
            return Result.fail("秒杀券已抢完");
        }
        // 5. 扣除库存
        boolean success = voucherService.update().setSql("stock = stock -1")
                .eq("voucher_id", voucherId)
                .eq("stock",voucher.getStock())  // 重点是这里,添加了乐观锁
                .update();
        if (! success){
            // 扣除失败
            return Result.fail("秒杀券已抢完");
        }
        // 6. 创建订单

        // 6.1 订单id
        VoucherOrder voucherOrder = new VoucherOrder();
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 6.2 用户id
        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        // 6.3 代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        // 7. 返回订单id

        return Result.ok();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

添加了乐观锁导致了成功率过低,异常率高于我们预计的50%,所以需要进行再次修改。

在这里插入图片描述

针对本次业务,由于库存天然需要大于0,所以进行再次修改

@Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {
        // 1. 查询优惠券
        SeckillVoucher voucher = voucherService.getById(voucherId);
        // 2. 判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 秒杀尚未开始
            return Result.fail("秒杀尚未开始");
        }
        // 3. 判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 秒杀已经结束
            return Result.fail("秒杀尚未开始");
        }
        // 4. 判断库存是否充足
        if (voucher.getStock()<1) {
            // 库存不足
            return Result.fail("秒杀券已抢完");
        }
        // 5. 扣除库存
        boolean success = voucherService.update().setSql("stock = stock -1")
                .eq("voucher_id", voucherId)
                .gt("stock",0)  // 修改后的乐观锁
                .update();
        if (! success){
            // 扣除失败
            return Result.fail("秒杀券已抢完");
        }
        // 6. 创建订单

        // 6.1 订单id
        VoucherOrder voucherOrder = new VoucherOrder();
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 6.2 用户id
        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        // 6.3 代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        // 7. 返回订单id

        return Result.ok();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

现在我们可以发现异常率是50%,可以很好的解决超卖的问题。

在这里插入图片描述

3.3.4 一人一单实现(悲观锁实现)

修改订单实现类

@Override
public Result seckillVoucher(Long voucherId) {
    // 1. 查询优惠券
    SeckillVoucher voucher = voucherService.getById(voucherId);
    // 2. 判断秒杀是否开始
    if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
        // 秒杀尚未开始
        return Result.fail("秒杀尚未开始");
    }
    // 3. 判断秒杀是否结束
    if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
        // 秒杀已经结束
        return Result.fail("秒杀尚未开始");
    }
    // 4. 判断库存是否充足
    if (voucher.getStock() < 1) {
        // 库存不足
        return Result.fail("秒杀券已抢完");
    }
    Long userId = UserHolder.getUser().getId();
    synchronized (userId.toString().intern()) {
        // spring事务会失效,所以需要获取代理对象,来完成事务功能
        IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
        return proxy.createVoucherOrder(voucherId);
    }
}

@Transactional
@Override
public Result createVoucherOrder(Long voucherId) {
    // 查询是否存在该用户的订单
    Long userId = UserHolder.getUser().getId();
    Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
    if (count > 0) {
        return Result.fail("您已购买过该券");
    }
    // 5. 扣除库存
    boolean success = voucherService.update().setSql("stock = stock -1")
        .eq("voucher_id", voucherId)
        .gt("stock", 0) // 修改后的乐观锁
        .update();
    if (!success) {
        // 扣除失败
        return Result.fail("秒杀券已抢完");
    }
    // 6. 创建订单


    // 6.1 订单id
    VoucherOrder voucherOrder = new VoucherOrder();
    long orderId = redisIdWorker.nextId("order");
    voucherOrder.setId(orderId);
    // 6.2 用户id
    voucherOrder.setUserId(userId);
    // 6.3 代金券id
    voucherOrder.setVoucherId(voucherId);
    save(voucherOrder);
    // 7. 返回订单id

    return Result.ok();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

导入aspectj依赖

<!--aspectj的依赖-->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5

在启动类中,暴露aspectj的代理对象

@EnableAspectJAutoProxy(exposeProxy = true) // 暴露aspectj的代理对象
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {

    public static void main(String[] args) {
        SpringApplication.run(HmDianPingApplication.class, args);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

3.3.5 集群一人一单

配置集群环境

在这里插入图片描述

配置前端代理

在这里插入图片描述

集群模式下,会出现错误,每个服务器相当于一个进程,每个进程会有自己的JVM。而synchronizedJVM实现的,所以两个服务器没有共用锁,导致并发问题的发生。

在这里插入图片描述

3.4 分布式锁

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3.4.1 分布式锁的初级实现版本

public class SimpleRedisLock implements ILock{

    private String name;
    private StringRedisTemplate redisTemplate;

    private static final String KEY_PREFIX = "lock:";
    public SimpleRedisLock(String name, StringRedisTemplate redisTemplate) {
        this.name = name;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean tryLock(long timeoutSec) {

        long threadId = Thread.currentThread().getId();

        Boolean success = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);

        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        redisTemplate.delete(KEY_PREFIX + name);
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

问题:

上面的这种实现,会出现误删的效果。例如,线程1业务执行时间较长,超过了设置的超时时间,导致了,业务还没执行完,锁就自动释放了。此时,线程2尝试获取锁,由于线程1已经释放了锁,所以线程2可以获取锁,线程2此时执行自己的业务,而恰好线程1业务完成释放锁,造成了线程1释放了线程2的锁

在这里插入图片描述

修改后的版本,redis

public class SimpleRedisLock implements ILock{

    private String name;
    private StringRedisTemplate redisTemplate;

    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true);
    public SimpleRedisLock(String name, StringRedisTemplate redisTemplate) {
        this.name = name;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        // 1.存入锁的标识
        String threadId = ID_PREFIX+Thread.currentThread().getId();
        // 2.是否获取成功
        Boolean success = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId , timeoutSec, TimeUnit.SECONDS);

        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        // 1.从redis中获取锁标识
        String s = redisTemplate.opsForValue().get(KEY_PREFIX + name);
        // 2. 判断标识是否相等
        String threadId = ID_PREFIX+Thread.currentThread().getId();
        if (threadId.equals(s)){
            redisTemplate.delete(KEY_PREFIX + name);  
        }

    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

3.4.2 lua脚本

由于判断锁和释放锁不是原子性的操作,可能还会导致并发问题的出现。

在这里插入图片描述

Java调用lua脚本

在这里插入图片描述

lua脚本,位置在resource目录下

-- 比较线程标识与锁中标识是否一致
if (redis.call('get', KEYS[1]) == ARGV[1]) then
    -- 释放锁
    return redis.call('del', KEYS[1])
end
return 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

ILock实现类

package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock{

    private String name;
    private StringRedisTemplate redisTemplate;

    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true);
    
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua")); // 获取lua脚本
        UNLOCK_SCRIPT.setResultType(Long.class); // 设置lua脚本的返回值类型
    }
    public SimpleRedisLock(String name, StringRedisTemplate redisTemplate) {
        this.name = name;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        // 1.存入锁的标识
        String threadId = ID_PREFIX+Thread.currentThread().getId();
        // 2.是否获取成功
        Boolean success = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId , timeoutSec, TimeUnit.SECONDS);

        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        // 调用lua脚本
        redisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX+Thread.currentThread().getId()
        );

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

3.4.3 Redisson

在这里插入图片描述

  1. 导入依赖
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  1. redisson配置文件
@Configuration
public class RedisConfig {

    @Bean
    public RedissonClient redissonClient(){
        // 配置类
        Config config = new Config();
        // 添加redis地址,这里采用单机redis,也可以使用config.useClusterServers()添加集群地址
        config.useSingleServer().setAddress("redis://192.168.88.130:6379").setPassword("123456");
        // 创建客户端
        return Redisson.create(config);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  1. 使用
@Autowired
private RedissonClient redissonClient;

@Test
void testRedisson() throws InterruptedException {
    // 获取可重入锁,指定锁的名称
    RLock lock = redissonClient.getLock("anyLock");
    // 尝试获取锁,获取锁的最大等待时间(期间重试) 锁自动释放时间
    boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
    // 判断是否获取成功
    if (isLock) {
        try {
            System.out.println("执行业务");
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

3.4.4 Redisson原理

在这里插入图片描述

获取锁的流程

在这里插入图片描述

释放所的流程

在这里插入图片描述

通过测试,验我们的流程

private RLock lock ;
@BeforeEach
void setup(){
    lock = redissonClient.getLock("order");
}

@Test
void method1()  {
    // 尝试获取锁
    boolean isLock = lock.tryLock();
    if (!isLock){
        log.error("获取锁失败...1");
    }
    try{
        log.info("获取锁成功...1");
        method2();
        log.info("开始执行业务...1");
    }finally {
        log.warn("准备释放锁...1");
        lock.unlock();
    }
}


void method2(){
    // 尝试获取锁
    boolean isLock = lock.tryLock();
    if (!isLock){
        log.error("获取锁失败...2");
    }
    try{
        log.info("获取锁成功...2");
        log.info("开始执行业务...2");
    }finally {
        log.warn("准备释放锁...2");
        lock.unlock();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

执行到method1获取锁之后和我们的猜想是一样的通过Hash进行存储

在这里插入图片描述

超时等待原理

1. 异步查询锁的剩余时间
2. 判断设定时间是否大于(查询消耗时间+消耗时间)
	- 如果小于 返回 false
	- 如果大于 那么会订阅锁的释放消息,在锁释放的通知线程。在此期间线程会进行等待,等待时间为设定时间的剩余时间,
		 - 如果超过设定时间会返回false
		 - 如果没超过,会进入循环一直重试,尝试获取锁
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

超时释放原理

1. redisson内部设置了一个定时任务,每隔设定时间的1/3,进行一次时间的刷新,这样就可以保证任务执行完成。(自己如果设定了超时时间,那么不会有定时任务)
2. 在释放锁的时候,也会把定时任务给释放掉
  • 1
  • 2

在这里插入图片描述

在这里插入图片描述

3.5 优化秒杀应用

现在的流程

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3.5.1 基于 Redis阻塞队列 完成秒杀

修改VoucherServiceImpl.java文件

@Service
public class VoucherServiceImpl extends ServiceImpl<VoucherMapper, Voucher> implements IVoucherService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public Result queryVoucherOfShop(Long shopId) {
        // 查询优惠券信息
        List<Voucher> vouchers = getBaseMapper().queryVoucherOfShop(shopId);
        // 返回结果
        return Result.ok(vouchers);
    }

    @Override
    @Transactional
    public void addSeckillVoucher(Voucher voucher) {
        // 保存优惠券
        save(voucher);
        // 保存秒杀信息
        SeckillVoucher seckillVoucher = new SeckillVoucher();
        seckillVoucher.setVoucherId(voucher.getId());
        seckillVoucher.setStock(voucher.getStock());
        seckillVoucher.setBeginTime(voucher.getBeginTime());
        seckillVoucher.setEndTime(voucher.getEndTime());
        seckillVoucherService.save(seckillVoucher);
		// 保存优惠券到数据库的时候,同时保存到redis,通过redis来判断库存和一人一单
        redisTemplate.opsForValue().set(SECKILL_STOCK_KEY+voucher.getId(),voucher.getStock().toString());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

新建lua脚本

-- seckill.lua
-- 1.参数列表
-- 1.1.优惠券id
local voucherId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
-- 2.数据歌ey
-- 2.1.库存ey
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.订单key
local orderKey = 'seckill:order:' .. voucherId
-- 3.脚本业务
-- 3.1.判断库存是否充足get stockKey
if (tonumber(redis.call('get', stockKey)) <= 0) then
    -- 3.2.库存不足,返回1
    return 1
end
-- 3.3.判断用户是否下单SISMEMBER orderKey userId
if (redis.call('sismember', orderKey, userId) == 1) then
    --3.3.存在,说明是重复下单,返回2
    return 2
end
-- 3.4. 扣库存
redis.call('incrby', stockKey, -1)
-- 3.5. 下单保存用户
redis.call("sadd", orderKey, userId)
return 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

修改VoucherOrderServiceImpl.java文件

@Service
@Slf4j
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    private ISeckillVoucherService voucherService;

    @Autowired
    private RedisIdWorker redisIdWorker;

    @Autowired
    private StringRedisTemplate redisTemplate;

    private final BlockingQueue<VoucherOrder> orderTask = new ArrayBlockingQueue<>(1024*1024);

    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua")); // 获取lua脚本
        SECKILL_SCRIPT.setResultType(Long.class); // 设置lua脚本的返回值类型
    }
	
    @PostConstruct // @PostConstruct修饰的方法会在类初始化之后执行
    private void init(){
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }

    private class VoucherOrderHandler implements Runnable{

        @Override
        public void run() {
            while (true){
               
                try {
                     // 1. 获取队列中的订单信息
                    VoucherOrder voucherOrder = orderTask.take();
                     // 2. 创建订单
                    handleVoucherOrder(voucherOrder);
                } catch (InterruptedException e) {
                    log.error("创建订单时出现异常",e);
                }
               
            }
        }
    }

    private  IVoucherOrderService proxy;
    private void handleVoucherOrder(VoucherOrder voucherOrder) {
        // 1.无法从UserHolder当中获取userID,该函数在子线程当中运行     
        Long userId =voucherOrder.getUserId();
        // 其实我感觉这里不需要再考虑并发的情况了,并发情况已经被Redis处理了
        SimpleRedisLock simpleRedisLock = new SimpleRedisLock("order:"+userId,redisTemplate);
        boolean isLock = simpleRedisLock.tryLock(1200);
        if (!isLock) {
            log.error("一个用户只允许抢一个优惠券");
            return;
        }
        try {
             proxy.createVoucherOrder(voucherOrder);
        }finally {
            simpleRedisLock.unlock();
        }
    }

    @Override
    public Result seckillVoucher(Long voucherId) {  // 需要对秒杀时间进行判断!
        // 获取用户
        Long userId = UserHolder.getUser().getId();
        Long result = redisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(), // 没有key类型的参数,不能传null,要传空集合
                voucherId.toString(), userId.toString()
        );
        // 2. 判断结果是否为0
        int r = result.intValue();
        if (r != 0){
            // 2.1 不为0,没有购买资格
            return Result.fail(r == 1 ? "库存不足":"不能重复下单");
        }
        // 2.2 为0,有购买资格,将信息保存到阻塞队列里面
        // TODO 保存阻塞队列
        long orderId = redisIdWorker.nextId("order:");
        // 3. 准备订单信息
        VoucherOrder voucherOrder = new VoucherOrder();
        voucherOrder.setId(orderId);
        // 3.1 用户id
        voucherOrder.setUserId(userId);
        // 3.2 代金券id
        voucherOrder.setVoucherId(voucherId);
        // 代理对象现如今也无法获取,代理对象也是存储在ThreadLocal当中
        proxy = (IVoucherOrderService) AopContext.currentProxy();
        orderTask.add(voucherOrder);
        // 把代理对象放到成员变量当中

        // 4. 返回订单id
        return Result.ok(orderId);
    }
    @Transactional
    @Override
    public void createVoucherOrder(VoucherOrder voucherOrder) {
        // 查询是否存在该用户的订单
        Long userId = voucherOrder.getUserId();
        Integer count = query().eq("user_id", userId).eq("voucher_id", voucherOrder).count();
        if (count > 0) {
            log.error("您已购买过该券");
            return ;
        }
        // 5. 扣除库存
        boolean success = voucherService.update().setSql("stock = stock -1")
                .eq("voucher_id", voucherOrder)
                .gt("stock", 0) // 修改后的乐观锁
                .update();
        if (!success) {
            // 扣除失败
            log.error("秒杀券已抢完");
            return;
        }
        // 6. 将订单保存至数据库
        save(voucherOrder);

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124

在这里插入图片描述

在这里插入图片描述

秒杀优化的思路是什么?

  1. 先利用Redis完成库存余量、一人一单判断,完成抢单业务
  2. 再将下单业务放入阻塞队列,利用独立线程异步下单

基于阻塞队列的异步秒杀存在哪些问题?

  1. 内存限制问题
  2. 数据安全问题

3.6 基于消息队列完成秒杀

在这里插入图片描述

Redis实现消息队列的三种方式

  • list结构:基于List结构模拟消息队列
  • PubSub:基本的点对点消息模型
  • Stream:比较完善的消息队列模型

3.6.1 基于List消息队列

List消息队列的优缺点

优点:

  • 利用Redis存储,不受限于VM内存上限
  • 基于Redis的持久化机制,数据安全性有保证
  • 可以满足消息有序性

缺点:

  • 无法避免消息丢失
  • 只支持单消费者

3.6.2 基于 PubSubd 消息队列

优点:

  • 采用发布订阅模型,支持多生产、多消费

缺点:

  • 不支持数据持久化
  • 无法避免消息丢失
  • 消息堆积有上限,超出时数据丢失

3.6.3 基于Stream的消息队列

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3.7 达人探店

3.7.1 发布探店笔记

SystemConstants.java中修改上传文件路径

public class SystemConstants {
    public static final String IMAGE_UPLOAD_DIR = "E:\\redis-code\\nginx-1.18.0\\html\\hmdp\\imgs\\"; // 修改这一行的文件路径,如果有OSS服务器,可以自己重新实现该接口
    public static final String USER_NICK_NAME_PREFIX = "user_";
    public static final int DEFAULT_PAGE_SIZE = 5;
    public static final int MAX_PAGE_SIZE = 10;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3.7.2 查看探店笔记

在BlogServiceImpl.java中修改文件

@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
    @Autowired
    private IUserService userService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryBlogById(Long id) {
        Blog blog = getById(id);
        if (blog == null){
            return Result.fail("笔记不存在");
        }
        queryBlogUser(blog);
        return Result.ok(blog);
    }

    private void queryBlogUser(Blog blog) {
        Long userId = blog.getUserId();
        User user = userService.getById(userId);
        blog.setName(user.getNickName());
        blog.setIcon(user.getIcon());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

3.7.3 点赞排行榜功能

修改实体类blog.java

@Data
@Accessors(chain = true)
@TableName("tb_blog")
public class Blog implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * 商户id
     */
    private Long shopId;
    /**
     * 用户id
     */
    private Long userId;
    /**
     * 用户图标
     */
    @TableField(exist = false)
    private String icon;
    /**
     * 用户姓名
     */
    @TableField(exist = false)
    private String name;
    /**
     * 是否点赞过了
     */
    @TableField(exist = false)
    private Boolean isLike;

    /**
     * 标题
     */
    private String title;

    /**
     * 探店的照片,最多9张,多张以","隔开
     */
    private String images;

    /**
     * 探店的文字描述
     */
    private String content;

    /**
     * 点赞数量
     */
    private Integer liked;

    /**
     * 评论数量
     */
    private Integer comments;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

修改实现类BlogServiceImpl.java,在实现类中要实现两个功能

  • 完成点赞功能,同时添加到数据库和Redis当中
  • liked字段进行修改
Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
    @Autowired
    private IUserService userService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result queryHotBlog(Integer current) {

        // 根据用户查询
        Page<Blog> page = query()
                .orderByDesc("liked")
                .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
        // 获取当前页数据
        List<Blog> records = page.getRecords();
        // 查询用户
        records.forEach(blog -> {
            this.queryBlogUser(blog);
            this.isBlogLike(blog);
        });

        return Result.ok(records);
    }

    @Override
    public Result queryBlogById(Long id) {
        Blog blog = getById(id);
        if (blog == null){
            return Result.fail("笔记不存在");
        }
        queryBlogUser(blog);
        isBlogLike(blog);
        return Result.ok(blog);
    }

    private void isBlogLike(Blog blog) {
        // 1. 获取用户id
        if (UserHolder.getUser() == null){
            return;
        }
        Long userId = UserHolder.getUser().getId();
        // 2. 判断用户是否已经点赞了
        String key = BLOG_LIKED_KEY + blog.getId();
        Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
        blog.setIsLike(score != null);
    }

    @Override
    public Result likeBlog(Long id) {
        // 1. 获取用户id
        Long userId = UserHolder.getUser().getId();
        // 2. 判断用户是否已经点赞了
        String key = BLOG_LIKED_KEY+ id;
        Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
        if (score == null){
            // 2.1 如果未点赞,那么数据库的点赞数+1,并将其存入redis集合
            boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
            if (isSuccess){
                stringRedisTemplate.opsForZSet().add(key, String.valueOf(userId),System.currentTimeMillis());
            }
        }else {
            // 2.2 如果点赞,那么数据库的点赞数-1,并将其移除redis集合
            boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
            if (isSuccess){
                stringRedisTemplate.opsForZSet().remove(key, String.valueOf(userId));
            }
        }

        return Result.ok();
    }

    @Override
    public Result queryBlogLikes(Long id) {
        // 1. 查询top5点赞数 zrange key 0 4
        String key = BLOG_LIKED_KEY+ id;
        // 2. 我只用两个用户查过,发现是有序的,感觉内部实现应该采用的TreeSet
        Set<String> userIds = stringRedisTemplate.opsForZSet().range(key, 0, 4);
        if (userIds == null || userIds.isEmpty()){
            return Result.ok(Collections.emptyList());
        }

        List<User> users = new ArrayList<>(5);
        // 分别对每一个id进行查询,我看视频里面是通过自定义排序做的,我觉得太麻烦了,就分别查询了。
        userIds.forEach(userId->{
            users.add(userService.getById(userId));
        });

        List<UserDTO> userDTOS = users
                .stream()
                .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
                .collect(Collectors.toList());
        return Result.ok(userDTOS);
    }

    private void queryBlogUser(Blog blog) {
        Long userId = blog.getUserId();
        User user = userService.getById(userId);
        blog.setName(user.getNickName());
        blog.setIcon(user.getIcon());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102

3.8 好友关注

3.8.1 关注和取关

FollowServiceImpl.java当中,有两个方法,follo()用来关注或者取消关注,isFollow()用来判断是否关注了目标

@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private IUserService userService;
    @Override
    public Result follow(Long followUserId, Boolean isFollow) {
        // 1. 获取登录用户
        Long userId = UserHolder.getUser().getId();
        // 2. 判断是关注还是取关
        if (isFollow){
            // 2. 关注
            Follow follow = new Follow();
            follow.setFollowUserId(followUserId);
            follow.setUserId(userId);
            boolean isSuccess = save(follow);
            if (isSuccess){
                String key = "follow:"+userId;
                stringRedisTemplate.opsForSet().add(key, String.valueOf(followUserId));
            }
        }else {
            LambdaQueryWrapper<Follow> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(Follow::getFollowUserId, followUserId);
            queryWrapper.eq(Follow::getUserId, userId);
            boolean isSuccess = remove(queryWrapper);
            if (isSuccess){
                String key = "follow:"+userId;
                stringRedisTemplate.opsForSet().remove(key);
            }
        }

        return Result.ok();
    }

    @Override
    public Result isFollow(Long followUserId) {
        // 1. 获取登录用户
        Long userId = UserHolder.getUser().getId();
        // 2.查询是否关注该用户
        LambdaQueryWrapper<Follow> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Follow::getFollowUserId, followUserId);
        queryWrapper.eq(Follow::getUserId, userId);
        // 3. 判断是否大于0
        int count = count(queryWrapper);

        return Result.ok(count>0);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

3.8.2 共同好友

把关注列表存在RedisSet当中,通过 进行交集,就可以查找共同关注。

修改FollowServiceImpl.java

@Override
public Result followCommon(Long id) {
    // 1. 获取当前用户
    Long userId = UserHolder.getUser().getId();
    // 2. 获取当前用户关注列表和目标用户关注列表
    Set<String> followIds = stringRedisTemplate.opsForSet().intersect(userId.toString(), id.toString());
    if (followIds == null || followIds.isEmpty()){
        return Result.ok(Collections.emptyList());
    }
    List<UserDTO> userDTOS = userService.listByIds(followIds)
        .stream()
        .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
        .collect(Collectors.toList());

    return Result.ok(userDTOS);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

3.8.3 feed流

在这里插入图片描述

千万粉丝才考虑使用推拉结合,一般采用推模式就可以了。

在保存博客的时候,将id发送到每个粉丝发件箱,修改BlogServiceImpl.java的保存博客方法

@Override
public Result saveBlog(Blog blog) {
    // 获取用户
    UserDTO user = UserHolder.getUser();
    blog.setUserId(user.getId());
    // 保存探店博文
    boolean isSuccess = save(blog);
    if (!isSuccess) {
        return Result.fail("保存失败");
    }

    // 查询该用户所有的粉丝
    LambdaQueryWrapper<Follow> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(Follow::getFollowUserId, user.getId());
    List<Follow> followList = followService.list(queryWrapper);

    for (Follow follow : followList) {
        String key = "feed:"+follow.getUserId();
        stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(),System.currentTimeMillis());
    }
    return Result.ok();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

3.9 GEO 地理数据

在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/682859
推荐阅读
相关标签
  

闽ICP备14008679号