这类项目在网上其实很多,但是实际的工作流到底是什么呢?难不成只有简单的数据库操作和逻辑判断吗?实际的工作流都有哪些呢?站在整体上来看都需要哪些呢?下面就以一个项目来讲解下都有什么?
其实可以看到,该部分其实是DDD结构中的一个单独的领域,主要是用来走抽奖逻辑。那么实际上仅仅对于抽奖这件事来说,其实就是抽奖策略的设计。通过策略包装里面的doDraw方法选择合适的策略进行抽奖。那么核心流就是策略都有哪些?
实际上关于这方面的策略主要有总体策略和单项策略。
单项策略就是说加入某个奖品抽完了,我们不需要重新计算概率,如果刚好抽到了没有的奖品,那么就相当于没抽到
而总体概率加入说没有商品了,需要重新计算现有商品的概率。
这一步算的上是,抽出奖品后以什么样的规则进行发放了。
lottery-domain└──src└──main└──java└──cn.itedus.lottery.domain.award├──model├──repository│├──impl││└──AwardRepository│└──IAwardRepository└──service├──factory│├──DistributionGoodsFactory.java│└──GoodsConfig.java└──goods├──impl│├──CouponGoods.java│├──DescGoods.java│├──PhysicalGoods.java│└──RedeemCodeGoods.java├──DistributionBase.java└──IDistributionGoodsc.java关于award发奖领域中主要的核心实现在于service中的两块功能逻辑实现,分别是:goods商品处理、factory工厂
goods:包装适配各类奖品的发放逻辑,虽然我们目前的抽奖系统仅是给用户返回一个中奖描述,但在实际的业务场景中,是真实的调用优惠券、兑换码、物流发货等操作,而这些内容经过封装后就可以在自己的商品类下实现了。
factory:工厂模式通过调用方提供发奖类型,返回对应的发奖服务。通过这样由具体的子类决定返回结果,并做相应的业务处理。从而不至于让领域层包装太多的频繁变化的业务属性,因为如果你的核心功能域是在做业务逻辑封装,就会就会变得非常庞大且混乱。
实际上活动的创建在实际的工作流中必须涉及到这些步骤,那么基于这些步骤就可以设计具体的代码结构
lottery-domain└──src└──main└──java└──cn.itedus.lottery.domain.activity├──model├──repository│└──IActivityRepository└──service├──deploy├──partake[待开发]└──stateflow├──event│├──ArraignmentState.java│├──CloseState.java│├──DoingState.java│├──EditingState.java│├──OpenState.java│├──PassState.java│└──RefuseState.java├──impl│└──StateHandlerImpl.java├──AbstractState.java├──IStateHandler.java└──StateConfig.javaID生成与分库分表关于ID的生成因为有三种不同ID用于在不同的场景下;
所以针对订单号的这种情况,需要考虑分库分表的实现思路
这个图其实就简单的将主题的思路都囊括出来了,当有了各种活动以后,需要对当前用户能否参与活动进行检验,如状态、日期、库存、参与次数等。然后进入抽奖部分,按照不同的策略进行抽奖,最终做到落库,在这里后续的设计打算使用MQ进行解耦分离。
应用层编排的设计的具体思路其实还是需要知道一开始的活动才行的,但是假如说活动有很多,怎么知道要参加什么活动呢?
所以需要一种规则引擎
通过这种规则引擎的方式来选取不同的活动,然后在执行后续的逻辑。
使用消息队列必须要考虑的是发送成功或者失败,以后重复消费的问题。
首先需要开启幂等。然后发送端如果发送失败的话,更新表,这个表中存储的是发送成功或者失败的状态。如果发送失败的话,将来需要使用定时任务进行回调,而图中下半部分的MQ是否消费成功,则是手动开启了消费确认。
按照定时任务扫描,如果成功了就发送MQ,没有成功的话,就继续等待下一次定时任务扫描即可。
即使是使用Redis分布式锁,我们也不希望把锁的颗粒度放的太粗,否则还是会出现活动有库存但不能秒杀,提示“活动过于火爆”。那么我们就需要按照活动编号把库存锁的颗粒度缩小,实际操作也并不复杂,只是把活动ID+库存扣减后的值一起作为分布式锁的Key,这样就缩小了锁的颗粒度。
其中有一个比较关键的就是扣减库存后,在各个以下的流程节点中,如果有流程失败则进行缓存库存的恢复操作。
1、阿里云服务器三台4c8G100mbps带宽
2、一台中间件机器
3、一台监控机器prometheus、influxdb、grafana
4、一台应用机器jdk、lottery、arthas、nedo_export
梯度压测(逐渐增加并发,观察系统的负载,找到系统的临界点)
总线程总数:275
总循环次数:40000次
总样本数:1,040,000
开始压测:MySQL直接报错
问题1:数据库连接异常
引入DBRouter待数据源配置的用master分支的代码(需要在dbrouter的master分支加一个druid依赖否则会报如下错误)
Causedby:java.lang.ClassNotFoundException:com.alibaba.druid.pool.DruidDataSourceatjava.net.URLClassLoader.findClass(URLClassLoader.java:387)~[na:1.8.0_371]atjava.lang.ClassLoader.loadClass(ClassLoader.java:418)~[na:1.8.0_371]atsun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)~[na:1.8.0_371]atjava.lang.ClassLoader.loadClass(ClassLoader.java:351)~[na:1.8.0_371]atjava.lang.Class.forName0(NativeMethod)~[na:1.8.0_371]atjava.lang.Class.forName(Class.java:264)~[na:1.8.0_371]atcn.bugstack.middleware.db.router.config.DataSourceAutoConfig.createDataSource(DataSourceAutoConfig.java:103)~[db-router-spring-boot-starter-1.0.2-SNAPSHOT.jar:1.0.2-SNAPSHOT]加入Druid依赖,用的是Master分支代码需要加一个Druid数据源否则启动
在Lottery的父依赖中排除指定依赖
压测2:修改Druid数据源后直接压测
系统负载:1分钟负载2.15分钟负载1.315分钟负载0.7;说明系统可以处理过来系统的负载和CPU的核数有关,对于4核CPU来说当负载大于4就需要介入查看。CPU使用和内存占用不高
每个样本的汇总报告:系统的TPS持续增加
TPS
活动线程数
(1)Jmeter测试计划
(2)样本和线程数
(3)聚合报告
prometheus监控,整个系统资源使用率不高,负载不高IO也不高
TPS:
CPU:利用率很低
压测三改进:
(2)由于操作数据库比较多查询较多,建立字段索引列,加快查询效率
创建联合索引,user_take_activity和user_take_activity_count添加联合索引,activity表可以给activity_id加一个单列索引(数据量较少,暂时看不出效果)
索引执行效果
样本:
RT:
总结:
监控doDrawProcess找到耗时的doPartake方法
监控doPartake方法
优化查询账单接口:将查库操作改为查Redis
修改后的代码:
压测后的结果:RT有所减少TPS有所增加
Arthas返回结果:查询账单接口减少50ms
增加线程数压测
样本数:
聚合报告:TPS平均在1500左右RT平均在472
prometheus监控显示系统1分钟负载已达到5.4说明系统中有大量的排队请求,系统已经请求不过来了,这个时候就需要优化了,因为已经达到系统的瓶颈了,带宽也达到最大值,说明带宽也影响了系统的处理能力,CPU和内存正常
磁盘IO使用率很高、网络带宽不足或延迟造成系统的负载很高但是CPU和内存正常