一些项目的总结

2 篇文章 0 订阅
2 篇文章 0 订阅

实现登录校验

单台服务器版

使用SpringMVC拦截器Interceptor+Session+ThreadLocal实现,具体如下

  1. 新建一个工具类UserHolder,用于封装ThreadLocal的方法。
public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
    public static void saveUser(UserDTO user){
        tl.set(user);
    }
    public static UserDTO getUser(){
        return tl.get();
    }
    public static void removeUser(){
        tl.remove();
    }
}
  1. 新建一个UserLoginInterceptor拦截器实现HandlerInterceptor接口,在preHandle()中判断是否拦截当前用户。
/**
 * 拦截器,拦截用户未登录的部分请求
 */
@Component
public class UserLoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1. 从session获取当前用户
        HttpSession session = request.getSession();
        Object user = session.getAttribute("user");

        //2. 当前用户是否存在
        if (user == null) {
            //3. 不存在,拦截
            response.setStatus(401);
            return false;
        }
        //4. 存在,保存到ThreadLocal并放行
        UserHolder.saveUser((UserDTO) user);
        return true;

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    	//释放ThreadLocal,防止内存泄漏
        UserHolder.removeUser();
    }
}
  1. 将自定义的拦截器,注册到SpringMvc中;新建一个MvcConfig,实现WebMvcConfigurer,并重写addInterceptors()方法。
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Autowired
    private UserLoginInterceptor userLoginInterceptor;

    /**
     * 用于Controller层的登录权限控制
     *
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //采用排除不需要拦截的请求地址(也可以用addPathPatterns()表示哪些路径需要拦截)
        registry.addInterceptor(userLoginInterceptor).excludePathPatterns(
                "/shop/**",
                "/voucher/**",
                "/shop-type/**",
                "/upload/**",
                "/blog/hot",
                "/user/code",
                "/user/login"
        );
    }
}

服务器集群版

注意:如果使用集群部署项目,会出现session不共享的问题(同一个用户发送多次请求,被多台集群接受,这时不是每台服务器都有用户session)。
解决方法:使用Redis缓存Session,Session有的特性Redis都有,Redis还能共享数据。

  • 共享数据
  • 内存存储
  • key-value形式

Redis使用Hash存储对象的优点:节省内存(相比与String存储对象而言)、可以对单个字段(key)进行修改,更加灵活
UserLoginInterceptor

/**
 * 拦截器,拦截用户未登录的部分请求
 */
@Component
public class UserLoginInterceptor implements HandlerInterceptor {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1. 从请求头获取token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            // 不存在,拦截
            response.setStatus(401);
            return false;
        }

        //2. 使用token从Redis获取用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);

        //3. 将用户(Map)--> 用户(UserDTO),isIgnoreError:转换出现错误是否忽略
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);

        //4. 当前用户是否存在
        if (userDTO == null) {
            // 不存在,拦截
            response.setStatus(401);
            return false;
        }

        //5. 存在,将用户存在ThreadLocal
        UserHolder.saveUser(userDTO);

        //6. 刷新token有效期
        stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MILLISECONDS);

        //7. 放行
        return true;
    }

在这里插入图片描述

Redis缓存商户信息

在这里插入图片描述

(一)缓存穿透

在这里插入图片描述

(二)缓存雪崩

在这里插入图片描述

(三)缓存击穿

在这里插入图片描述
在这里插入图片描述

(四)总结

在这里插入图片描述
参考链接1
参考链接2
参考链接3

使用Redis解决秒杀问题

ShopService:缓存商家数据时,缓存穿透、击穿的问题
ShopTypeService:Redis缓存首页店铺分类功能
UserService:使用Redis缓存短信验证码和用户登录成功的token
VoucherOrderService:使用Redis解决秒杀优惠券下单(超卖问题和一人一单)

未实现秒杀前:
在这里插入图片描述

        //1. 查询优惠券信息
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);

        //2. 判断秒杀是否开始
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime beginTime = seckillVoucher.getBeginTime();
        LocalDateTime endTime = seckillVoucher.getEndTime();
        if (beginTime.isAfter(now)) {
            //3. 否,返回异常结果
            return Result.fail("抢购时间未到!");
        }
        if (endTime.isBefore(now)) {
            return Result.fail("抢购时间已结束!");
        }

        //4. 是,判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
            //5. 否,返回异常结果
            return Result.fail("抢购完了,下次再来吧");
        }

        //6. 是,扣减库存
//        seckillVoucher.setStock(seckillVoucher.getStock() - 1);
//        seckillVoucherService.updateById(seckillVoucher);
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId).update();
        if (!success) {
            return Result.fail("库存不足!");
        }
        //7. 创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //7.1 订单id
        long orderId = redisWorker.nextId("order");
        voucherOrder.setId(orderId);
        //7.2 用户id
        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        //7.3 优惠券id
        voucherOrder.setVoucherId(voucherId);

        //8. 返回订单id
        return Result.ok(orderId);

以上是未实现秒杀,有诸多问题(Jmeter):

