一、全局ID生成器
1.1 概念
订单表的订单号使用数据库自增id会存在一些问题:
(1)id规律性太明显,会暴露一些信息如下单数量。
(2)受表单数据量影响(时间长了几百万、几千万的订单)。
全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具。具有以下特点:
(1)唯一性;(2)高可用(稳定);(3)高性能(生成快);(4)递增性(有利于数据库创建索引);(5)安全性(不会暴露信息)。
使用Redis来满足上面的要求:
(1)使用redis的string的自增(incr)来保证唯一性。
(2)使用redis的集群、哨兵策略来保证高可用。
(3)redis本身性能高。
(4)使用redis的string的自增(incr)。
(5)使用redis保证安全性的方案如下(使用String的Long型):
当然,redis不是全局ID的唯一方案,还有其他很多方案。
1.2 Redis自增实现
@ComponentpublicclassRedisIdWorker{privatestaticfinallongBEGIN_TIMESTAM=1640995200L;privateStringRedisTemplatestringRedisTemplate;/* *序列号位数 */privatefinalintCOUNT_BITS=32;publicRedisIdWorker(StringRedisTemplatestringRedisTemplate){this.stringRedisTemplate=stringRedisTemplate;}publiclongnextId(StringkeyPrefix){//1.生成时间戳LocalDateTimenow=LocalDateTime.now();longnowSecond=now.toEpochSecond(ZoneOffset.UTC);longtimestamp=nowSecond-BEGIN_TIMESTAM;//2.生成序列号Stringdate=now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));longcount=stringRedisTemplate.opsForValue().increment("icr:"+keyPrefix+":"+date);//3.拼接并返回returntimestamp<<COUNT_BITS|count;}}其他的全局唯一ID生成策略:
UUID
snowflake算法
数据库自增
二、线程并发安全问题
2.1 秒杀下单功能(一般实现)
2.1.1 功能点描述
2.1.2 代码实现
@ServicepublicclassVoucherOrderServiceImplextendsServiceImpl<VoucherOrderMapper,VoucherOrder>implementsIVoucherOrderService{@ResourceprivateISeckillVoucherServiceseckillVoucherService;@ResourceprivateRedisIdWorkerredisIdWorker;@Override@TransactionalpublicResultseckillVoucher(LongvoucherId){// 1.查询优惠券SeckillVouchervoucher=seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if(voucher.getBeginTime().isAfter(LocalDateTime.now())){// 尚未开始returnResult.fail("秒杀尚未开始!");}// 3.判断秒杀是否已经结束if(voucher.getEndTime().isBefore(LocalDateTime.now())){// 尚未开始returnResult.fail("秒杀已经结束!");}// 4.判断库存是否充足if(voucher.getStock()<1){// 库存不足returnResult.fail("库存不足!");}//5.扣减库存booleansuccess=seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id",voucherId).update();if(!success){returnResult.fail("库存不足");}// 6.创建订单VoucherOrdervoucherOrder=newVoucherOrder();// 6.1.订单idlongorderId=redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 6.2.用户idlonguserId=UserHolder.getUser().getId();voucherOrder.setUserId(userId);// 7.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 7.返回订单idreturnResult.ok(orderId);}}启动服务测试:
补充:使用jmeter模拟真实秒杀场景进行测试
上面的测试方法是在前端页面手动点击的,但现实场景中会有多个并发请求,可以使用jmeter进行模拟。
注意:要在请求头里添加authorization
可以查到刚才请求的:
问题分析:
正常情况下:
异常情况:
引发了并发安全问题,解决方法是锁机制。
2.1.3 悲观锁与乐观锁
悲观锁方法简单,下面说明实现乐观锁的方法,常见的两种方法是版本号法(表有一个字段为版本号,将版本号的值作为更新的条件)和CAS法(用库存数量代替版本号字段,原理与版本号相同)。
booleansuccess=seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id",voucherId).eq("stock",voucher.getStock()).update();重启服务,删除数据库数据进行重试:
结果:
原因:因版本号字段前后不一致更新失败的线程下单失败,失败率非常高。
解决失败率高的问题:
把版本号前后一致的条件改为库存大于0.
重启服务,删除数据库数据进行测试:
补充:乐观锁的缺点
乐观锁要访问数据库,对数据库的压力还是很大的,对于高并发场景还是有缺点的。
2.2 一人一单功能
目标:一个用户只能抢购一张优惠券。
2.2.1 实现思路
2.2.2 代码实现
// 一人一单LonguserId=UserHolder.getUser().getId();intcount=query().eq("user_id",userId).eq("voucher_id",voucherId).count();// 5.2.判断是否存在if(count>0){// 用户已经购买过了returnResult.fail("用户已经购买过一次!");}重启服务,删除数据库数据进行测试:
还是发生了线程安全问题。但这里不能使用乐观锁方案,只能使用悲观锁方案,将“是否下过单,更新库存,下单”的过程上锁。
2.2.3 解决线程安全问题(悲观锁)
@ServicepublicclassVoucherOrderServiceImplextendsServiceImpl<VoucherOrderMapper,VoucherOrder>implementsIVoucherOrderService{@ResourceprivateISeckillVoucherServiceseckillVoucherService;@ResourceprivateRedisIdWorkerredisIdWorker;@OverridepublicResultseckillVoucher(LongvoucherId){// 1.查询优惠券SeckillVouchervoucher=seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if(voucher.getBeginTime().isAfter(LocalDateTime.now())){// 尚未开始returnResult.fail("秒杀尚未开始!");}// 3.判断秒杀是否已经结束if(voucher.getEndTime().isBefore(LocalDateTime.now())){// 尚未开始returnResult.fail("秒杀已经结束!");}// 4.判断库存是否充足if(voucher.getStock()<1){// 库存不足returnResult.fail("库存不足!");}returncreateVoucherOrder(voucherId);}@TransactionalpublicResultcreateVoucherOrder(LongvoucherId){// 一人一单LonguserId=UserHolder.getUser().getId();synchronized(userId.toString().intern()){//intern()可以保证对象唯一(toString方法底层是new一个String,即时值相同也不是同一个对象),去字符串常量池找值一样的字符串的地址返回intcount=query().eq("user_id",userId).eq("voucher_id",voucherId).count();// 5.2.判断是否存在if(count>0){// 用户已经购买过了returnResult.fail("用户已经购买过一次!");}//5.扣减库存booleansuccess=seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id",voucherId).gt("stock",0).update();if(!success){returnResult.fail("库存不足");}// 6.创建订单VoucherOrdervoucherOrder=newVoucherOrder();// 6.1.订单idlongorderId=redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 6.2.用户id//long userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);// 7.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 7.返回订单idreturnResult.ok(orderId);}}}但上述代码还是有问题,因为createVoucherOrder()里的锁是在方法执行完,事务提交前释放的,所以可能出现一个线程释放了锁没提交事务(未写入数据库),另一个线程查询不到更新结果导致线程安全问题。
补充:悲观锁写法优化
但上面的写法会发生事务失效的问题,这里把老师原话写在下面(是的,我还不懂):
createVoucherOrder()方法加了事务,但外边的seckillVoucher()没有加事务,createVoucherOrder()方法调用拿到的是当前的VoucherOrderServiceImpl对象,而不是它的代理对象。事务要想生效是因为spring对类做了动态代理,获取了类(这里是VoucherOrderServiceImpl)的代理对象,用代理对象做的事务处理,当前的VoucherOrderServiceImpl对象不是代理对象,无事务功能。
举个例子可能会理解一些:
不会失效的例子1:
@ServicepublicclassOrderService{@AutowiredprivateUserServiceuserService;// 这里注入的是代理对象publicvoidplaceOrder(){userService.saveUser();// 通过代理调用}}@ServicepublicclassUserService{@TransactionalpublicvoidsaveUser(){// 业务逻辑}}会失效的例子1:
publicinterfaceIOrderService{publicvoidplaceOrder();}@ServicepublicclassOrderServiceImplimplement