网站建设价格差异好大,网站建设经济可行性,百度应用商店,wordpress 大网站全局唯一ID
唯一ID的必要性
每个店铺都可以发布优惠券#xff1a; 当用户抢购时#xff0c;就会生成订单并保存到tb_voucher_order这张表中#xff0c;而订单表如果使用数据库自增ID就存在一些问题#xff1a; id的规律性太明显#xff0c;容易被用户根据id的间隔来猜测…全局唯一ID
唯一ID的必要性
每个店铺都可以发布优惠券 当用户抢购时就会生成订单并保存到tb_voucher_order这张表中而订单表如果使用数据库自增ID就存在一些问题 id的规律性太明显容易被用户根据id的间隔来猜测到销量等商业信息不够保密 受单表数据量的限制mysql的id自增长有数值约束且数据量大的情况下会进行分库分表表不同自增长id可能相同在分布式系统中是不允许的
全局ID生成器是一种在分布式系统下用来生成全局唯一ID的工具一般要满足下列特性 Redis恰好满足以上特性为了增加ID的安全性我们可以不直接使用Redis自增的数值而是拼接一些其它信息
ID的组成部分符号位1bit永远为0
时间戳31bit以秒为单位可以使用69年
序列号32bit秒内的计数器支持每秒产生2^32个不同ID这个序列号足够大几乎不可能到达极限 redis实现全局唯一ID
获取当前时间戳的秒数
LocalDateTime time LocalDateTime.of(2023, 9, 2, 0, 0, 0);long of time.toEpochSecond(ZoneOffset.UTC);
生成序列号自增长的key为了防止一直使用该key最后导致达到redis的上限故需要拼接上日期既防止达到上限又能方便统计同一天的下单量 //开始时间戳秒数private static final long BEGIN_TIMESTAMP 1693612800L;//序列号位数private static final int COUNT_BITS 32;private StringRedisTemplate stringRedisTemplate;public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate stringRedisTemplate;}public long nextId(String keyPrefix) {//生成时间戳LocalDateTime now LocalDateTime.now();long nowSecond now.toEpochSecond(ZoneOffset.UTC);long timestamp nowSecond - BEGIN_TIMESTAMP;//利用redis的自增生成序列号String date now.format(DateTimeFormatter.ofPattern(yyyy:MM:dd));Long increment stringRedisTemplate.opsForValue().increment(icr: keyPrefix : date);//拼接return timestamp COUNT_BITS | increment;}
添加优惠券
每个店铺都可以发布优惠券分为平价券和特价券。平价券可以任意购买而特价券需要秒杀抢购 平价卷由于优惠力度并不是很大所以是可以任意领取
而代金券由于优惠力度大所以像第二种卷就得限制数量特价卷除了具有优惠卷的基本信息以外还具有库存抢购时间结束时间等等字段
添加特价券
{shopId:1,
title:100元代金券,
subTitle:周一至周五均可使用,
rules:全场通用\\n无需预约\\n可无限叠加\\不兑现、不找零\\n仅限堂食,
payValue:8000,
actualValue:10000,
type:1,
stock:100,
beginTime:2023-09-02T10:09:17,
endTime:2023-09-26T12:09:04
}
由于没有后台管理系统故使用postman进行post请求添加需要关闭拦截器同时设置有效的开始时间和结束时间优惠券才会显示 实现秒杀下单
下单核心思路当我们点击抢购时会触发右侧的请求我们只需要编写对应的controller即可service层编写对应的代码操作数据库即可 下单时需要判断两点 秒杀是否开始或结束如果尚未开始或已经结束则无法下单 库存是否充足不足则无法下单
下单核心逻辑分析
当用户开始进行下单我们应当去查询优惠卷信息查询到优惠卷信息判断是否满足秒杀条件
比如时间是否充足如果时间充足则进一步判断库存是否足够如果两者都满足则扣减库存创建订单然后返回订单id如果有一个条件不满足则直接结束 代码实现
由于涉及到优惠券表和优惠券订单表两张表的dml操作需要加上Transactional声明事务
TransactionalOverridepublic 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(库存不足);}boolean success seckillVoucherService.update().setSql(stock stock -1).eq(voucher_id, voucherId).update();if (!success) {return Result.fail(库存不足);}VoucherOrder voucherOrder new VoucherOrder();//订单idlong orderId redisIdWorker.nextId(order);//用户idLong userId UserHolder.getUser().getId();voucherOrder.setUserId(userId);voucherOrder.setVoucherId(voucherId);voucherOrder.setId(orderId);save(voucherOrder);return Result.ok(orderId);}
超卖问题
模拟实现
使用jmeter模拟实现注意带上请求头authorization值为登录时的token的key 从数据库的库存中我们可以看到已经出现了超卖现象库存出现了负数 超卖原因
我们原有的代码是这么写的
if (voucher.getStock() 1) {// 库存不足return Result.fail(库存不足);}//5扣减库存boolean success seckillVoucherService.update().setSql(stock stock -1).eq(voucher_id, voucherId).update();if (!success) {//扣减库存return Result.fail(库存不足);}
假设线程1过来查询库存判断出来库存大于1正准备去扣减库存但是还没有来得及去扣减此时线程2过来线程2也去查询库存发现这个数量一定也大于1那么这两个线程都会去扣减库存最终多个线程相当于一起去扣减库存此时就会出现库存的超卖问题。 解决方案
超卖问题是典型的多线程安全问题针对这一问题的常见解决方案就是加锁而对于加锁我们通常有两种解决方案见下图 悲观锁 悲观锁可以实现对于数据的串行化执行比如syn和lock都是悲观锁的代表同时悲观锁中又可以再细分为公平锁非公平锁可重入锁等等 乐观锁 乐观锁会有一个版本号每次操作数据会对版本号1再提交回数据时会去校验是否比之前的版本大1 如果大1 则进行操作成功这套机制的核心逻辑在于如果在操作过程中版本号只比原来大1 那么就意味着操作过程中没有人对他进行过修改他的操作就是安全的如果不大1则数据被修改过当然乐观锁还有一些变种的处理方式比如cas即查值进行比对发现值没有被修改认为线程安全进行修改值解决线程安全问题 解决方案实现
采用乐观锁方案对于优惠券库存我们并需要设置版本号因为查询到的库存和最后修改数据时再查第二遍库存后我们只需要将这两次库存量进行比较就能知道库存是否被修改过即线程是否安全且为了性能我们会将修改数据时设置的条件并不需要两次库存完全相同只需要在进行修改时加上库存大于0的条件即可上面代码只需要修改此处即可
boolean success seckillVoucherService.update().setSql(stock stock -1).eq(voucher_id, voucherId).gt(stock,0).update();
开了两百个线程之后异常率达到完美的50%同时数据库数据正常 一人一单
优惠卷是为了引流但是目前的情况是一个人可以无限制的抢这个优惠卷所以我们应当增加一层逻辑让一个用户只能下一个单而不是让一个用户下多个单
具体操作逻辑如下比如时间是否充足如果时间充足则进一步判断库存是否足够然后再根据优惠卷id和用户id查询是否已经下过这个订单如果下过这个订单则不再下单否则进行下单 初步实现在扣减库存前查询订单表该用户是否已经下过单 TransactionalOverridepublic 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(库存不足);}Long userId UserHolder.getUser().getId();// 5.1.查询订单int count query().eq(user_id, userId).eq(voucher_id, voucherId).count();// 5.2.判断是否存在if (count 0) {// 用户已经购买过了return Result.fail(用户已经购买过一次);}//扣减库存boolean success seckillVoucherService.update().setSql(stock stock -1).eq(voucher_id, voucherId).gt(stock,0).update();if (!success) {return Result.fail(库存不足);}VoucherOrder voucherOrder new VoucherOrder();//订单idlong orderId redisIdWorker.nextId(order);voucherOrder.setUserId(userId);voucherOrder.setVoucherId(voucherId);voucherOrder.setId(orderId);save(voucherOrder);return Result.ok(orderId);} 还是出现了一人多张优惠券订单的情况 存在问题现在的问题还是和之前一样并发过来查询数据库都不存在订单所以我们还是需要加锁但是乐观锁比较适合更新数据而现在是插入数据所以我们需要使用悲观锁操作可以直接在方法上直接加上synchronized 锁来解决 这样添加锁锁的粒度太粗了在使用锁过程中控制锁粒度 是一个非常重要的事情因为如果锁的粒度太大会导致每个线程进来都会锁住所以我们需要去控制锁的粒度可以将用户下单的代码封装成一个方法对该业务进行上锁将锁的范围缩小同时由于spring的事务必须等到锁释放之后才会提交如果锁释放之后有别的线程进入下单业务而此时spring事务尚未提交这就会造成订单尚未写入数据库该线程仍会查到无订单继续进行下单操作无法解决线程安全问题所以我们要先提交事务才能释放锁就能避免该问题。 最终实现
由于createVoucherOrder()要受事务控制要注入IVoucherOrderService拿到代理对象通过该代理对象调用该方法事务才能生效为了使事务提交在释放锁之前可以将锁直接锁死事务方法。 Autowiredprivate ISeckillVoucherService seckillVoucherService;Autowiredprivate RedisIdWorker redisIdWorker;Autowiredprivate IVoucherOrderService voucherOrderService;Overridepublic 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(库存不足);}Long userId UserHolder.getUser().getId();synchronized (userId.toString().intern()) {return voucherOrderService.createVoucherOrder(voucherId);}}Transactionalpublic Result createVoucherOrder(Long voucherId) {Long userId UserHolder.getUser().getId();// 5.1.查询订单int count query().eq(user_id, userId).eq(voucher_id, voucherId).count();// 5.2.判断是否存在if (count 0) {// 用户已经购买过了return Result.fail(用户已经购买过一次);}//扣减库存boolean success seckillVoucherService.update().setSql(stock stock -1).eq(voucher_id, voucherId).gt(stock, 0).update();if (!success) {return Result.fail(库存不足);}VoucherOrder voucherOrder new VoucherOrder();//订单idlong orderId redisIdWorker.nextId(order);voucherOrder.setUserId(userId);voucherOrder.setVoucherId(voucherId);voucherOrder.setId(orderId);save(voucherOrder);return Result.ok(orderId);}