全局ID生成器
当用户抢购时,就会生成订单并保存到tb voucher orderi这张表中,而订单表如果使用数据库自增ID就存在一些问题:
●id的规律性太明显
●受单表数据量的限制
完整ID生成器代码:
@Component
public class IDWorker {
private static final long BEGIN_TIMESTAMP=1640995200L;
@Autowired
private StringRedisTemplate stringRedisTemplate;
public long nextId(String prefix){
//生成时间戳
LocalDateTime now = LocalDateTime.now();
long l = now.toEpochSecond(ZoneOffset.UTC);
long timestamp=l - BEGIN_TIMESTAMP;
System.out.println(timestamp);
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
Long count = stringRedisTemplate.opsForValue().increment("icr:" + prefix + ":" + date);
System.out.println(count);
return timestamp <<32 | count;
}
}
结果:生成的订单ID:81466540238569473
下单流程:
代码实现
@Transactional(rollbackFor = Exception.class)
public Result order(Long voucherId) {
SeckillVoucher seckillVoucher = iSeckillVoucherService.getById(voucherId);
LocalDateTime beginTime = seckillVoucher.getBeginTime();
LocalDateTime endTime = seckillVoucher.getEndTime();
//现在时间在秒杀活动之前
if (beginTime.isAfter(LocalDateTime.now())) {
return Result.fail("秒杀活动还没开始!");
}
//现在时间在秒杀活动之后
if (endTime.isBefore(LocalDateTime.now())) {
return Result.fail("秒杀活动已经结束!");
}
//判断库存是否充足
if (seckillVoucher.getStock()<1){
return Result.fail("库存不足");
}
//TODO 库存减1
iSeckillVoucherService.update().setSql("stock=
stock- 1").eq("voucher_id",voucherId).update();
//TODO 创建订单
VoucherOrder order=new VoucherOrder();
long orderID = idWorker.nextId(RedisConstants.SECKILL_STOCK_KEY);
order.setVoucherId(voucherId);
order.setId(orderID);
order.setUserId(UserHolder.getUser().getId());
save(order);
//TODO 返回订单ID
return Result.ok(orderID);
}
在使用乐观锁实现模式中出现了问题,由于判断库存为当前获得的库存,所以导致失败率非常高,所以将其修改为判断条件为库存>0时,就可以下单。
@Transactional(rollbackFor = Exception.class)
public Result order(Long voucherId) {
SeckillVoucher seckillVoucher = iSeckillVoucherService.getById(voucherId);
LocalDateTime beginTime = seckillVoucher.getBeginTime();
LocalDateTime endTime = seckillVoucher.getEndTime();
//现在时间在秒杀活动之前
if (beginTime.isAfter(LocalDateTime.now())) {
return Result.fail("秒杀活动还没开始!");
}
//现在时间在秒杀活动之后
if (endTime.isBefore(LocalDateTime.now())) {
return Result.fail("秒杀活动已经结束!");
}
//判断库存是否充足
if (seckillVoucher.getStock()<1){
return Result.fail("库存不足");
}
//TODO 库存减1
// 在执行减库存操作时,判断是否和查询时的库存一致,一致才可修改
boolean success = iSeckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).
gt("stock", 0).update();
if (!success){
return Result.fail("库存不足,秒杀失败!");
}
//TODO 创建订单
VoucherOrder order=new VoucherOrder();
long orderID = idWorker.nextId(RedisConstants.SECKILL_STOCK_KEY);
order.setVoucherId(voucherId);
order.setId(orderID);
order.setUserId(UserHolder.getUser().getId());
save(order);
//TODO 返回订单ID
return Result.ok(orderID);
}
查看数据库,成功解决超卖问题。
一人一单问题。
下面为实现代码
public Result order(Long voucherId) {
SeckillVoucher seckillVoucher = iSeckillVoucherService.getById(voucherId);
LocalDateTime beginTime = seckillVoucher.getBeginTime();
LocalDateTime endTime = seckillVoucher.getEndTime();
//现在时间在秒杀活动之前
if (beginTime.isAfter(LocalDateTime.now())) {
return Result.fail("秒杀活动还没开始!");
}
//现在时间在秒杀活动之后
if (endTime.isBefore(LocalDateTime.now())) {
return Result.fail("秒杀活动已经结束!");
}
//判断库存是否充足
if (seckillVoucher.getStock()<1){
return Result.fail("库存不足");
}
Long userId = UserHolder.getUser().getId();
System.out.println("====>"+userId);
//TODO 先释放锁,然后再提交事务
//和 spring代理对象不同
synchronized (userId.toString().intern()) {
System.out.println("得到锁了!!!!!!!!!!!!!!");
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.getResult(voucherId);
}
}
@Transactional(rollbackFor = Exception.class)
public Result getResult(Long voucherId) {
Long userId = UserHolder.getUser().getId();
Integer count = query().eq("user_id", UserHolder.getUser().getId()).eq("voucher_id", voucherId).count();
System.out.println(count);
//判断用户是否下过单
if (count > 0) {
return Result.fail("您已经下过订单");
}
//TODO 库存减1
// 在执行减库存操作时,判断是否和查询时的库存一致,一致才可修改
boolean success = iSeckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).
gt("stock", 0).update();
if (!success) {
return Result.fail("库存不足,秒杀失败!");
}
//TODO 创建订单
VoucherOrder order = new VoucherOrder();
long orderID = idWorker.nextId(RedisConstants.SECKILL_STOCK_KEY);
order.setVoucherId(voucherId);
order.setId(orderID);
order.setUserId(userId);
save(order);
//TODO 返回订单ID
return Result.ok(orderID);
}
}
难点为,锁加在哪,以及springboot代理事务机制,springboot通过代理对象来进行事务管理,
所以需要自己去手动代理
但是这只能保证单体架构下一人一单,在做了集群之后,由于synchronized 是由jvm虚拟机维护的锁监视器常量池 ,集群之后jvm不同,所以依然会导致线程安全问题,通过jmeter依然发现一人能下两单。
必须保证setnx和expire具备原子性,要么都成功,要么都失败。
public interface ILock {
//获取锁
boolean tryLock(long timeoutSec);
//释放锁
void unlock();
}
public class Lock implements ILock{
private String name;
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX="lock:";
public Lock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
//获取线程标识
long threadId = Thread.currentThread().getId();
//获取值
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent
(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
return BooleanUtil.isTrue(success);
}
@Override
public void unlock() {
stringRedisTemplate.delete(KEY_PREFIX+name);
}
}