1、订单系统具备的表
- 订单主表:订单表,保存订单的基本信息
- 订单商品表:保存订单的商品信息
- 订单支付表:保存订单的支付和退款信息
- 订单优惠表:保存订单使用的优惠信息
表之间的关系是:订单主表和后面几个表是一对多的关系,关联的外键就是订单主表的主键
2、如何解决重复下单
通过“订单服务”的幂等性:对于订单服务来讲,很难确定发过来的订单请求是不是重复请求。也不能通过查询数据库表“sql查询”来确定是不是有重复订单,因为不能认为商品一样、价格一样、用户一样就是一个订单(一个用户可能下单一样的商品)。
- 要解决这个问题,有一个思路:通过数据表的主键来解决。订单服务中提供一个接口,无需传参,每次都返回一个新的订单编号。用户在进入下单界面时候,前端主动去请求获取一个订单号,后续请求创建订单时候,都需要将这个订单编号带上,订单表中的唯一主键约束订单编号,这样就可以通过sql的方式避免重复下单。
3、如何解决“ABA”问题
ABA问题是:用户下单后,对订单状态做反复修改时候,由于网络或者环境问题,导致程序执行的时间延迟导致状态变更错误。举例,用户下单的订单,快递员在填写快递编号时候,先写了888,感觉不对,又改为了666,如果第一个请求由于网络问题执行了update快递编号为888,但是返回较晚或者没有返回,改为666状态执行成功了。由于前端没有拿到888状态,所以前端就重复请求了个更改快递编号的接口。最终时实现的结果是,快递编号应该是666,但是实际存储到数据库的是888.
-
要解决这个问题,需要在订单主表中增加一列,列明可以叫version(版本号)。每次查询订单时候,都需要将version的值返回给页面,页面在提交更改数据的请求时候,需要将这个version数据值带上,更新数据时候需要将带入的version和数据库的version比较,如果一致就更新,如果不一致就不更细。一致更新的时候,需要对version+1操作
update orders set tracking_num=888,version+=1 where version=1通过这种方式可以保证,我的当前操作订单的这段时间,没有其他人去修改订单,如果有其他人修改的话,version就不同了,那么我的修改是不会生效的。
-
回顾下aba问题。
- 如果更新了888成功后,更新666的时候携带如果是老版本号version,那么更新666会失败。
- 如果更新了888成功后,更新666时候携带的是新版本version,那么更新666会成功,这时候888重新请求时候,由于携带的版本号是老版本号(version在第一次更新时候已经+1),那么久第二次发送的888会变更失败。
5、流量大的详情页如何设计
-
商品系统需要保存的信息:基本信息(标题、副标题、原价、促销价)、商品参数、商品介绍、图片视频、促销信息、推荐商品、评价评论、配送信息、店铺信息。
基本信息:
- 存储时候用redis(memche)+mysql数据库,更新时候,需要在更新数据库同时更新下redis。这种更新模式被称为:Cache Aside ;
- 设计商品信息表时候,需要注意保存每个历史版本。因为商品信息是时刻变化的,但是订单中关联的商品信息,必须是下单那时候的商品数据(例如价格等)
商品参数信息:使用mongodb。主要原因是每个商品的属性不同,例如电脑内存、酒的度数、口红色号,每个商品设计一个表,少数量时候能,商品数量多的时候不行了。
商品视频图片:存储cdn(fastd)
- 为应对商品详情页的高访问量,最好做到详情页的静态化。也就是详情页生成html静态页面,因为一般一个时间段内是不会修改详情的。
6、购物车
设计表:id、userId,skuId(商品id)、count、selected(是否选中)、timestamp
7、账户余额和对账
对不上账,本质上是冗余数据的一致性问题。
对账户系统来说,流水和余额是互为冗余数据的。流水记录的是用户的每次交易数,可以通过流水记录计算用户当前的余额。但是计算余额耗费时间,不能每次查询余额都去计算,所以还需要保存余额数据。但是如果只存余额就会面临如果余额数据被篡改,就无法追查。所以虽然流水和余额互为冗余,但是还是要保留。这就是账户系统的设计。
有几个设计原则需要遵守,1)流水只能新增一旦修改新增不能修改和删除。即便是取消的交易,也只能新增流水。2)流水号必须递增。因为需要这样来确定流水的先后顺序。
账户对账时候,一般出现流水和余额不一致,就会用流水记录计算后修证余额,这样才能后续保证对账正确。
- 保证数据的一致性上,使用数据库的事务。数据库提供的事务最初就是为交易设计(英语中交易和事务是一个词语);事务需要保证的是:原子性、一致性、隔离性、持久型。
那么具体到数据库事务中,如果严格执行ACID的事务的特性,那么久只能是串行执行事务,这样就降低了性能。从实际考虑,类似账户系统等绝大多数交易系统,事务的原子性和持久型是必须要满足的,否则就是事务了事务意义。一致性和隔离性可以适当牺牲来换取性能。
- mysql提供的事务的隔离级别分为4中(从脏读、不可重复读、幻读3个角度看),RU(read uncommited)、RC(read commit)、RR(repeatable read)、serielizable; 先说基本不用的两种,RU级别就是完全不隔离,每个事务的中间状态对其他事务都是可见的。serielizable;标识序列化,具备完美的事务特性,但是会导致性能严重低下。常用的两种隔离是RC和RR,其中mysq默认是RR。两者的唯一区别是否支持可重复读。
可重复读的意义是:一个事务的内部,两次读取一个数据都是一样的,不论其他事务是否在操作这个数据。
8、分布式事务
没有一种有效的分布式事务组件能完成,需要我们根据分布式事务的理论指导来自行完成开发。理论上分布式事务也需要遵守分布式ACID,为此实现方案有:2pc 、3PC、TCC、Saga、本地消息表等。这些方案对事务的实现强弱不同,使用的场景也不同,这里面2pc和本地消息表是比较常用的
- 2pc是指两阶段提交,
分为准备阶段和提交阶段。准备阶段是,每个单独系统(例如订单系统)执行数据库事务的准备(事务的begin);这里需要注意的是,有可能一个系统的使用了不同的数据表,那么需要开启一个数据库事务,执行订单优惠券写入优惠券记录,和执行订单表执行订单插入记录。也就是一个事务,两个表操作。
如果发生网络失败,就需要多次重试,一直到成功为止。
2pc的事务很好的解决了分布式事务,并且支持强一致性。
缺点:由于这种强一致性,导致数据库事务会话有可能卡住一些对数据库表的操作,从而造成系统性能下降;
-
本地消息表
实现方式是:正常执行订单库的数据库事务,去完成增加订单表数据库,同时发一个MQ,异步请求去完成清空购物车里的东西。(消息队列RocketMq其实就是本地消息表的一个实现)
9、使用elasticSearch来实现商品搜索系统; es使用了倒排索引来实现快速搜索。倒排索引就是专门为搜索设计的索引结构。
安装elasticsearch后,需要安装中文的分词器 IK analysis;
10、mysql的极限
一台mysql的处理性能极限,1秒钟处理1万条简单sql;这里的简单sql是指带有索引去查询;
' 文档总结了部分电商系统,订单服务的部分问题。部分方案来源于极客时间的“后端存储实战” '