全局ID生成器:
是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:
为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息:
Redis自增ID策略:
/**
* 开始时间戳
*/
private static final long BEGIN_TIMESTAMP = 1640995200;
/**
* 序列号的位数
*/
private static final int COUNT_BITS = 32;
/**
* id生成策略
*
* @param keyPrefix 业务前缀
* @return
*/
@Resource
private StringRedisTemplate stringRedisTemplate;
public long nextId(String keyPrefix) {
//1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
//2.生成序列号
//2.1获取当前日期,精确到天
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
//2.2自增长
Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
//3.拼接并返回
return timestamp << COUNT_BITS | count;
}
public static void main(String[] args) {
LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
long second = time.toEpochSecond(ZoneOffset.UTC);
System.out.println(second);
}
测试ID自增生成策略:
500个线程每一个线程生成100id一共50000个id花的时间:
private ExecutorService service = Executors.newFixedThreadPool(500);
@Test
void testIdWorker() throws InterruptedException {
CountDownLatch latch =new CountDownLatch(500);
Runnable task = ()->{
for (int i=0;i<100;i++){
long id= redisIdWorker.nextId("order");
System.out.println("id=" +id);
}
latch.countDown();
};
long start=System.currentTimeMillis();
for (int i=0;i<500;i++){
service.submit(task);
}
latch.await();
long end=System.currentTimeMillis();
System.out.println("end=" +(end-start));
}
每个店铺都可以发布优惠卷:
当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就会存在一些问题:
全局唯一ID生成策略:
实现优惠券秒杀下单
每个店铺都可以发布优惠券,分为平价卷和特价卷。平价劵可以任意购买,而特价劵需要秒杀抢购:
表关系如下:
在VoucherController中提供一个接口,可以添加秒杀优惠券:http://localhost:8081/voucher/seckill
{
"shopId":1,
"title":"100元代金券",
"subTitle":"周一至周五均可使用",
"rules": "全场通用\\n无需预约\\n可无限叠加\\不兑换、不找零\\n仅限堂食",
"payValue": 8000,
"actualValue": 10000,
"type": 1,
"stock": 100,
"beginTime": "2022-10-25T12:09:04",
"endTime": "2022-12-25T12:09:04"
}
可以通过postman调用
实现秒杀下单:
下单时需要判断两点:
库存超卖问题分析:
超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:
乐观锁:
版本号法
CAS法(CompareAndSet)
乐观锁解决超卖问题:
//1.查询优惠劵
SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
//2.判断秒杀是否开始
LocalDateTime beginTime = voucher.getBeginTime();
if (beginTime.isAfter(LocalDateTime.now())) {
//尚未开始
return Result.fail("活动尚未开始");
}
//3.判断秒杀是否已经结束
LocalDateTime endTime = voucher.getEndTime();
if (LocalDateTime.now().isAfter(endTime)) {
//已结束
return Result.fail("活动已经结束");
}
//4判断库存是否充足
if (voucher.getStock() < 1) {
//库存不足
return Result.fail("库存不足!");
}
//5.扣减库存
boolean success =iSeckillVoucherService.update()
.setSql("stock =stock -1")
.eq("voucher_id",voucherId).gt("stock",0).
update();
if (!success) {
//扣减失败
return Result.fail("库存不足!");
}
//6.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//6.1 订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//6.2 用户id
Long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
//6.3 代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// 7.返回订单id
return Result.ok(orderId);
超卖这样的线程安全问题,解决方案有哪些?
1.悲观锁:添加同步锁,让线程串行执行
2.乐观锁:不加锁,在更新时判读是否有其它线程在修改
一人一单:
需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单
@Autowired
private ISeckillVoucherService iSeckillVoucherService;
@Autowired
private RedisIdWorker redisIdWorker;
@Override
public Result seckillVoucher(Long voucherId) {
//1.查询优惠劵
SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
//2.判断秒杀是否开始
LocalDateTime beginTime = voucher.getBeginTime();
if (beginTime.isAfter(LocalDateTime.now())) {
//尚未开始
return Result.fail("活动尚未开始");
}
//3.判断秒杀是否已经结束
LocalDateTime endTime = voucher.getEndTime();
if (LocalDateTime.now().isAfter(endTime)) {
//已结束
return Result.fail("活动已经结束");
}
//4判断库存是否充足
if (voucher.getStock() < 1) {
//库存不足
return Result.fail("库存不足!");
}
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()){
//获取spring事务代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
//6.一个人一单
Long userId = UserHolder.getUser().getId();
//6.1查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
//6.2判断是否存在
if (count > 0) {
//用户以及购买过
return Result.fail("用户已经购买过一次");
}
//7.扣减库存
boolean success = iSeckillVoucherService.update()
.setSql("stock =stock -1")
.eq("voucher_id", voucherId)
.gt("stock", 0).update();
if (!success) {
//扣减失败
return Result.fail("库存不足!");
}
//8.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//8.1 订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//8.2 用户id
voucherOrder.setUserId(userId);
//8.3 代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// 9.返回订单id
return Result.ok(orderId);
}
一人一单的并发安全问题:
通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了。
1.我们将服务启动两份,端口分别为8081和8082:
2.修改nginx的conf目录下的nginx.conf文件,配置反向代理和负载均衡:
现在,用户请求会在这两个节点上负载均衡,再次测试下是否存在线程安全问题。
一人一单的并发安全问题: