当前位置: 首页 > news >正文

网站建设的电销西安网站seo优化公司

网站建设的电销,西安网站seo优化公司,电子商务网站规书,网站开发评分标准#x1f468;‍#x1f393;作者简介#xff1a;一位大四、研0学生#xff0c;正在努力准备大四暑假的实习 #x1f30c;上期文章#xff1a;Redis#xff1a;原理速成项目实战——Redis实战6#xff08;封装缓存工具#xff08;高级写法#xff09;缓存总… ‍作者简介一位大四、研0学生正在努力准备大四暑假的实习 上期文章Redis原理速成项目实战——Redis实战6封装缓存工具高级写法缓存总结 订阅专栏Redis原理速成项目实战 希望文章对你们有所帮助 这篇文章写了很久。我自己在边实现、边用jmeter来测试、边根据结果来优化我的代码对于那些线程并发的问题我大致是可以靠自己来解决但是为了写好这篇文章为了做好线程并发问题的分析我在独立实现完之后还是按黑马程序员的进度走了一下他埋坑的地方其实都是线程并发问题的坑我也自己掉一掉并在这篇文章中进行总结。 文章中会涉及一些java面试常见的问题常量池、Spring代理失效。如果没有印象大家可以专门找一下这些面经去了解一下。 聊到电商一定离不开秒杀而Redis在整个秒杀的业务中的作用是非常巨大的接下来将会利用Redis实现全局ID并实现秒杀并且解决超卖问题、实现一人一单逐渐优化业务。 优惠券秒杀 全局唯一IDRedis实现全局唯一ID优惠券秒杀下单添加优惠券实现秒杀下单 库存超卖问题库存超卖问题分析乐观锁解决超卖 实现一人一单功能集群下的线程并发安全问题 全局唯一ID 每个店铺都可以发布优惠券代金券当用户抢购的时候就会生成订单并且保存到tb_voucher_order这张表中 可以发现我们的主键ID没有使用自增长这是因为如果使用数据库自增ID就会存在一些问题 1、ID的规律性太明显容易让别人猜测到信息 2、受单表数据量的限制订单可能数据非常大可能会分多表进行存储但表的自增长相互之间不受影响所以不同表之间可能会出现ID相同的情况也就是说这种时候会违背ID的唯一性这显然是不可以的 而全局ID生成器是一种分布式系统下用来生成全部唯一ID的工具一般满足以下特性 1、唯一性 2、高可用 3、高性能 4、递增性 5、安全性 除了第5点Redis及其数据结构已经可以直接满足前4点的要求了为了增加ID的安全性不要直接使用Redis自增的数值而是拼接一些其他信息最终我们将ID组成定义为64位的二进制数分别是1位符号位31位时间戳32位序列号 1、符号位1bit永远为0 2、时间戳31bit以秒为单位可以用69年 3、序列号32bit秒内的计数器支持每秒产生2^32个不同的ID这是用来处理相同秒内时间戳相同的多个业务 这样的结构是可以大幅度提高安全性的不同时间下的ID一定不同相同时间的情况下也会因为32位的序列号而导致ID不同。 Redis实现全局唯一ID 我们在utils包下创建RedisIdWorker类 Component public class RedisIdWorker {Resourceprivate StringRedisTemplate stringRedisTemplate;/*** 开始时间戳由main函数运行得到*/public static final long BEGIN_TIMESTAMP 1704499200L;/*** 序列号的位数*/public static final int COUNT_BITS 32;public long nextId(String keyPrefix){//获得当前时间LocalDateTime now LocalDateTime.now();long nowSecond now.toEpochSecond(ZoneOffset.UTC);//生成时间戳long timestamp nowSecond - BEGIN_TIMESTAMP;/*** 接下来生成序列号* 我们的key的设置除了加上icr表示是自增长的还需要在最后拼接一个日期字符串* 这是因为我们的序列号上限是2^32并不大如果每天的key都是一样的这是很有可能超过上限的* 在后面拼接一个日期字符串可以保证每一天的key都是不一样的而且一天内也基本不可能到达2^32的上限* 这样做还有一个好处我们以后可以根据每天或者每月来查看value值起到统计效果*///获取当前日期精确到天String date now.format(DateTimeFormatter.ofPattern(yyyy:MM:dd));//ID自增长这里最好用基本类型而不是包装类因为后面还会做运算long count stringRedisTemplate.opsForValue().increment(icr: keyPrefix : date);//拼接并返回这里灵活用位运算return timestamp COUNT_BITS | count;}public static void main(String[] args) {//定义时间为2024年1月1日00:00:00LocalDateTime time LocalDateTime.of(2024, 1, 6, 0, 0, 0);//将时间变成变成秒数的形式long second time.toEpochSecond(ZoneOffset.UTC);//在这里运行出来的时间作为BEGIN_TIMESETAMPSystem.out.println(second);} }编写测试代码 Resourceprivate RedisIdWorker redisIdWorker;//性能池private ExecutorService es Executors.newFixedThreadPool(500);Testvoid testIdWorker() throws InterruptedException {//因为线程池是异步的因此我们要用CountDownLatch去截断这样才能正常计时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();};//将任务提交300次并进行计时long begin System.currentTimeMillis();for (int i 0; i 300; i) {es.submit(task);}latch.await();//等待所有的countDown结束long end System.currentTimeMillis();System.out.println(time (end - begin));} 运行后可以发现id各不重复估计id生成的花费时间差不多只有2秒id的打印也是会花时间的 打开Redis客户端可以发现我成功的生成了3万条的id 优惠券秒杀下单 每个店铺都可以发布优惠券分为平价券和特价券平价券可以任意购买而特价券需要秒杀抢购表关系如下 1、tb_voucher优惠券基本信息金额规则等 上面的type可以表示标识出是平价券还是特价券如果是特价券我们也需要一些特定的信息因此我们会专门拓展出一张表。 2、tb_seckill_voucher优惠券库存、开始抢购时间、结束抢购时间特价券需要此表 添加优惠券 在VoucherController中提供一个接口调用就可以实现添加秒杀优惠券 虽然我们传入的参数只有Voucher但是它也同样可以用来保存需要秒杀的券 真正的添加不是客户来做的要给后台来做我们可以使用postman 可以发现我们的数据库中已经存储了这个秒杀券 实现秒杀下单 点击限时抢购查看请求URL 说明请求方式POST请求路径voucher-order/seckill/{id}请求参数id,优惠券id返回值订单id 下单的时候我们需要判断2点 1、秒杀是否开始或结束 2、库存是否充足 业务流程 controller serviceimpl /*** p* 服务实现类* /p** author 王雄俊* since 2024-01-06*/ Service public class VoucherOrderServiceImpl extends ServiceImplVoucherOrderMapper, VoucherOrder implements IVoucherOrderService {//注入秒杀优惠券的serviceResourceprivate ISeckillVoucherService seckillVoucherService;Resourceprivate RedisIdWorker redisIdWorker;OverrideTransactionalpublic Result seckillVoucher(Long voucherId) {//查询优惠券SeckillVoucher voucher seckillVoucherService.getById(voucherId);//判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {return Result.fail(秒杀尚未开始);}//判断秒杀是否结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail(秒杀已经结束);}//判断库存是否充足if (voucher.getStock() 1) {return Result.fail(库存不足);}//扣减库存用mybatis-plus来写boolean success seckillVoucherService.update().setSql(stock stock - 1).eq(voucher_id, voucherId).update();//where条件if (!success){return Result.fail(库存不足);}System.out.println(啊啊啊啊啊);//创建订单需要订单id、用户id、代金券idVoucherOrder voucherOrder new VoucherOrder();long orderId redisIdWorker.nextId(order);voucherOrder.setId(orderId);Long userId UserHolder.getUser().getId();//用户Id去ThreadLocal中取voucherOrder.setUserId(userId);voucherOrder.setVoucherId(voucherId);save(voucherOrder);//返回订单IDreturn Result.ok(orderId);} }这边实现了最基础的订单秒杀但是它存在很多问题 库存超卖问题 既然是秒杀那每秒钟很可能会有成千上万的用户进行访问那么这就对我们的并行化要求非常高线程安全问题肯定是很重要的上面的代码肯定是会存在线程安全问题的我们可以用jmeter来做测试为了方便我们到时候观察测试结果我们去数据库手动把优惠券数量调回100接着在jmeter中用200个线程来进行抢购 这里设置的请求头则表示200个线程全部都由这一个用户来执行 运行后可以看到有些请求成功有些请求失败 预期是有100个线程失败的但是打开聚合报告可以发现失败的线程数量不到一半 说明有些线程意外成功了打开数据库发现票数为-9说明发生了超卖 这会给商家带来损失。 库存超卖问题分析 假设库存容量为1相当于一种临界资源高并发的时候可能出现的异常情况 也就是说我们在某一时段会同时有多个线程查询库存的时候得到的库存量为1这时候都会进行扣减操作造成超卖。 针对这种线程安全问题常见解决方法就是直接加锁可以分为悲观锁和乐观锁 悲观锁认为线程安全问题一定会发生因此在操作数据之前先获取锁确保线程串行执行。Synchronized、Lock等 乐观锁认为线程安全问题不一定会发生因此不加锁只是在更新数据时去判断有没有其它线程对数据做了修改。如果没有修改那就是安全的如果已经被其他线程修改说明发生了安全问题此时可以重试或异常 显然乐观锁的性能会好很多但是实现起来会更复杂我们要处理好关键的一点那就是更新数据的时候该如何去判断有没有其它线程对数据做了修改。 乐观锁的实现方式有2种方法其实思想相同 1、版本号法 给数据增加一个字段version初始值为1每次我们要修改库存量之前都需要先查询库存量与版本号然后线程执行SQL语句执行SQL语句必须要确定数据库中的这条数据的版本号就是查询出来的版本号如果不相同说明有其他线程修改了数据导致当前数据的版本号与之前查询的不一样 2、CAS法 上面的方法加一个版本号其实是一种标识但是我们不一定要借助version实际上我们可以直接依靠库存量来做标识在对数据库进行修改的时候我们要首先判断当前数据的库存量与之前线程查询出来的库存量是否相同不相同则说明发生线程安全问题不能修改 乐观锁解决超卖 我们选用CAS法来解决超卖根据上述思想我们只需要在SQL语句那增加一个判断库存量的条件 测试一下上面的代码先把数据库做还原把订单数据删光并还原stock为100然后测试jmeter可以发现jmeter中显示大量的失败数据库中也显示没有超卖 超卖问题确实没有出现了但是这显然是不合常理的200个线程抢100张票票居然只能卖出20张。这说明乐观锁有弊端。 我们对于乐观锁的分析是拿stock1的情况来说的所以当线程查询出来的stock与数据库的stock不一致的时候足以说明票已经卖完了。 假设stock100当线程查询出来的stock与数据库的stock不一致的时候并不能说明票卖完了理论上库存量大概率不为0该线程还是应该要能够实现买票操作但全都因为查询的stock与数据库不一致导致有大量线程买票失败。 传统乐观锁太谨慎了我们应该要对其进行改进 我们不再判断查询条件而只需要查询数据库中的stock是否大于0 再次打开jmeter进行测试异常率50%解决了上述问题 但是这不代表乐观锁就是完美的很显然代码逻辑中要操作数据库大量的线程就会给数据库带来压力仅仅使用乐观锁在更高并发的场景下还是不太够的。 实现一人一单功能 我们在jmeter中的测试200个线程全部都由一个用户来执行因此打开订单表我们可以发现订单全部被同一个用户买了 商家做优惠券就是为了吸引更多的用户一人多单可能会导致商家变相亏本。 其实思路是很简单的我们只需要判断当前尝试抢优惠券的线程其用户id在订单表中是否已经存在了如果存在则不允许下单 我们在库存修改的代码之前加上这一部分逻辑 //一人一单Long userId UserHolder.getUser().getId();//查询订单int count query().eq(user_id, userId).eq(voucher_id, voucherId).count();//判断是否存在if (count 0){return Result.fail(您已购买过一次);}再次测试jmeter 数据库显示这个用户买了10张优惠券一人多单的问题有所缓解但依旧存在 这是因为上面的那一串逻辑还是存在了并发安全问题在某一时刻还是会有很多的线程同一个用户进入了这部分逻辑判断了count为0因此进行了删减库存的操作。 这里我们肯定也要加锁由于这一串逻辑并没有涉及到修改数据库的操作所以我们只能加悲观锁。 Overridepublic Result seckillVoucher(Long voucherId) {//查询优惠券SeckillVoucher voucher seckillVoucherService.getById(voucherId);//判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {return Result.fail(秒杀尚未开始);}//判断秒杀是否结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail(秒杀已经结束);}//判断库存是否充足if (voucher.getStock() 1) {return Result.fail(库存不足);}//返回订单IDreturn createVoucherOrder(voucherId);}Transactional //事务回滚放到这个函数public Result createVoucherOrder(Long voucherId) {//一人一单Long userId UserHolder.getUser().getId();/*** userId值一样的我们用同一把锁但是每个请求一来我们的id对象都是全新的* Long类型会存在这个问题所以我们要用toString方法* 但是toString方法其实是对long类型new了一个字符串所以每调用一个toString都是一个全新对象* 所以要加上intern()方法从常量池中返回字符串的规范表示*/synchronized (userId.toString().intern()) {//查询订单int count query().eq(user_id, userId).eq(voucher_id, voucherId).count();//判断是否存在if (count 0) {return Result.fail(您已购买过一次);}//扣减库存boolean success seckillVoucherService.update().setSql(stock stock - 1).eq(voucher_id, voucherId).gt(stock, 0).update();if (!success) {return Result.fail(库存不足);}//创建订单需要订单id、用户id、代金券idVoucherOrder voucherOrder new VoucherOrder();long orderId redisIdWorker.nextId(order);voucherOrder.setId(orderId);voucherOrder.setUserId(userId);voucherOrder.setVoucherId(voucherId);save(voucherOrder);//返回订单IDreturn Result.ok(orderId);}}需要注意一个细节上面代码还是会发生并发安全问题 我们这边的整个函数已经是被Spring托管了所以事务的提交会在函数执行完毕之后也就是说我们会先释放锁再提交事务当我们事务还没有提交完成修改数据还没写入数据库却又有其他线程进来了再次发生线程并发问题。 所以锁的范围太小了我们应该要把整个函数都锁起来 但依旧有问题直接调用createVoucherOrder方法是不行的因为它相当于调用了this.createVoucherOrder然而当前类并不是代理对象这会导致Sping代理失效 所以我们要先获得当前对象的代理对象然后再去调用这个函数 IVoucherOrderService proxy (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId);需要引入依赖 dependencygroupIdorg.aspectj/groupIdartifactIdaspectjweaver/artifactId /dependency并且在启动类中需要暴露代理对象 运行项目打开jmeter进行测试 完美解决 注意我没有在createVoucherOrder这个函数上面直接加锁不然所有进行操作的线程都串行执行实在太影响效率了 集群下的线程并发安全问题 现在已经通过加锁解决一人一单问题安全但是这只能解决单机情况的集群模式依旧不行在这里试着模拟一下集群的方式来进行测试。 1、将服务启动2份端口分别为8081与8082 重启形成2个机子的集群 2、修改nginx的conf目录下的nginx.conf文件配置反向代理、负载均衡 最后重新加载一下Nginx并重启 最后访问网址并连续刷新2次 http://localhost:8080/api/voucher/list/1 查看后台可以发现两个启动服务都可以接受到信息因为api8080包括了8081与8082访问是以轮转的方式进行的 这样就实现了负载均衡。 测试大家只需要在锁那里打个断点并且在postman里面分别抢券都用同一个用户来进行优惠券抢购可以发现只用1个用户信息数据库中却少了2张券说明又一次发生了并发问题。 从头分析一下 1、对于一个服务中的2个线程可能发生下面的并发问题 2、我们解决方法是加锁 之所以这样能实现是因为我们锁住的对象是userId.toString().intern()也就是从这台Tomcat常量池中取出userId.toString()同一个userId之间肯定是相同的因此可以锁住防止并发。 3、但如果我们部署另外一台Tomcat这是锁的锁监视器其监视的内容和之前锁中的监视器内容是不一样的那么新Tomcat的线程获取锁就会成功获取的userId.toString()是不一样的不理解的可以去看toString方法的源码并成功的操作数据库因此才会造成线程并行问题。 如下图线程1、3发生了线程安全问题 因此我们只能保证单个JVM下的线程安全却无法保证集群中多个JVM的线程安全我们需要在集群中加锁也就是分布式锁将在后续讲解。
http://www.hkea.cn/news/14312540/

