状态机
什么是状态机需求说明在订单模块中订单共设计了7种状态,如下图
订单会在多个状态间进行转换,这就需要开发人员都去记忆这些转换关系,这是非常麻烦的
12345678910if(订单状态 == 待支付){ //如果用户支付成功,执行此场景下的业务逻辑,更新订单状态为派单中 update(id,派单中))if(订单状态 == 待支付){ //如果用户取消支付,执行此场景下的业务逻辑。更新订单状态为已取消 update(id,已取消))...
针对这种情况,可以引入状态机对订单状态进行统一管理,可以认为状态机就是一个封装好的组件
调用它的时候,只需要告诉它订单id和要执行的事件,它内部就可以完成对应的所有操作,类似于下面代码
12345//调用状态机执行支付成功事件orderStateMachine.changeStatus(订单id,支付成功事件);//调用状态机执行支付取消事件orderStateMachine.changeStatus(订单id,支付取消事件);
因此使用状态机的好处就是:
易于理解:可以使业务模型清晰,开发人员可以更好地理解状态转 ...
事务失效
事务失效问题如果只对其中的一段代码添加事务,比方只对其中的**this.save(orders);**一行添加事务
想实现的话有两种方案:
使用Spring的编程式事务,这种方式控制粒度更精准,但是缺点是代码耦合度高,需要自己写代码处理事务
将需要控制事务的那段代码单独提到一个方法中去,然后仅仅对新抽取的方法控制事务
123456789101112@Overridepublic PlaceOrderResDTO placeOrder(PlaceOrderReqDTO placeOrderReqDTO) { //... 省略代码 this.saveOrders(orders); //... 省略代码}//新编写一个方法,将需要事务控制事务的方法,提到这个位置来@Transactionalpublic void saveOrders(Orders orders){ this.save(orders);}
上面的方法可以很好的解决长事务的问题,但是它也引入了一个新的问题,那就是事务失效
1234567@Transactio ...
冷热分离
冷热分离需求分析随着时间的推移,订单数据会逐渐增加,而在这些数据中,超过15天的订单是无法再进行变化的
因此一笔订单在15日之后的访问频率就会大大降低,为了提高订单表的查询效率,可以它转移到单独的库中
也就是将订单数据分到两个库中进行存储
订单库:存储15日之内的订单(数据量较小,访问频率高,属于热数据)
历史订单库:存储15日之前的订单(数据量庞大,访问频率低,属于冷数据)
这就是冷热分离(根据数据的特性,将数据划分为冷数据和热数据,然后针对冷热数据进行分离存储)
存储选型订单冷热分离之后,通常采用MongoDB、HBase、TiDB等分布式数据库来存储冷数据
这么多数据库该如何选择呢?
假设电商项目有50万用户规模,请估算出订单数据量的规模
用户活跃度:假设50万用户中有20%是活跃用户,也就是有50万 * 20% = 10万活跃用户
订单频率:假设每个活跃用户每月至少下一次订单,也就是每年有10万 * 12次/年 = 120万订单
订单大小:假设每个订单大约占用3KB的空间,也就是120万 * 3KB ≈ 3.6GB
对于年百万级 ...
数据表设计规范
设计范式数据库设计三范式:
第一范式:数据表中的每一列都是原子的,不可再拆分
第二范式:每张表中的一条记录只描述一个实体
第三范式:数据表中不要存在冗余字段(可以通过其他字段推导出的字段)
1)第一范式:数据表中的每一列都是原子的,不可再拆分
有一张用户考勤表设计如下
id
用户名
考勤时间
1
张三
9:00 - 18:00
2
李四
9:30 - 18:00
表中的考勤时间字段明显不是原子的,可以拆分为:上班时间和下班时间
id
用户名
上班时间
下班时间
1
张三
9:00
18:00
2
李四
9:30
18:00
2)第二范式:每张表中的一条记录只描述一个实体
有一张会员账户表设计如下
会员编号
会员名称
手机号
账户编号
账户类型
账户余额
C00001
张三
13800000001
ACC001
红包
200
C00001
张三
13800000001
ACC002
红包
300
C00001
张三
13800000001
ACC003
金币
500
目前表中明显分为两部分数据:会员表和账户表,应该拆到两 ...
缓存优化
针对系统中访问频率很高的页面,常见的优化方案如下:
将页面做页面静态化处理
如果静态化之后依旧无法满足需求,可以将静态资源转移到CDN服务器
页面上变动的内容可以异步请求后端服务器获取数据,后端可以添加缓存来减轻数据库的压力
数据库方面可以通过合理的存储引擎选择、表字段设计、索引设计等进一步提高查询效率
页面静态化什么是页面静态化?
页面静态化就是使用模板引擎技术(freemarker、Thymeleaf)将动态数据生成html静态网页
并将其与之使用的图片、css、js等静态资源存储到类似于Nginx之类支持高并发的存储中
因为纯静态网页通过类似Nginx这种服务器去加载要比去Tomcat加载快很多
CDN内容分发网络(Content Delivery Network,CDN),是一种分布式的内容分发网,它需要在多个城市建立节点
然后将内容分发到各个城市的节点上,用户访问的时候就可以请求就近的服务器了
SpringCache介绍目前业界最主流的缓存技术就是Redis,而Java操作Redis目前有两种主流方案:
RedisTemplate:Spring提供的一个对象,内置了大 ...
订单重复提交问题
订单重复提交订单重复提交指的是同一个订单多次提交的情况,常见原因如下:
重复点击:用户可能会因为网络延迟、手抖、页面加载缓慢等原因,多次点击下单提交按钮
页面刷新:用户在订单提交后,如果刷新页面,浏览器可能会重新发送上一次的表单提交请求
防止订单重复提交的本质就是保证下单方法不要被重复调用,这首先可以从前端处理
比如在用户点击提交按钮后,立即将按钮禁用或者改为提交中状态,防止用户再次点击
但是前端只能提高用户的体验,不能保证安全,因为用户可以通过一些脚本提交请求,因此还需要后端处理
后端的处理方式有很多,经常用到的就是Token机制和分布式锁
Token机制
生成Token:用户每次进入下单页面,前端会向服务器请求一个唯一的Token
存储Token:后端将生成Token存放在Redis中,并设置一定的过期时间,然后给前端返回
验证Token:用户提交订单时需要携带Token,服务器验证Token,一旦Token被使用过就失效
分布式锁分布式锁是在分布式系统下的锁机制,旨在保证代码在加锁期间只能被同一个线程访问
实现分布式锁的方案有很多,比如下面这些:
MySQL:可以 ...
分库分表
随着用户不断的进行下单,MySQL订单表中的数据会不断的增多,其存储及查询性能也会随之下降
当数据量没有那么大的时候,可以通过添加索引和缓存来进行查询优化,但是数据量过大,就只能分库分表了
分库分表可以简单理解为原来一个表存储数据现在改为通过多个数据库及多个表去存储
一般认为,当MySQL单表行数超过500万行或者单表容量超过2GB时建议进行分库分表
在数据量及访问压力不是特别大的情况,首先考虑缓存、读写分离、索引技术等方案
只有上面方法已经无法支撑时,再考虑进行分库分表,因为分库分表成本比较高,带来的问题也比较多
方案介绍分库分表包括分库和分表两个部分,在生产中通常包括:垂直分库、水平分库、垂直分表、水平分表四种方式
在微服务环境下,一般一个微服务对应一个数据库,也就是说已经实现了分库,因此重点来看分表
垂直分表垂直分表就是将一张表中的列分到多张表中,一般用在单张数据表列比较多的情况下按冷热字段进行拆分
例如下面的案例中,用户在浏览商品列表时,只会关注商品名称、图片、价格等
只有对某商品感兴趣时才会查看该商品的详细描述
也就是说,商品描述字段访问频次较低,且该字段存储占用空间较 ...
超卖方案(线程锁)
超卖方案Java锁jvm提供了很多锁,它们都可以解决线程安全问题,大致可以分为两类:悲观锁和乐观锁
悲观锁悲观锁认为当前线程在操作共享资源期间,总会有其它线程修改数据
为了保证线程安全,就要在操作前总是先加锁,操作完成后释放锁
像Java中的synchronized、ReentrantLock都属于悲观锁
乐观锁乐观锁认为当前线程在操作共享资源期间,不会有其它线程去并发修改数据,所以谁都可以去执行代码
但是为了保证数据的安全性,它会在更新之前先进行一次新老值的比较,即CAS(Compare And Swap)
示例:库存数据对应一个版本,库存每次变化则版本号跟着变化,如下:
优惠券活动名称
库存
版本号
元旦促销活动
1
1
修改库存前拿到库存及对应的版本号:1和1,然后判断库存如果大于0,则将库存减1,然后准备更新库存,更新前先校验版本号
如果版本号依旧是1,说明自己在执行的过程没有其它线程去修改过库存,此时将库存更新为0,版本号更新为2
如果版本号不再是1,说明自己在执行的过程中,其它线程修改了库存版本,此次更新会被放弃或者重试
分布式锁上边介绍的锁只控制了单个 ...
策略模式
策略模式问题说明当前项目中,订单的状态比较多,不同状态下取消订单的逻辑是不同的;而且用户不同,可操作的内容也不同
普通用户:可取消待支付、派单中、待服务的订单
运营人员:可取消派单中、待服务、服务中、完成的订单
因此最终实现取消订单的代码逻辑如下所示:
12345678910111213141516171819202122232425262728293031public void cancel(OrderCancelDTO orderCancelDTO) { Orders orders = getById(orderCancelDTO.getId());//查询订单信息 Integer ordersStatus = orders.getOrdersStatus();//订单状态 CurrentUserInfo currentUserInfo = UserContext.currentUser();//获取当前用户 Integer userType = currentUserInfo.getUserType();//用户类型 ...
责任链模式
责任链模式基本理论责任链模式允许请求沿着处理链进行传递,链中的每个处理者都要决定是自己处理,还是交给下一环处理
在这里有一个很经典的案例就是请假审批,比如我们有这样一个需求,就是当员工请假时,审批规则如下:
请假天数少于3天,组长审批
请假天数在3-5天,主管审批
请求天数大于5天,经理审批
此时就可以使用责任链模式来编写这个代码,这种设计模式有三个核心角色:
抽象处理者:通常包含一个指向下一个处理者的引用和一个处理方法的声明
具体处理者:指定下一个处理者是谁,并且要实现处理的方法
客户端:创建处理者对象并组成责任链的结构,负责将请求发送给第一个处理者
测试代码如下:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273package com.jzo2o.orders.dispatch.test;public class ApproveTest { ...
