推荐家居企业网站建设,构建大型网站,wordpress文章头图,中国建设银行吉林分行网站全局ID生成器: 全局ID生成器#xff0c;是一种在分布式系统下用来生成全局唯一ID的工具#xff0c;一般要满足以下特性 唯一性高可用(随时访问随时生成)递增性安全性(不能具有规律性)高性能(生成ID的速度快) 为了增加ID的安全性#xff0c;我们不会使用redis自增的数值是一种在分布式系统下用来生成全局唯一ID的工具一般要满足以下特性 唯一性高可用(随时访问随时生成)递增性安全性(不能具有规律性)高性能(生成ID的速度快) 为了增加ID的安全性我们不会使用redis自增的数值而是使用拼接一些其他的信息 就算时间戳相同也是可以在每一秒内支持2^32个订单 唯一ID组成部分 符号位1bit永远为0表示此ID永远是正数 时间戳31bit以秒为单位可以使用69年 序列号32bit秒内的计数器可以支持2^32个不同ID Redis自增ID策略 每天一个key方便统计订单量ID构造是时间戳计数器 Component
public class RedisIDWorker {Autowiredprivate StringRedisTemplate template;public long NextID(String keyPrefix){//1.生成时间戳long currentSystem.currentTimeMillis();//2.生成序列号//2.1获取到日期的时间,让这一天的订单自增,还可以方便做统计SimpleDateFormat formatnew SimpleDateFormat(yyyy:MM:dd);String dataformat.format(System.currentTimeMillis());long count template.opsForValue().increment(increment:keyPrefix:data);System.out.println(countjjjj);//3.拼接并且返回return current32|count;}}SpringBootTest
class RedisIDWorkerTest {Autowiredprivate RedisIDWorker worker;Testvoid nextID() throws InterruptedException {CountDownLatch latchnew CountDownLatch(300);Runnable runnablenew Runnable() {Overridepublic void run() {Long ID worker.NextID(order业务);System.out.println(ID);latch.countDown();}};ExecutorService service Executors.newFixedThreadPool(1);for(int i0;i300;i){service.submit(runnable);}latch.await();System.out.println(线程池中的任务已经全部完成);}
} 实现获取优惠卷: 1)秒杀是否开始或结束如果尚未开始或已经结束则无法下单 2)库存是否充足不足则无法下单 加上事务的原因是因为想要进行修改优惠卷的剩余个数的数据库操作和新增订单的操作要么全部执行成功要么全部执行失败 Controller
public class UserController {Autowiredprivate DemoMapper mapper;Autowiredprivate RedisIDWorker worker;RequestMapping(/GetCard)ResponseBodyTransactionalpublic String GetOrder(Integer cardID,Integer userID){//在这里面还应该加上用户ID对应的用户是否存在//1.进行判断优惠卷ID是否存在Card cardmapper.SelectCardByID(cardID);if(cardIDnull){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(data1){return 获取优惠卷失败,优惠卷已经没有库存了;}Order ordernew Order();order.setOrderID((int) worker.NextID(优惠卷秒杀));//在实际开发中其实可以在session中获取到userID,当前为了实现方便只是在方法中传递了userIDorder.setUserID(userID);order.setCardID(cardID);mapper.InsertOrder(order);return 获取优惠卷成功;}
}关于超卖(优惠卷被减成负数)超卖问题就是典型的多线程并发安全问题针对这一问题的常见解决方案就是加锁 假设线程1再进行查询库存和删除库存的过程中还没有删除库存那么有其他线程在中间插入一些逻辑就会造成线程安全问题 1)线程1进行查询库存发现库存中只剩下一个优惠卷了 2)此时在线程1进行查询库存之后更新库存中的数据之前(cardscards-1) 3)此时线程2进行尝试获取优惠卷判断当前库存中只是剩下一个优惠卷了此时线程2的时间片用完了进入到休眠操作 4)此时线程1开始进行更新库存数据此时优惠卷已经为0了 5)但是此时线程2被唤醒因为唤醒之前已经判断过优惠卷还有1个感知不到线程1进行更新了库存此时线程2更新数据库于是优惠卷就被减为了1 update card set cardscards-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 cardscards-1 where cardID#{cardID} and cards#{count} 因为此时总的库存数已经不和刚才线程2查询出来的库存数相等了所以线程2执行数据库操作失败所以线程2的用户获取到优惠卷失败解决方案还是进行修改SQL语句 update card set cardscards-1 where cardID#{cardID}and cards0 一人一单的功能:在进行更新优惠卷数目的时候根据优惠卷ID和用户ID来进行查询订单如果查询的订单不为空说明之前用户已经下过单了那么直接返回false 但是此时做多线程并发访问的时候(用户传入了userID和CardID来去访问请求结果又发现相同的用户ID下了很多单于是又发生了线程安全问题
Controller
public class UserController {Autowiredprivate DemoMapper mapper;Autowiredprivate RedisIDWorker worker;RequestMapping(/GetCard)ResponseBodyTransactionalpublic String GetOrder(Integer cardID,Integer userID){//在这里面还应该加上用户ID对应的用户是否存在//1.进行判断优惠卷ID是否存在Card cardmapper.SelectCardByID(cardID);if(cardIDnull){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 orderDemomapper.SelectOrder(userID,cardID);if(orderDemo!null){return 您当前已经下过单了,无法再次进行购买;}int countcard.getCards();if(count1){return 当前优惠卷已经没有库存了;}//4.进行扣减库存int data mapper.DecrmentCardData(cardID);//update card set cardscards-1 where cardID#{cardID} and cards#{count}//在这里面说明如果如果cards的值和上面第三部查询出优惠卷的count值是相等的,那么就直接进行更新操作,否则就执行失败//5.进行新创建订单if(data1){return 获取优惠卷失败,优惠卷已经没有库存了;}Order ordernew Order();order.setOrderID((int) worker.NextID(优惠卷秒杀));order.setUserID(userID);order.setCardID(cardID);mapper.InsertOrder(order);return 获取优惠卷成功;}
}这个线程安全问题就很好理解了假设有100个线程来同时进行访问我们的代码这100个线程同时并行执行(100个线程同时执行代码的每一句)那么就又会同时插入多个订单了 所以我们要给查询出订单到判断订单到插入订单的这些逻辑进行从加上悲观锁 此时我们把新增订单和查询订单和修改优惠卷的业务给加锁但是上面锁的是this锁的是当前对象但是锁的粒度很大其实只有当两个userID相同的用户来进行竞争锁就可以了下面的代码锁定范围就变小了程序的执行性能也就变高了 但是上面的代码仍然是存在线程安全问题的由于此方法是添加事务操作的必须是先释放锁再去提交事务但是释放锁并提交事务的这个操作并不是原子的所以说在释放锁并提交事务的过程中该线程还没有提交订单如果有其他线程(相同的userID)来进行抢优惠卷的操作那么一定会发生线程安全问题所以提交的方案应该是该线程提交事务之后再去释放锁 但是上面的代码还是存在着问题在单体服务项目中加锁方式是可行的但是在分布式环境中这种方式就是不可行的锁在JVM底层是依靠ObjectMonitor来进行实现的因为在分布式项目中有着不同的机器不同的机器存在着不同的JVM同时不同的机器就有不同的分布式锁所以要想解决这个问题只能让多台机器使用同一把锁多个JVM使用同一把锁