分布式数据一致性(库存)
库存冻结现状
目前购物车添加商品、删除商品、修改商品数量、购物车过期库存解冻、成单后清空购物车,都涉及库存变化。
以添加商品为例,目前实现逻辑为:
1、调用库存系统扣减库存
2、购物车写库
3、第2步失败时,调用库存系统回滚库存。
以上均为线上同步调用。网络抖动时,产生的数据不一致系统无法自动恢复。
购物车与库存数据一致性
因购物车和库存系统分属不同的数据库,此处涉及分布式事务问题。
互联网系统出于系统性能的考虑,一般不追求实时一致性,而目标转向最终一致性。
最终一致性,可基于消息和处理状态实现。参考文章
以添加商品为例,实现逻辑仍与现有基本一致。一个事务可分解为3个动作:冻结库存、购物车写库、库存还原
每个动作都在本地库进行操作记录,离线定时对前后两个动作的操作记录进行核对,后一动作有缺失则进行重新触发。
最终一致性实现设计
概念定义:
1、事务(Transaction):购物车每次添加商品、删除商品、修改商品数量,称之为一个事务。
2、动作(Action):具体的数据写入操作,称之为一个动作。
以数据库为单元切分,不同数据库的操作必须切分为不同的动作。同一数据库的连续操作,可合并为一个动作。
3、链接(Link):前后相邻两个动作,需离线进行核对,称之为链接。先执行的动作简称为前动作,后执行的动作简称为后动作。
每个动作只能对应一个链接输入,可以对应多个链接输出。
由此,一个事务处理逻辑,可形成一个单向无环图。图的节点表示动作,连线表示链接,箭头方向表示处理先后顺序。
具体到购物车与库存一致性处理,事务图可表示如下:
注意:图中所指结果成功/失败,一律指业务逻辑上的成败(如库存不足)。系统故障(如数据库连接中断),只能作为异常处理。
为避免动作重复执行,每个事务需生成一个全局唯一的事务id。全局唯一id生成可参考snowflake
每个数据库,新增一张事务状态表transaction_action,表结构字段如下:
1、id:自增id. 主键
2、transaction_type:事务类型。例如购物车添加商品为一个事务类型,订单提交为另一个事务类型
2、transaction_id:事务id。例如每次购物车添加商品,均为一个新的事务
3、action_type:动作类型。例如购物车添加商品,分解为3个动作,就有3个动作类型。
4、input_json:动作输入参数。为动作处理所需的数据。例如购物车冻结库存,应有参数:商品id、库存扣减数量,为方便后续处理,还应有user_id
5、output_json:动作输出结果。为动作后续处理所需的数据。例如购物车冻结库存,应有参数:冻结是否成功。为方便后续动作处理,还将输入参数复制到输出。
6、output_code:动作结果状态码。为方便判断是否需执行相应后续动作。例如购物车冻结库存,输出状态码即为冻结是否成功。
注意:结果成功/失败,一律指业务逻辑上的成败(如库存不足)。系统故障(如数据库连接中断),只能作为异常处理,视为该动作未处理。
7、execute_time:动作执行时间点。用于记录和核对。
以上transaction_type+transaction_id+action_type应建唯一索引, action_type+execute_time+output_code应建索引。
利用数据库事务,实现业务数据与动作状态的一致性。每个动作函数的处理伪代码如下:
var executed = query(SELECT id FROM transaction_action WHERE transaction_type=? and action_type=? and transaction_id=?);
if executed then return; /检查动作是否已处理/
… 实际业务处理中的非写库处理(例如商品信息读取)
BEGIN TRANSACTION
UPDATE stock …… /动作相应的实际业务数据写库操作,例如修改商品库存数/
INSERT transaction_action …… /记录动作处理状态。因唯一索引的存在,可完全避免动作重复处理/
COMMIT
… 实际业务处理中的非写库处理(例如日志输出)
离线定时任务进行动作状态核对。每个链接(一对动作类型),对应一个定时任务。
对比链接前动作记录,后动作有缺失的,进行重新触发。定时任务伪代码如下:
/从前后动作所属的库中,读取近期待核对的动作记录/
var transactions0 = query(SELECT transaction_id FROM transaction_action WHERE action_type=’前动作’ and execute_time>? and output_code IN ?);
var transactions1 = query(SELECT transaction_id FROM transaction_action WHERE action_type=’后动作’ and execute_time>?);
/计算差异记录。从前动作记录中,去除已执行的后记录即可/
var diffTransaction = transactions0 - transactions1;
/根据差异记录,逐个后动作重新触发/
for each transaction in diffTransaction do
{
invoke(transaction);
}
水平分库的数据库本地事务处理
目前Java中数据库访问多用mybatis,并结合spring进行事务管理。一般是使用DataSourceTransactionManager,仅支持单个数据源。
而目前很多基于JDBC驱动的透明水平分库框架,一般不支持数据库事务。
可实现一个支持分库路由的DataSource,每次事务前,先根据分库字段值进行路由切换。可路由DataSource相关实现可参考。
或者用venus-data(非venus-jdbc),应该支持单库本地事务。
动作记录表的分库规则,应与业务分库规则一致,以确保动作记录和业务数据在同一个库。
例如购物车‘写库’动作记录,应按user_id分库。假设库存按商品id分库,则相应的’冻结库存’动作记录,也应按商品id分库