相关文章:

  • 外贸网站收录工具公司做年审在哪个网站
  • 制作自己的网站教程长清做网站公司
  • 大型建站公司是干嘛的深圳seo公司
  • 网站建设业务的延伸性如何搭建论坛网站
  • 给网站做seo伪原创工具
  • 网站做的漂亮的企业怎么介绍自己的网站建设
  • 无极网站设计合肥百度快速排名提升
  • 张家界商城网站建设如何做垂直网站
  • 太原北京网站建设公司哪家好页面素材图片
  • 长沙营销型网站设计wordpress wp-polls
  • 注册网站域名的入口是网站空间登录
  • 建站模板行情专科网站开发简历
  • 四川省住房和城镇建设官方网站网站301定向
  • wordpress 子目录建站做韦恩图网站
  • 南宁网站建设网站推广惠州住房和建设局网站
  • 惠州公司做网站重庆出名的网站建设公司
  • 常州模板建站平台调用wordpress评论框
  • 东莞网站外包翻墙在线代理
  • 苏州自学网站建设平台手机软件设计用什么软件
  • 如何使用二级域名做网站wordpress修改成中文
  • 台州做微网站我想做网站怎么做
  • 樟木头做网站php网站后台地址
  • 做外语网站的公司厦门百度竞价推广
  • 如何做网站图标精密导航
  • 医疗器械网站素材线上引流线下推广方案
  • 东营网站建设制作网站定制设计价目表
  • 手机端网站开发素材中信建设有限责任公司龙芳
  • 网站没做好能不能备案网页视频怎么下载到电脑本地
  • 做网站站长先把作息和身体搞好河北省城乡和建设厅网站首页
  • 哪个网站做清洁的活多东莞网站建设公司直播