学校英文版网站建设方案,瑞安做企业网站找哪家,利用wordpress漏洞地址,图片设计软件有哪些文章目录 1 全局唯一ID的需求分析2 Redis实现全局唯一Id3 添加优惠卷4 实现秒杀下单5 库存超卖问题分析6 乐观锁解决超卖问题6 悲观锁实现一人一单7 集群环境下的并发问题 1 全局唯一ID的需求分析
每个店铺都可以发布优惠券#xff1a; 当用户抢购时#xff0c;就会生成订单… 文章目录 1 全局唯一ID的需求分析2 Redis实现全局唯一Id3 添加优惠卷4 实现秒杀下单5 库存超卖问题分析6 乐观锁解决超卖问题6 悲观锁实现一人一单7 集群环境下的并发问题 1 全局唯一ID的需求分析
每个店铺都可以发布优惠券 当用户抢购时就会生成订单并保存到tb_voucher_order这张表中而订单表如果使用数据库自增ID就存在一些问题
id的规律性太明显受单表数据量的限制
场景分析如果我们的id具有太明显的规则用户或者说商业对手很容易猜测出来我们的一些敏感信息比如商城在一天时间内卖出了多少单这明显不合适。
场景分析二随着我们商城规模越来越大mysql的单表的容量不宜超过500W数据量过大之后我们要进行拆库拆表但拆分表了之后他们从逻辑上讲他们是同一张表所以他们的id是不能一样的 于是乎我们需要保证id的唯一性。
全局ID生成器是一种在分布式系统下用来生成全局唯一ID的工具一般要满足下列特性 为了增加ID的安全性我们可以不直接使用Redis自增的数值而是拼接一些其它信息 ID的组成部分符号位1bit永远为0
时间戳31bit以秒为单位可以使用69年
序列号32bit秒内的计数器支持每秒产生 2 32 2^{32} 232个不同ID
2 Redis实现全局唯一Id
Component
public class RedisIdWorker {/*** 开始时间戳*/private static final long BEGIN_TIMESTAMP 1640995200L;/*** 序列号的位数*/private static final int COUNT_BITS 32;private StringRedisTemplate stringRedisTemplate;public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {this.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;}
}测试类
知识小贴士关于countdownlatch
countdownlatch名为信号枪主要的作用是同步协调在多线程的等待于唤醒问题
我们如果没有CountDownLatch 那么由于程序是异步的当异步程序没有执行完时主线程就已经执行完了然后我们期望的是分线程全部走完之后主线程再走所以我们此时需要使用到CountDownLatch
CountDownLatch 中有两个最重要的方法
1、countDown
2、await
await 方法 是阻塞方法我们担心分线程没有执行完时main线程就先执行所以使用await可以让main线程阻塞那么什么时候main线程不再阻塞呢当CountDownLatch 内部维护的 变量变为0时就不再阻塞直接放行那么什么时候CountDownLatch 维护的变量变为0 呢我们只需要调用一次countDown 内部变量就减少1我们让分线程和变量绑定 执行完一个分线程就减少一个变量当分线程全部走完CountDownLatch 维护的变量就是0此时await就不再阻塞统计出来的时间也就是所有分线程执行完后的时间。
Test
void testIdWorker() throws InterruptedException {CountDownLatch latch new CountDownLatch(300);Runnable task () - {for (int i 0; i 100; i) {long id redisIdWorker.nextId(order);System.out.println(id id);}latch.countDown();};long begin System.currentTimeMillis();for (int i 0; i 300; i) {es.submit(task);}latch.await();long end System.currentTimeMillis();System.out.println(time (end - begin));
}3 添加优惠卷
每个店铺都可以发布优惠券分为平价券和特价券。平价券可以任意购买而特价券需要秒杀抢购 tb_voucher优惠券的基本信息优惠金额、使用规则等 tb_seckill_voucher优惠券的库存、开始抢购时间结束抢购时间。特价优惠券才需要填写这些信息
平价卷由于优惠力度并不是很大所以是可以任意领取
而代金券由于优惠力度大所以像第二种券就得限制数量从表结构上也能看出特价券除了具有优惠卷的基本信息以外还具有库存抢购时间结束时间等等字段
新增普通卷代码: VoucherController
PostMapping
public Result addVoucher(RequestBody Voucher voucher) {voucherService.save(voucher);return Result.ok(voucher.getId());
}新增秒杀卷代码 VoucherController
PostMapping(seckill)
public Result addSeckillVoucher(RequestBody Voucher voucher) {voucherService.addSeckillVoucher(voucher);return Result.ok(voucher.getId());
}VoucherServiceImpl
Override
Transactional
public void addSeckillVoucher(Voucher voucher) {// 保存优惠券save(voucher);// 保存秒杀信息SeckillVoucher seckillVoucher new SeckillVoucher();seckillVoucher.setVoucherId(voucher.getId());seckillVoucher.setStock(voucher.getStock());seckillVoucher.setBeginTime(voucher.getBeginTime());seckillVoucher.setEndTime(voucher.getEndTime());seckillVoucherService.save(seckillVoucher);// 保存秒杀库存到Redis中stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY voucher.getId(), voucher.getStock().toString());
}4 实现秒杀下单
下单核心思路当我们点击抢购时会触发右侧的请求我们只需要编写对应的controller即可 秒杀下单应该思考的内容
下单时需要判断两点
秒杀是否开始或结束如果尚未开始或已经结束则无法下单库存是否充足不足则无法下单
下单核心逻辑分析
当用户开始进行下单我们应当去查询优惠卷信息查询到优惠卷信息判断是否满足秒杀条件
比如时间是否充足如果时间充足则进一步判断库存是否足够如果两者都满足则扣减库存创建订单然后返回订单id如果有一个条件不满足则直接结束。 VoucherOrderServiceImpl
Override
public Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 尚未开始return Result.fail(秒杀尚未开始);}// 3.判断秒杀是否已经结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {// 尚未开始return Result.fail(秒杀已经结束);}// 4.判断库存是否充足if (voucher.getStock() 1) {// 库存不足return Result.fail(库存不足);}//5扣减库存boolean success seckillVoucherService.update().setSql(stock stock -1).eq(voucher_id, voucherId).update();if (!success) {//扣减库存return Result.fail(库存不足);}//6.创建订单VoucherOrder voucherOrder new VoucherOrder();// 6.1.订单idlong orderId redisIdWorker.nextId(order);voucherOrder.setId(orderId);// 6.2.用户idLong userId UserHolder.getUser().getId();voucherOrder.setUserId(userId);// 6.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);return Result.ok(orderId);}5 库存超卖问题分析
有关超卖问题分析在我们原有代码中是这么写的 if (voucher.getStock() 1) {// 库存不足return Result.fail(库存不足);}//5扣减库存boolean success seckillVoucherService.update().setSql(stock stock -1).eq(voucher_id, voucherId).update();if (!success) {//扣减库存return Result.fail(库存不足);}假设线程1过来查询库存判断出来库存大于1正准备去扣减库存但是还没有来得及去扣减此时线程2过来线程2也去查询库存发现这个数量一定也大于1那么这两个线程都会去扣减库存最终多个线程相当于一起去扣减库存此时就会出现库存的超卖问题。 超卖问题是典型的多线程安全问题针对这一问题的常见解决方案就是加锁而对于加锁我们通常有两种解决方案见下图 悲观锁
悲观锁可以实现对于数据的串行化执行比如syn和lock都是悲观锁的代表同时悲观锁中又可以再细分为公平锁非公平锁可重入锁等等
乐观锁
乐观锁会有一个版本号每次操作数据会对版本号1再提交回数据时会去校验是否比之前的版本大1 如果大1 则进行操作成功这套机制的核心逻辑在于如果在操作过程中版本号只比原来大1 那么就意味着操作过程中没有人对他进行过修改他的操作就是安全的如果不大1则数据被修改过当然乐观锁还有一些变种的处理方式比如CAS
乐观锁的典型代表就是cas利用cas进行无锁化机制加锁var5 是操作前读取的内存值while中的var1var2 是预估值如果预估值 内存值则代表中间没有被人修改过此时就将新值去替换 内存值
其中do while 是为了在操作失败时再次进行自旋操作即把之前的逻辑再操作一次。
int var5;
do {var5 this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 var4));return var5;课程中的使用方式
课程中的使用方式是没有像cas一样带自旋的操作也没有对version的版本号1 他的操作逻辑是在操作时对版本号进行1 操作然后要求version 如果是1 的情况下才能操作那么第一个线程在操作后数据库中的version变成了2但是他自己满足version1 所以没有问题此时线程2执行线程2 最后也需要加上条件version 1 但是现在由于线程1已经操作过了所以线程2操作时就不满足version1 的条件了所以线程2无法执行成功 6 乐观锁解决超卖问题
修改代码方案一、
VoucherOrderServiceImpl 在扣减库存时改为
boolean success seckillVoucherService.update().setSql(stock stock -1) //set stock stock -1.eq(voucher_id, voucherId).eq(stock,voucher.getStock()).update(); //where id and stock ?以上逻辑的核心含义是只要我扣减库存时的库存和之前我查询到的库存是一样的就意味着没有人在中间修改过库存那么此时就是安全的但是以上这种方式通过测试发现会有很多失败的情况失败的原因在于在使用乐观锁过程中假设100个线程同时都拿到了100的库存然后大家一起去进行扣减但是100个人中只有1个人能扣减成功其他的人在处理时他们在扣减时库存已经被修改过了所以此时其他线程都会失败
修改代码方案二、
之前的方式要修改前后都保持一致但是这样我们分析过成功的概率太低所以我们的乐观锁需要变一下改成stock大于0 即可
boolean success seckillVoucherService.update().setSql(stock stock -1).eq(voucher_id, voucherId).update().gt(stock,0); //where id ? and stock 0知识小扩展
针对CAS中的自旋压力过大我们可以使用Longaddr这个类去解决
Java8 提供的一个对AtomicLong改进后的一个类LongAdder
大量线程并发更新一个原子性的时候天然的问题就是自旋会导致并发性问题当然这也比我们直接使用syn来的好
所以利用这么一个类LongAdder来进行优化
如果获取某个值则会对cell和base的值进行递增最后返回一个完整的值 6 悲观锁实现一人一单
需求修改秒杀业务要求同一个优惠券一个用户只能下一单
现在的问题在于
优惠卷是为了引流但是目前的情况是一个人可以无限制的抢这个优惠卷所以我们应当增加一层逻辑让一个用户只能下一个单而不是让一个用户下多个单
具体操作逻辑如下比如时间是否充足如果时间充足则进一步判断库存是否足够然后再根据优惠卷id和用户id查询是否已经下过这个订单如果下过这个订单则不再下单否则进行下单 VoucherOrderServiceImpl
初步代码增加一人一单逻辑
Override
public Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 尚未开始return Result.fail(秒杀尚未开始);}// 3.判断秒杀是否已经结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {// 尚未开始return Result.fail(秒杀已经结束);}// 4.判断库存是否充足if (voucher.getStock() 1) {// 库存不足return Result.fail(库存不足);}// 5.一人一单逻辑// 5.1.用户idLong userId UserHolder.getUser().getId();int count query().eq(user_id, userId).eq(voucher_id, voucherId).count();// 5.2.判断是否存在if (count 0) {// 用户已经购买过了return Result.fail(用户已经购买过一次);}//6扣减库存boolean success seckillVoucherService.update().setSql(stock stock -1).eq(voucher_id, voucherId).update();if (!success) {//扣减库存return Result.fail(库存不足);}//7.创建订单VoucherOrder voucherOrder new VoucherOrder();// 7.1.订单idlong orderId redisIdWorker.nextId(order);voucherOrder.setId(orderId);voucherOrder.setUserId(userId);// 7.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);return Result.ok(orderId);}存在问题现在的问题还是和之前一样并发过来查询数据库都不存在订单所以我们还是需要加锁但是乐观锁比较适合更新数据而现在是插入数据所以我们需要使用悲观锁操作
**注意**在这里提到了非常多的问题我们需要慢慢的来思考首先我们的初始方案是封装了一个createVoucherOrder方法同时为了确保他线程安全在方法上添加了一把synchronized 锁
Transactional
public synchronized Result createVoucherOrder(Long voucherId) {Long userId UserHolder.getUser().getId();// 5.1.查询订单int count query().eq(user_id, userId).eq(voucher_id, voucherId).count();// 5.2.判断是否存在if (count 0) {// 用户已经购买过了return Result.fail(用户已经购买过一次);}// 6.扣减库存boolean success seckillVoucherService.update().setSql(stock stock - 1) // set stock stock - 1.eq(voucher_id, voucherId).gt(stock, 0) // where id ? and stock 0.update();if (!success) {// 扣减失败return Result.fail(库存不足);}// 7.创建订单VoucherOrder voucherOrder new VoucherOrder();// 7.1.订单idlong orderId redisIdWorker.nextId(order);voucherOrder.setId(orderId);// 7.2.用户idvoucherOrder.setUserId(userId);// 7.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 7.返回订单idreturn Result.ok(orderId);
}但是这样添加锁锁的粒度太粗了在使用锁过程中控制锁粒度 是一个非常重要的事情因为如果锁的粒度太大会导致每个线程进来都会锁住所以我们需要去控制锁的粒度以下这段代码需要修改为
intern() 这个方法是从常量池中拿到数据如果我们直接使用userId.toString() 他拿到的对象实际上是不同的对象new出来的对象我们使用锁必须保证锁必须是同一把所以我们需要使用intern()方法
Transactional
public Result createVoucherOrder(Long voucherId) {Long userId UserHolder.getUser().getId();synchronized(userId.toString().intern()){// 5.1.查询订单int count query().eq(user_id, userId).eq(voucher_id, voucherId).count();// 5.2.判断是否存在if (count 0) {// 用户已经购买过了return Result.fail(用户已经购买过一次);}// 6.扣减库存boolean success seckillVoucherService.update().setSql(stock stock - 1) // set stock stock - 1.eq(voucher_id, voucherId).gt(stock, 0) // where id ? and stock 0.update();if (!success) {// 扣减失败return Result.fail(库存不足);}// 7.创建订单VoucherOrder voucherOrder new VoucherOrder();// 7.1.订单idlong orderId redisIdWorker.nextId(order);voucherOrder.setId(orderId);// 7.2.用户idvoucherOrder.setUserId(userId);// 7.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 7.返回订单idreturn Result.ok(orderId);}
}但是以上代码还是存在问题问题的原因在于当前方法被spring的事务控制如果你在方法内部加锁可能会导致当前方法事务还没有提交但是锁已经释放也会导致问题所以我们选择将当前方法整体包裹起来确保事务不会出现问题如下
在seckillVoucher 方法中添加以下逻辑这样就能保证事务的特性同时也控制了锁的粒度 但是以上做法依然有问题因为你调用的方法其实是this.的方式调用的事务想要生效还得利用代理来生效所以这个地方我们需要获得原始的事务对象 来操作事务 7 集群环境下的并发问题
通过加锁可以解决在单机情况下的一人一单安全问题但是在集群模式下就不行了。
1、我们将服务启动两份端口分别为8081和8082 2、然后修改nginx的conf目录下的nginx.conf文件配置反向代理和负载均衡 有关锁失效原因分析
由于现在我们部署了多个tomcat每个tomcat都有一个属于自己的jvm那么假设在服务器A的tomcat内部有两个线程这两个线程由于使用的是同一份代码那么他们的锁对象是同一个是可以实现互斥的但是如果现在是服务器B的tomcat内部又有两个线程但是他们的锁对象写的虽然和服务器A一样但是锁对象却不是同一个所以线程3和线程4可以实现互斥但是却无法和线程1和线程2实现互斥这就是 集群环境下syn锁失效的原因在这种情况下我们就需要使用分布式锁来解决这个问题。
文章转载自: http://www.morning.jbtwq.cn.gov.cn.jbtwq.cn http://www.morning.mqfkd.cn.gov.cn.mqfkd.cn http://www.morning.kcbml.cn.gov.cn.kcbml.cn http://www.morning.gyylt.cn.gov.cn.gyylt.cn http://www.morning.yrjfb.cn.gov.cn.yrjfb.cn http://www.morning.gsyns.cn.gov.cn.gsyns.cn http://www.morning.gjssk.cn.gov.cn.gjssk.cn http://www.morning.wylpy.cn.gov.cn.wylpy.cn http://www.morning.ffbl.cn.gov.cn.ffbl.cn http://www.morning.qbtkg.cn.gov.cn.qbtkg.cn http://www.morning.fsfz.cn.gov.cn.fsfz.cn http://www.morning.dnmgr.cn.gov.cn.dnmgr.cn http://www.morning.fbnsx.cn.gov.cn.fbnsx.cn http://www.morning.bdsyu.cn.gov.cn.bdsyu.cn http://www.morning.27asw.cn.gov.cn.27asw.cn http://www.morning.hhpkb.cn.gov.cn.hhpkb.cn http://www.morning.ckdgj.cn.gov.cn.ckdgj.cn http://www.morning.gnjtg.cn.gov.cn.gnjtg.cn http://www.morning.wzjhl.cn.gov.cn.wzjhl.cn http://www.morning.rzmkl.cn.gov.cn.rzmkl.cn http://www.morning.pyswr.cn.gov.cn.pyswr.cn http://www.morning.dphmj.cn.gov.cn.dphmj.cn http://www.morning.jwbfj.cn.gov.cn.jwbfj.cn http://www.morning.gklxm.cn.gov.cn.gklxm.cn http://www.morning.mjyrg.cn.gov.cn.mjyrg.cn http://www.morning.hgsylxs.com.gov.cn.hgsylxs.com http://www.morning.frsbf.cn.gov.cn.frsbf.cn http://www.morning.xgcwm.cn.gov.cn.xgcwm.cn http://www.morning.thpns.cn.gov.cn.thpns.cn http://www.morning.jrqbr.cn.gov.cn.jrqbr.cn http://www.morning.mygbt.cn.gov.cn.mygbt.cn http://www.morning.shinezoneserver.com.gov.cn.shinezoneserver.com http://www.morning.nsrlb.cn.gov.cn.nsrlb.cn http://www.morning.gpsrk.cn.gov.cn.gpsrk.cn http://www.morning.kmqjx.cn.gov.cn.kmqjx.cn http://www.morning.tntqr.cn.gov.cn.tntqr.cn http://www.morning.jrtjc.cn.gov.cn.jrtjc.cn http://www.morning.ctrkh.cn.gov.cn.ctrkh.cn http://www.morning.nydtt.cn.gov.cn.nydtt.cn http://www.morning.gxwyr.cn.gov.cn.gxwyr.cn http://www.morning.cgmzt.cn.gov.cn.cgmzt.cn http://www.morning.rsfp.cn.gov.cn.rsfp.cn http://www.morning.glwyn.cn.gov.cn.glwyn.cn http://www.morning.xqjh.cn.gov.cn.xqjh.cn http://www.morning.kjxgc.cn.gov.cn.kjxgc.cn http://www.morning.xrrjb.cn.gov.cn.xrrjb.cn http://www.morning.tbstj.cn.gov.cn.tbstj.cn http://www.morning.thlzt.cn.gov.cn.thlzt.cn http://www.morning.qlckc.cn.gov.cn.qlckc.cn http://www.morning.qphcq.cn.gov.cn.qphcq.cn http://www.morning.rwyd.cn.gov.cn.rwyd.cn http://www.morning.wdhzk.cn.gov.cn.wdhzk.cn http://www.morning.jxwhr.cn.gov.cn.jxwhr.cn http://www.morning.xglgm.cn.gov.cn.xglgm.cn http://www.morning.rbmm.cn.gov.cn.rbmm.cn http://www.morning.krdb.cn.gov.cn.krdb.cn http://www.morning.ptwzy.cn.gov.cn.ptwzy.cn http://www.morning.krkwp.cn.gov.cn.krkwp.cn http://www.morning.wbysj.cn.gov.cn.wbysj.cn http://www.morning.hlzpb.cn.gov.cn.hlzpb.cn http://www.morning.mlyq.cn.gov.cn.mlyq.cn http://www.morning.fgsqz.cn.gov.cn.fgsqz.cn http://www.morning.qczjc.cn.gov.cn.qczjc.cn http://www.morning.ylyzk.cn.gov.cn.ylyzk.cn http://www.morning.fqqlq.cn.gov.cn.fqqlq.cn http://www.morning.qrzqd.cn.gov.cn.qrzqd.cn http://www.morning.khfk.cn.gov.cn.khfk.cn http://www.morning.qmxsx.cn.gov.cn.qmxsx.cn http://www.morning.phxdc.cn.gov.cn.phxdc.cn http://www.morning.rgsgk.cn.gov.cn.rgsgk.cn http://www.morning.nchsz.cn.gov.cn.nchsz.cn http://www.morning.tpyjr.cn.gov.cn.tpyjr.cn http://www.morning.nwqyq.cn.gov.cn.nwqyq.cn http://www.morning.knmp.cn.gov.cn.knmp.cn http://www.morning.thzwj.cn.gov.cn.thzwj.cn http://www.morning.rkrl.cn.gov.cn.rkrl.cn http://www.morning.tfrmx.cn.gov.cn.tfrmx.cn http://www.morning.fplwz.cn.gov.cn.fplwz.cn http://www.morning.jcypk.cn.gov.cn.jcypk.cn http://www.morning.rgzc.cn.gov.cn.rgzc.cn