创建和更新订单
表设计
最少应该有以下几张表:
- 订单主表:保存订单基本信息
- 订单商品表:保存订单中的商品信息
- 订单支付表:保存订单支付和退款信息
- 订单优惠表:保存订单的优惠信息
订单主表和字表是一对多关系,关联的外键是订单主表的主键(订单号)。
幂等性
可能出现重复下单的场景:一种是就是用户多点了,另外就是网络错误导致(RPC、网关的重试机制)
解决方案:通过一个专门生成全局唯一订单号的服务,插入数据的时候将其保存
ABA 问题
更新订单相关信息的时候可能出现。
解决方案:在表中加个时间戳或者版本的字段,每次查询的时候将其返回,更新的时候要比较下当前订单数据的版本号,是否和预期的一致。不一致直接拒绝更新,一致的话在一个事务中更新数据的同时将版本号+1。
商品详情页
问题
一是高并发,商品的详情页每天都会有很高的 DAU(日均访问次数)。
二是商品数据规模的问题,比如下图中要存储的:
方案
基本信息相对来说比较固定,可以在数据库中建表存储,也要提供缓存(比如 Redis、Memcached)帮助系统抗一些读请求。另外要注意的是,要保留商品的历史版本,因为订单中关联的商品数据必须是下单哪个时刻的商品数据,可以用一张历史表来保存。
商品参数指商品特征,比如内存大小、屏幕尺寸、口红色号等,和基本属性一样,是结构化的数据,但是不同类型的商品参数也是不一样的。对于属性不固定的数据来说,可以使用 MongoDB 来存储,因为 MongoDB 没有数据表要有固定结构的要求。
MongoDB 中的每一行数据,在存储层就是简单地被转化成 BSON 格式后存起来,这个 BSON 就是一种更紧凑的 JSON。所以,即使在同一张表里面,它每一行数据的结构都可以是不一样的。当然,这样的灵活性也是有代价的,MongoDB 不支持 SQL,多表联查和复杂事务比较孱弱,不太适合存储一般的数据。
图片和视频由于占用空间比较大,一般的存储方式是在数据库中只保留图片视频的 id 或者 url,实际的图片视频以文件方式存储,可以保存在对象存储(Object Storage)中。
对象存储可以简单理解为无限容量的大文件 KV 存储,key 是唯一的,对象是 value,可以通过 key 操作 value(写入、访问、删除)。对象存储提供了客户端 API,可以在 Web 页面或者 app 中直接访问,而不用通过后端服务。页面通过提供的 URL 直接访问,省事不占带宽,同时,对象存储云服务也提供了自带 CDN(Content Delivery Network)服务,响应时间比直接请求业务服务器更短。云服务厂商还会提供针对性优化,比如说缩放和转码服务。
商品介绍在商详页中占得比重是最大的,包含了大量的带格式文字、图片和视频。其中图片和视频自然要存放在对象存储里面,商品介绍的文本,一般都是随着商详页一起静态化,保存在 HTML 文件中。
静态化是相对于动态页面来说的。一般我们部署到 Tomcat 中的 Web 系统,返回的都是动态页面,也就是在 Web 请求时,动态生成的。比如说商详页,一个 Web 请求过来,带着 SKUID,Tomcat 中的商详页模块,再去访问各种数据库、调用后端服务,动态把这个商详页拼出来,返回给浏览器。
商详页的绝大部分内容都是商品介绍,它是不怎么变的。那不如就把这个页面事先生成好,保存成一个静态的 HTML,访问商详页的时候,直接返回这个 HTML。这就是静态化。
商详页静态化之后,不仅仅是可以节省服务器资源,还可以利用 CDN 加速,把商详页放到离用户最近的 CDN 服务器上,让商详页访问更快。
至于商品价格、促销信息等这些需要频繁变动的信息,不能静态化到页面中,可以在前端页面使用 AJAX 请求商品系统动态获取。这样就兼顾了静态化带来的优势,也能解决商品价格等信息需要实时更新的问题。
购物车
场景
- 未登录,浏览器加购,关闭浏览器再打开,加购商品应该存在
- 未登录,浏览器加购,然后登录,加购的物品存在
- 登录后再注销,关闭浏览器,步骤 2 中加购的商品不登录看不到(未登录特定账号看到的购物车是空的)
- 手机登录,步骤 2 中的加购的存在
原则
用户未登录,需要临时暂存购物车中的商品;
用户登录,将暂存购物车的商品加入到用户购物车,清空暂存购物车;
用户登录后,各端同步用户购物车。
设计
暂存购物车
如果存在服务端,需要唯一标识存储,浪费服务端资源,故需要存储在客户端。
存在客户端的途径:SESSION 不合适(时间短,SESSION 数据其实还是在服务端);Cookie 存储的话,实现简单,通过服务端读写 Cookie 实现具体的加减购物车合并购物车逻辑;LocalStorage 存储的话,实现复杂(客户端和服务端都需要实现逻辑),但是容量大,不用每次请求都携带,节省带宽。
用户购物车
还是推荐 MySQL,如果用 Redis 存在丢数据的情况,查询方式和事务都不如 MySQL。
引入 Redis 需考虑问题:
- 不同用户不同购物车,缓存命中率不高,为了维护缓存,还提高了系统复杂度,有没有必要
- Redis 的缓存更新策略
账户系统
目的
对账系统存在的目的:来核对、矫正账户系统和其他系统之间的数据差异。
每个账户系统都不是孤立存在的,至少要和财务、订单、交易这些系统有着密切的关联。理想情况下,账户系统内的数据应该是自洽的。所有用户的账户余额加起来,应该等于这个电商公司在银行专用账户的总余额。账户系统的数据也应该和其他系统的数据能对的上。比如说,每个用户的余额应该能和交易系统中充值记录,以及订单系统中的订单对的上。
记录流水,可以修改由于系统 bug 或者认为篡改导致的账户余额的问题。流水的数据模型至少要包含:流水 ID、交易金额、交易时间戳、交易双方的系统、账户、交易单号等信息。
原则
- 流水记录只能新增,一旦记录成功不允许修改和删除。即使是由于正当原因需要取消一笔已经完成的交易,也不应该去删除交易流水。正确的做法是再记录一笔“取消交易”的流水。
- 流水号必须是递增的,我们需要用流水号来确定交易的先后顺序。
在对账的时候,一旦出现了流水和余额不一致,并且无法通过业务手段来确定到底是哪儿记错了的情况,一般的处理原则是以交易流水为准来修正余额数据,这样才能保证后续的交易能“对上账”。
方案
首先要保证只有记录流水的时候更新余额,不可以将更新余额单独提供;其次使用数据库事务,记录流水和修改余额两个操作要么都成功,要么都失败。
MySQL 事务和 ACID 相关内容不在此赘述
分布式事务
背景
分布式环境下跨系统数据一致性问题。
方案
2PC、3PC、TCC、Saga、本地消息表都是分布式事务的解决方案