黑马点评主要功能点实现总结

黑马点评主要功能点实现总结

一、登录认证

1、发送手机验证码

流程图:

image-20231009184932442

实现代码(service):

    @Override
    public Result sendCode(String phone) {
​
        //校验手机号
        if (RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("手机号格式错误");
        }
​
        //如果符合,生成验证码
        String code = RandomUtil.randomNumbers(6);
        code = "666666";
​
        //验证码保存到redis
        // session.setAttribute(LOGIN_CODE_KEY,code);
        redisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
​
        //模拟发送验证码
        log.debug("模拟发送登录code:{}",code);
​
        return Result.ok();
    }

2、验证码登录

流程图:

image-20231009190938785

代码实现(service):

    @Override
    public Result login(LoginFormDTO loginForm) {
​
        //校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("手机号格式不正确");
        }
​
        //检验验证码
        String cacheCode = redisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);
        String code = loginForm.getCode();
        if (cacheCode==null || !code.equals(cacheCode.toString())){
            return Result.fail("验证码不正确");
        }
​
        //根据手机号查询用户
        User user = query().eq("phone", phone).one();
        //如果不存在,则创建用户
        if (user==null){
            user = createUserWithPhone(phone);
        }
​
        //将用户信息保存到redis
        //session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
        String token = UUID.randomUUID().toString(true);
        String tokenKey = LOGIN_USER_KEY+token;
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        //将对象转换成map,并将所有的值转化成string类型
        Map<String, Object> map = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString())
                );
​
        redisTemplate.opsForHash().putAll(tokenKey,map);
        redisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
​
        return Result.ok(token);
    }
​
    private User createUserWithPhone(String phone) {
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
        save(user);
        return user;
    }

3、退出登录

思路:删除客户端浏览器中的token;或者删除redis中缓存的用户数据即可。本项目通过后端返回ok,然后前端删除token实现。

实现代码:略

4、拦截器

1、认证信息拦截器

拦截需要用户登录信息的请求。

拦截器实现代码:

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;
​
    }
​
}
2、token刷新拦截器

拦截所有请求,如果用户已登录,则刷新token时间。

流程图:

image-20231009215111579

实现代码:

public class refreshTokenInterceptor implements HandlerInterceptor {
​
    private StringRedisTemplate redisTemplate;
​
    public refreshTokenInterceptor(StringRedisTemplate redisTemplate){
        this.redisTemplate = redisTemplate;
    }
​
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
​
        //从请求头中获取token
        String token = request.getHeader("authorization");
        String tokenKey = LOGIN_USER_KEY + token;
​
        //判断是否携带token
        if (StrUtil.isBlank(token)){
            return true;
        }
​
        //从redis获取用户信息
        Map<Object, Object> userMap = redisTemplate.opsForHash().entries(tokenKey);
        //判断是否有缓存
        if (userMap.isEmpty()){
            return true;
        }
​
        User user = BeanUtil.fillBeanWithMap(userMap, new User(), false);
​
        //将用户信息保存到线程域
        UserHolder.saveUser(user);
​
        //刷新token有效期
        redisTemplate.expire(tokenKey,LOGIN_USER_TTL, TimeUnit.MINUTES);
​
        //放行
        return true;
​
    }
​
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

二、查询缓存

1、缓存更新

先更新数据库,再删除缓存;并加上事务;

实现代码(service):

    @Override
    @Transactional
    public Result update(Shop shop) {
​
        Long shopId = shop.getId();
​
        if (shopId==null){
            return Result.fail("商铺id不能为空");
        }
​
        //1 更新数据库
        updateById(shop);
​
        //2 删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY + shopId);
​
        return Result.ok();
    }

2、缓存穿透

问题描述:缓存穿透指大量不存在的key查询数据,由于redis中不存在,会直接请求数据库,给数据库造成巨大压力。

解决方案:

  • 缓存空对象

    对不存在的数据也建立缓存。

    //在下面的缓存击穿中实现
  • 布隆过滤器

    在客户端和redis之间添加布隆过滤器,布隆过滤器使用的算法会提前判断key值是否存在,只有存在的时候才进行后续的查询操作。

