一、全局ID生成器
对于优惠券业务,亦即订单业务,其中的优惠券订单id存储到数据库中时将不采用mysql自增,因为这是一种不安全且当有庞大的订单时又不能保证分布式全局唯一性的做法。因此,就需要有一这样一个生成全局id的角色。
在分布式系统下,为了保证id的唯一性、高可用、高性能、递增性以及安全性,可以采用redis来实现全局id的生成。
基于redis的id生成器,将生成这样的复杂的id:
@Component
public class RedisIdWorker {
private static final long BEGIN_TIMESTAMP=1640995200L;//开始时间戳
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;
//序列号
String date = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
long count = stringRedisTemplate.opsForValue().
increment("icr:" + keyPrefix + ":" + date);
//拼接返回
return timeStamp<<COUNT_BITS|count;
}
}
测试
@Test
public void testIdWorker() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(300);
Runnable task=()->{
for(int i=0;i<100;i++){
long id=redisIdWorker.nextId("order");
System.out.println("id="+id);
}
latch.countDown();
};
long begin = System.currentTimeMillis();
for(int i=0;i<300;i++){
es.submit(task);
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("time:"+(end-begin));
}
二、添加秒杀券
使用postman发送添加优惠券请求,模拟管理平台。
三、秒杀券订单
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
//查优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
//判断秒杀是否开始 是否结束
LocalDateTime beginTime = voucher.getBeginTime();
LocalDateTime endTime = voucher.getEndTime();
//秒杀未开始或结束 返回是错误
if(beginTime.isAfter(LocalDateTime.now())){
return Result.fail("秒杀尚未开始");
}
if (endTime.isBefore(LocalDateTime.now())){
return Result.fail("秒杀已结束");
}
//秒杀开始 判断库存是否充足
//不足 返回错误
if (voucher.getStock()==0){
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();
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);//订单id
voucherOrder.setUserId(UserHolder.getUser().getId());//用户id
voucherOrder.setVoucherId(voucherId);//优惠券id
save(voucherOrder);
//返回订单id
return Result.ok(orderId);
}
四、线程安全——超卖
使用jmeter模拟高并发的秒杀场景,添加200线程。测试发现,优惠券数量出现负数,即超卖问题,因此考虑加锁。
测试结果出现了少量的超卖问题,故可以考虑采用加乐观锁。有两种做法:一是采用加版本号,而是CAS。
修改操作其实是先查询后修改,每次查询同时获取版本号,修改时先对比版本号,若不同则报错,相同则修改同时版本号+1。
也可以不加版本号。用修改字段数据代替版本号,即先对比数据,未修改过再修改。
但是,对于业务来说,只要库存大于0这种并发修改就是没有问题的,而乐观锁则会将并发修改的其余线程全部失效,因此,对于业务来说,要对乐观锁进行修改:在修改数据时不判断数据是否被修改过,而是查询优惠券库存其是否>0.
//优惠券足够 扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock=stock-1")
.eq("voucher_id", voucherId).gt("stock",0)//大于0
.update();
五、线程安全——一人一单
一人一单的实现:当查询完库存充足后,查看券订单中的用户id是否已经存在。
但是同时,当秒杀刚开始时,众多线程同时涌入判断,将都会判定为第一次下单,因此,将整个查询订单—创建订单—扣减库存都加悲观锁。同时进行事务的处理。
@Override
public Result seckillVoucher(Long voucherId) {
//查优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
//判断秒杀是否开始 是否结束
LocalDateTime beginTime = voucher.getBeginTime();
LocalDateTime endTime = voucher.getEndTime();
//秒杀未开始或结束 返回是错误
if(beginTime.isAfter(LocalDateTime.now())){
return Result.fail("秒杀尚未开始");
}
if (endTime.isBefore(LocalDateTime.now())){
return Result.fail("秒杀已结束");
}
//秒杀开始 判断库存是否充足
//不足 返回错误
if (voucher.getStock()<1){
return Result.fail("库存不足");
}
//优惠券足够 创建订单
Long userId = UserHolder.getUser().getId();
synchronized(userId.toString().intern()){
//拿到这个类的代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}//先提交事务再释放悲观锁
}
//若将事务注解加在此处:
// 事务是采用的mapper代理方式实现;
// 调用此方法时 是采用this.方法()是非代理对象 是没有事务功能的
//因此需要在调用处拿到这个方法的代理对象 才能使事务生效
@Transactional
public Result createVoucherOrder(Long voucherId) {
//优惠券足够 查看该用户是否已经下过单
Long userId = UserHolder.getUser().getId();
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if(count>0){
return Result.fail("已经购买过这个券了");
}
// 扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock=stock-1")
.eq("voucher_id", voucherId).gt("stock",0)//大于0
.update();
if(!success){
return Result.fail("库存不足");
}
//创优惠券订单
VoucherOrder voucherOrder = new VoucherOrder();
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);//订单id
voucherOrder.setUserId(userId);//用户id
voucherOrder.setVoucherId(voucherId);//优惠券id
save(voucherOrder);
//返回订单id
return Result.ok(orderId);
}