全局ID生成器:
全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足以下特性
- 唯一性
- 高可用(随时访问随时生成)
- 递增性
- 安全性(不能具有规律性)
- 高性能(生成ID的速度快)
为了增加ID的安全性,我们不会使用redis自增的数值,而是使用拼接一些其他的信息
就算时间戳相同,也是可以在每一秒内支持2^32个订单
唯一ID组成部分:
符号位:1bit,永远为0,表示此ID永远是正数
时间戳:31bit,以秒为单位,可以使用69年
序列号:32bit,秒内的计数器,可以支持2^32个不同ID
- Redis自增ID策略
- 每天一个key,方便统计订单量
- ID构造是时间戳+计数器
@Component public class RedisIDWorker { @Autowired private StringRedisTemplate template; public long NextID(String keyPrefix){ //1.生成时间戳 long current=System.currentTimeMillis(); //2.生成序列号 //2.1获取到日期的时间,让这一天的订单自增,还可以方便做统计 SimpleDateFormat format=new SimpleDateFormat("yyyy:MM:dd"); String data=format.format(System.currentTimeMillis()); long count= template.opsForValue().increment("increment:"+keyPrefix+":"+data); System.out.println(count+"jjjj"); //3.拼接并且返回 return current<<32|count; } }
@SpringBootTest class RedisIDWorkerTest { @Autowired private RedisIDWorker worker; @Test void nextID() throws InterruptedException { CountDownLatch latch=new CountDownLatch(300); Runnable runnable=new Runnable() { @Override public void run() { Long ID= worker.NextID("order业务"); System.out.println(ID); latch.countDown(); } }; ExecutorService service= Executors.newFixedThreadPool(1); for(int i=0;i<300;i++){ service.submit(runnable); } latch.await(); System.out.println("线程池中的任务已经全部完成"); } }
实现获取优惠卷:
1)秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
2)库存是否充足,不足则无法下单
加上事务的原因是因为想要进行修改优惠卷的剩余个数的数据库操作和新增订单的操作要么全部执行成功,要么全部执行失败;
@Controller public class UserController { @Autowired private DemoMapper mapper; @Autowired private RedisIDWorker worker; @RequestMapping("/GetCard") @ResponseBody @Transactional public String GetOrder(Integer cardID,Integer userID){ //在这里面还应该加上用户ID对应的用户是否存在 //1.进行判断优惠卷ID是否存在 Card card=mapper.SelectCardByID(cardID); if(cardID==null){ return "当前优惠卷不存在"; } //2.判断秒杀是否开始 if(new Timestamp(System.currentTimeMillis()).compareTo(card.getStartTime())<0){ return "秒杀活动还没有开始"; } //2.判断秒杀活动是否结束 // if(new Timestamp(System.currentTimeMillis()-500000).compareTo(card.getStartTime())>0){ // return "秒杀活动已经结束"; // } //3.判断代金卷是否还充足 if(card.getCards()<1){ return "当前优惠卷已经没有库存了"; } //4.进行扣减库存 int data= mapper.DecrmentCardData(cardID); //5.进行新创建订单 if(data<1){ return "获取优惠卷失败,优惠卷已经没有库存了"; } Order order=new Order(); order.setOrderID((int) worker.NextID("优惠卷秒杀")); //在实际开发中其实可以在session中获取到userID,当前为了实现方便只是在方法中传递了userID order.setUserID(userID); order.setCardID(cardID); mapper.InsertOrder(order); return "获取优惠卷成功"; } }
关于超卖(优惠卷被减成负数),超卖问题就是典型的多线程并发安全问题,针对这一问题的常见解决方案就是加锁:
假设线程1再进行查询库存和删除库存的过程中,还没有删除库存,那么有其他线程在中间插入一些逻辑就会造成线程安全问题
1)线程1进行查询库存发现库存中只剩下一个优惠卷了
2)此时在线程1进行查询之后更新库存中的数据之前(cards=cards-1)
3)此时线程2进行尝试获取优惠卷,判断当前库存中只是剩下一个优惠卷了,此时线程2的时间片用完了,进入到休眠操作
4)此时线程1开始进行更新库存数据,此时优惠卷已经为0了
5)但是此时线程2被唤醒,因为唤醒之前已经判断过优惠卷还有1个,感知不到线程1进行更新了库存,此时线程2更新数据库,于是优惠卷就被减为了1
update card set cards=cards-1 where cardID=#{cardID}1)悲观锁:
认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行
例如Synchronized、Lock都属于悲观锁
2)乐观锁:认为线程安全不一定会发生,因此不加锁,只是在更新数据时取判断有没有其他线程对数据做了修改
如果没有修改则认为时安全的,自己才更新数据,一般用于更新数据
如果已经被其他线程修改说明发生了安全问题,此时可以重试或异常
关于超卖我们可以加一个乐观锁,乐观锁的关键是判断之前查询得到的数据有被修改过,常见的方式有两种:1)版本号法:顾名思义,就是在修改数据时加入一个version,如果修改的时候version与自己得到的version 不相同,那么就修改失败,可以尝试重试或异常
2)CAS法:就是更改数据,或者删除库存的时候,判断库存是否大于0,如果大于零,则扣除成功
但是如果这样当我们进行模拟100个用户进行并发访问的时候,就会发现:很少的用户能够抢到优惠卷,这是不符合业务逻辑的,但是此时还发现优惠卷的剩余次数还是大于0的,但是用户还抢不到优惠卷,这又是怎么回事呢?
1)假设此时线程1查询库存发现现在库存还有100个优惠卷
2)此时线程2也进行查询库存发现库存还有100个优惠卷
3)此时线程2的时间片用完,进入到阻塞状态
4)此时线程1的用户执行完获取到优惠卷的操作,库存中的优惠卷总数-1
5)此时线程2的用户开始被唤醒,执行获取到优惠卷操作,此时SQL语句执行失败
update card set cards=cards-1 where cardID=#{cardID} and cards=#{count}因为此时总的库存数已经不和刚才线程2查询出来的库存数相等了,所以线程2执行数据库操作失败,所以线程2的用户获取到优惠卷失败,解决方案还是进行修改SQL语句;
update card set cards=cards-1 where cardID=#{cardID}and cards>0
一人一单的功能:在进行更新优惠卷数目的时候根据优惠卷ID和用户ID来进行查询订单,如果查询的订单不为空,那么直接返回false
但是此时做多线程并发访问的时候(用户传入了userID和CardID来去访问请求,结果又发现相同的用户ID下了很多单,于是又发生了线程安全问题;
@Controller public class UserController { @Autowired private DemoMapper mapper; @Autowired private RedisIDWorker worker; @RequestMapping("/GetCard") @ResponseBody @Transactional public String GetOrder(Integer cardID,Integer userID){ //在这里面还应该加上用户ID对应的用户是否存在 //1.进行判断优惠卷ID是否存在 Card card=mapper.SelectCardByID(cardID); if(cardID==null){ return "当前优惠卷不存在"; } //2.判断秒杀是否开始 if(new Timestamp(System.currentTimeMillis()).compareTo(card.getStartTime())<0){ return "秒杀活动还没有开始"; } //2.判断秒杀活动是否结束 // if(new Timestamp(System.currentTimeMillis()-500000).compareTo(card.getStartTime())>0){ // return "秒杀活动已经结束"; // } //3.判断代金卷是否还充足 Order orderDemo=mapper.SelectOrder(userID,cardID); if(orderDemo!=null){ return "您当前已经下过单了,无法再次进行购买"; } int count=card.getCards(); if(count<1){ return "当前优惠卷已经没有库存了"; } //4.进行扣减库存 int data= mapper.DecrmentCardData(cardID); //update card set cards=cards-1 where cardID=#{cardID} and cards=#{count} //在这里面说明如果如果cards的值和上面第三部查询出优惠卷的count值是相等的,那么就直接进行更新操作,否则就执行失败 //5.进行新创建订单 if(data<1){ return "获取优惠卷失败,优惠卷已经没有库存了"; } Order order=new Order(); order.setOrderID((int) worker.NextID("优惠卷秒杀")); order.setUserID(userID); order.setCardID(cardID); mapper.InsertOrder(order); return "获取优惠卷成功"; } }
这个线程安全问题就很好理解了,假设有100个线程来同时进行访问我们的代码,这100个线程同时并行执行(100个线程同时执行代码的每一句),那么就又会同时插入多个订单了
所以我们要给查询出订单到判断订单到插入订单的这些逻辑进行从加上悲观锁
此时我们把新增订单和