3、缓存雪崩

问题描述:缓存雪崩指大量的key失效或者redis崩溃,压力全部给到数据库。

解决方案:

  • 给不同的key添加随机的TTL时间;

  • 使用redis集群提高系统的可用性;

  • 降级限流策略;

  • 给业务添加多级缓存;

4、缓存击穿

问题描述:缓存击穿又叫热点key问题,某些被高并发访问的key失效的时候,重建缓存的大量的并发请求到达数据库,给数据库造成巨大压力。

解决方案:

  • 互斥锁

    在重建缓存之前先尝试获取锁;

    流程:

    image-20231010040848157.png  0 → 100644

    代码实现:

        public Shop cacheJC1(Long id){
            //根据id查询redis
            String key = CACHE_SHOP_KEY + id;
            String shopJson = stringRedisTemplate.opsForValue().get(key);
            //如果有,直接返回
            if (StrUtil.isNotBlank(shopJson)){
                Shop shop = JSONUtil.toBean(shopJson, Shop.class);
                return shop;
            }
    ​
            //如果是空字符串,也返回(缓存穿透防御措施)
            // if (shopJson!=null){
            //     return null;
            // }
    ​
            String shopLock = LOCK_SHOP_KEY + id;
            //如果没有,尝试获取锁
            boolean b = tryLock(shopLock);
            if (!b){
                //获取锁失败,休眠,并重试从redis获取数据
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Shop shop = cacheJC1(id);
                return shop;
            }else {
                //获取锁成功,查询mysqll,重建缓存
                Shop shop = getById(id);
                if (shop == null){
                    //mysql没有数据
                    //缓存空值
                    stringRedisTemplate.opsForValue().set(key,"",2L,TimeUnit.MINUTES);
                    releaseLock(shopLock);
                    return null;
                }
    ​
                //MySQL有数据,缓存到redis(缓存30分钟)
                stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),30L, TimeUnit.MINUTES);
                releaseLock(shopLock);
                return shop;
            }
        }
    ​
        //使用redis模拟获取互斥锁
        public boolean tryLock(String shopLock){
            Boolean shopLock1 = stringRedisTemplate.opsForValue().setIfAbsent(shopLock, "shopLock",10L,TimeUnit.MINUTES);
            return BooleanUtil.isTrue(shopLock1);
        }
        //使用redis模拟释放互斥锁
        public void releaseLock(String shopLock){
            stringRedisTemplate.delete(shopLock);
        }

    缺点:如果线程未获取到锁,会等待,影响性能。

  • 逻辑过期

    在缓存的数据中添加一个过期时间字段,如果已过期,则尝试获取锁,然后返回旧数据并异步重建缓存。

    流程:

    image-20231010052817215

    代码实现:

        public Shop cacheJC2(Long id){
            //1 根据id查询redis
            String key = CACHE_SHOP_KEY + id;
            String shopDataJson = stringRedisTemplate.opsForValue().get(key);
            //2 判空
            String shopLock = LOCK_SHOP_KEY + id;
            if (StrUtil.isBlank(shopDataJson)){
                if (tryLock(shopLock)){
                    //拿到互斥锁,开启线程,缓存重建
                    CACHE_EXECUTOR.submit(()->{
                        try {
                            saveRedisData(id,10L);
                        }catch (Exception e){
                            System.out.println("shop缓存重建异常");
                        }finally {
                            //释放锁
                            releaseLock(shopLock);
                        }
                    });
                }
                //先返回旧的数据
                return null;
            }
            //3 有缓存,判断是否过期
            RedisData redisData = JSONUtil.toBean(shopDataJson, RedisData.class);
            Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
            LocalDateTime expireTime = redisData.getExpireTime();
            if (expireTime.isAfter(LocalDateTime.now())){
                //3.1 未过期,直接返回
                return shop;
            }
            //3.2 过期,开启线程试图重建缓存,并直接返回
            if (tryLock(shopLock)){
                //拿到互斥锁,开启线程,缓存重建
                CACHE_EXECUTOR.submit(()->{
                    try {
                        saveRedisData(id,10L);
                    }catch (Exception e){
                        System.out.println("shop缓存重建异常");
                    }finally {
                        //释放锁
                        releaseLock(shopLock);
                    }
                });
            }
            //没有得到互斥锁,直接返回旧数据
            return shop;
        }
    ​
        //使用redis模拟获取互斥锁
        public boolean tryLock(String shopLock){
            Boolean shopLock1 = stringRedisTemplate.opsForValue().setIfAbsent(shopLock, "shopLock",10L,TimeUnit.MINUTES);
            return BooleanUtil.isTrue(shopLock1);
        }
        //使用redis模拟释放互斥锁
        public void releaseLock(String shopLock){
            stringRedisTemplate.delete(shopLock);
        }

