目录

redis实现全局唯一ID

实现秒杀下单

超卖测试:

超卖问题解决方案:加锁

​编辑

实现一人一单

实现方式第一版

最终版:

集群环境下的并发问题


redis实现全局唯一ID

这里只需要注意一下时间戳的生成方式,注意以UTC计算当前时间秒的方式

    /**
     * 开始时间戳
     */
    private static final long BEGIN_TIMESTAMP = 1772556781L;
    private static final long COUNT_BITS = 32;
    private StringRedisTemplate stringRedisTemplate;
    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
    public long nextId(String keyPrefix) {
        // 1.生成时间戳
        long epochSecond = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
        long timestamp = epochSecond - BEGIN_TIMESTAMP;
        // 2.生成序列号
        // 2.1.获取当前日期,精确到天
        String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 2.2.自增长
        Long increment = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
        // 3.拼接并返回
        return timestamp << COUNT_BITS | increment;
    }

这里最后提到了位运算

举例:最开始时间戳只有高32位这些数字,左移32为后出现了低32位,然后序列号部分就做或运算之后填充进去

实现秒杀下单

为了实现下单功能,我们需要根据路径编写对应的程序

用mybatis-plus就可以快捷的操作每一个表的数据根据如下思路进行编写,不要忘了添加事务管理,因为有两个表的操作

    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    private RedisIdWorker redisIdWorker;
    @Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {
        //查询优惠券信息
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        //判断秒杀是否开始
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())){
            return Result.fail("秒杀尚未开始");
        }
        //判断秒杀是否结束
        if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())){
            return Result.fail("秒杀已经结束");
        }
        //判断是否库存充足
        if (seckillVoucher.getStock() < 1){
            return Result.fail("库存不足");
        }
        //扣减库存
        seckillVoucherService.update().setSql("stock = stock - 1")//相当于sql语句
                .eq("voucher_id", voucherId)//相当于where
                .update();
        //创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        voucherOrder.setUserId(UserHolder.getUser().getId());
        voucherOrder.setVoucherId(voucherId);
        // 6.1.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        save(voucherOrder);
        return Result.ok(orderId);
    }

我们观察一下为什么mybatis-plus能够知道操作哪一个数据库表的问题?

观察这个类,里面是没有自己书写getById这个数据库操作方法的

这里可以知道它继承自IService<SeckillVoucher>

IService 是 MyBatis-Plus 自带的接口,不是 MyBatis 的。
MyBatis vs MyBatis-Plus
重要区别:
MyBatis:原生框架,需要自己写 DAO 接口和 XML 映射
MyBatis-Plus:在 MyBatis 基础上的增强工具,提供了 IService 等通用服务

这里泛型SeckillVoucher中使用了@TableName注解,告诉了我们mybatis-plus操作的数据库

超卖测试:

用ApacheJmeter测试超卖问题,当库存只剩一个的时候,多个线程同时点击就会出现超卖的情况

在测试计划里添加http请求头管理器和cookie管理器

结构如图

根据cookie和authorization(也就是token)配置请求头的数据才能实现测试

库存有100但发送了200个请求,显示-9个数说明存在超卖问题

超卖问题解决方案:加锁

乐观锁使用方案:在设计数据时设计一个版本号,在更新数据后就版本号加1,在线程更新数据时就检测版本是否和一开始的版本一致,一致则说明没有其他线程操作数据,可以正常改,若不同就不改

CAS(比较)法:因为版本号的作用就是看是否数据进行过修改,那么我们去掉版本号只对比库存的数量就可以知道了

代码实现:

这里如果用eq的话会出现在多个线程同时查询到同一个stock时,就会有多个线程失败,所以失败率较高

所以只需要让stock数量大于0就行

实现一人一单

为了防止黄牛的情况出现,需要做到一人一单的情况

实现方式第一版

直接在扣减库存前查询是否存在这个用户

测试发现异常量比预期低,因为只能有一个成功

这是因为在同一时刻,同一个用户多个线程同时下单,在最快的线程把订单添加到数据库之前,有其他线程也下单了,所以会有线程安全问题,我们的解决方案是加悲观锁,因为这里不是读数据而是写数据

最终版:

把后3步提取出来,获取代理对象,才能调用使用了事务管理的方法

注意

1.只在方法内加锁的话,在释放锁的时候,方法的事务还没提交,也就是还没有写进数据库,若此时其他线程访问,此时会有线程安全问题

2.不在方法的声明上加锁的原因:只会让一个线程运行,其他用户就无法下单。不能实现一人一单

集群环境下的并发问题

在单体项目解决了一人一单的情况后,我们需要解决集群下并发问题

只需要在idea中设置多个端口同时运行,就可以模拟集群环境

有关锁失效原因分析

多个集群下有不同的jvm,一个jvm中有一个锁监视器,就只能监管一把锁,多个集群的情况下,就有多个锁监视器,所以每一个集群都有一个可以运行的线程

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