(一)超卖问题(乐观锁与悲观锁)

多个线程同时查询了数据库(查到都是1),查完后又都去修改了数据库,这时就导致了库存为负的问题;
在这里插入图片描述
这其实和基础学的多线程一样,是因为在多线程环境下操作了共享数据,所以出现了线程安全问题;
悲观锁的解决方法就是在修改数据库时上锁(如果获取锁失败就重新查询);
在这里插入图片描述
乐观锁实现方式1:
在这里插入图片描述
乐观锁实现方式2:
CAS(Compare-and-Swap)先比较再替换:就是在上图的基础上,将库存也就是stock去代替version的功能,让被修改的字段本身作为一个判断机制(eg:set stock =stock -1,where id = 10 and stock = 1);
对于这里的秒杀优惠券来说,set stock =stock -1,where id = 10 and stock > 0,更合适,因为只要库存大于0,就可以修改数据库,只要<=0时不能修改数据库就行。
在这里插入图片描述

(二)一人一单

在这里插入图片描述
问题一:
在这里插入图片描述
问题二:
在内部上锁之后,先解锁,解锁的同时其他线程进来的,但是该方法还没执行完毕,也就还没提交事务;导致的情况就是,锁释放了,事务还没提交(也就还没同步数据到数据库中),同样会出现线程安全问题。

问题三:
在这里插入图片描述

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>

事务问题,
视频讲解

实现秒杀后:

    /**
     * 秒杀优惠券下单
     *
     * @param voucherId
     * @return
     */
    @Override
    public Result seckillVoucher(Long voucherId) {
        //1. 查询优惠券信息
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);

        //2. 判断秒杀是否开始
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime beginTime = seckillVoucher.getBeginTime();
        LocalDateTime endTime = seckillVoucher.getEndTime();
        if (beginTime.isAfter(now)) {
            //3. 否,返回异常结果
            return Result.fail("抢购时间未到!");
        }
        if (endTime.isBefore(now)) {
            return Result.fail("抢购时间已结束!");
        }

        //4. 是,判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
            //5. 否,返回异常结果
            return Result.fail("抢购完了,下次再来吧");
        }

        Long userId = UserHolder.getUser().getId();
        //上锁,用户id当锁,这样就实现一个用户一把锁
        synchronized (userId.toString().intern()) {
            //获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
    }

    /**
     * 封装用户下单时的操作
     * @param voucherId
     * @return
     */
    public Result createVoucherOrder(Long voucherId) {
        Long userId = UserHolder.getUser().getId();

        //根据优惠券和用于id查询订单
        int count = this.query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if (count > 0) {
            //如果用户已经下过单
            return Result.fail("同一用户只能下一单!");
        }

        //6. 是,扣减库存
//        seckillVoucher.setStock(seckillVoucher.getStock() - 1);
//        seckillVoucherService.updateById(seckillVoucher);
        //采用乐观锁,解决超卖问题
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId).gt("stock", 0)
                .update();
        if (!success) {
            return Result.fail("库存不足!");
        }
        //7. 创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //7.1 订单id
        long orderId = redisWorker.nextId("order");
        voucherOrder.setId(orderId);
        //7.2 用户id
        voucherOrder.setUserId(userId);
        //7.3 优惠券id
        voucherOrder.setVoucherId(voucherId);
        //8. 返回订单id
        return Result.ok(orderId);
    }

(三)集群或分布式问题(分布式锁)

问题:将项目部署到集群或分布式,每一个集群都启动秒杀项目,这时有多个JVM的存在,每个JVM都有自己的锁,这就导致了每一个锁都有一个线程可以去操作数据库,导致了线程安全问题(集群中的某台机器对应了一个JVM)。
解决思路:让多个JVM只能使用同一把锁,这就需要用到分布式锁;
在这里插入图片描述
在这里插入图片描述

(1)基于Redis的分布式锁

在这里插入图片描述

V1.0

在这里插入图片描述

V2.0

如果线程1获取锁之后的业务阻塞了,锁超时后被释放,释放之后线程业务又醒了并继续执行业务,然后去把锁删除了,但是他本身是没有锁的,删的是线程2的锁,这时就又会出现线程安全问题。
在这里插入图片描述
在这里插入图片描述

V3.0

在这里插入图片描述
解决思路,要让判断锁和释放锁成为一个原子操作(Lua脚本)。
SimpleRedisLock静态代码块加载lua脚本:

    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

SimpleRedisLock重写unlock方法

    @Override
    public void unLock() {
        //调用Lua脚本,解决线程安全问题
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId()
        );
    }

Resouce下新建Lua脚本:

---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by 我和寂世.
--- DateTime: 2023/3/3 17:53
---

if (redis.call('get', KEYS[1]) == ARGV[1]) then
    -- 释放锁
    return redis.call('del', KEYS[1])
end
return 0

在这里插入图片描述

(四)Redisson(工具解决分布式锁问题)

在这里插入图片描述

(1)快速入门

在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值