三、优惠券秒杀

1、分布式id生成器

订单表的id如果使用数据自增,会有诸多问题。我们使用自定义的id生成器生成id;1位符号位-31位时间戳-32序列号

代码实现:

@Component
public class RedisIdWorker {
​
    @Resource
    private StringRedisTemplate stringRedisTemplate;
​
    //开始时间
    private static final long BEGIN_TIMESTAMP = 1672531200L;
​
    //序列号位数
    private static final int COUNT_BITS = 32;
​
    //key 用于区分不同业务
    //id = 1位符号位-31位时间戳-32序列号
    public long nextId(String key){
        //1 时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;
        //2 序列号
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        Long increment = stringRedisTemplate.opsForValue().increment("icr" + key + ":" + date);
        //3 拼接
        return timestamp<<COUNT_BITS|increment;
    }
​
    public static void main(String[] args) {
        LocalDateTime of = LocalDateTime.of(2023, 1, 1, 0, 0, 0);
        long l = of.toEpochSecond(ZoneOffset.UTC);
        System.out.println(l);
    }
}

2、自定义的分布式锁

//基于redis实现的分布式锁
public class SimpleRedisLock implements mylock {
​
    //key值前缀
    private static final String PREFIX = "lock:";
​
    //线程标识前缀
    private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";
​
    //业务前缀
    private String name;
​
    //redis
    private StringRedisTemplate stringRedisTemplate;
​
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }
​
    @Override
    public boolean tryLock(long timeoutSec) {
​
        String threadId = ID_PREFIX + Thread.currentThread().getId();
​
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(PREFIX + name, threadId + "",timeoutSec, TimeUnit.SECONDS);
​
        // System.out.println("success"+success);
        return Boolean.TRUE.equals(success);
    }
​
    @Override
    public void unLock() {
​
        //获取当前线程id
        String id = ID_PREFIX + Thread.currentThread().getId();
        //获取锁的线程id
        String tid = stringRedisTemplate.opsForValue().get(PREFIX + name);
        if (id.equals(tid)){
            //一致则释放锁
            stringRedisTemplate.delete(PREFIX + name);
        }
    }
}

3、核心功能点实现(具体代码略)

1、超卖问题

并发场景中共享数据不安全问题。

解决方案:

1、悲观锁

使用悲观锁的缺点是未获取到锁的线程会被阻塞,性能不好;

2、乐观锁

乐观锁不会阻塞线程,(有多种实现,这里使用CAS)他会在更新数据的时候再次确认数据是否被修改,只有再次确认为被修改才会继续执行。

2、一人一单

查询数据库订单信息,判断是否已有订单;

3、用户重复点击秒杀

在判断完秒杀是否开始、库存是否充足这些基本信息后,先尝试获取分布式锁,如果无法获取锁,则说明已点击过下单,这时直接返回错误信息。

4、某些情况下一个线程会错误释放其他线程获取的锁

分布式锁是通过redis的setnx实现的,键包含用户id,值包含线程id;在释放锁的操作中,会判断即将释放的锁是否是当前线程的锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值