http://blog.itpub.net/29254281/viewspace-1819422/
单位最近赶工,要做一款直播APP
本来关于后端,我建议了很多设计
包括
1.应用Redis键空间/事件通知功能,做两级缓存(Redis,JVM)
2.Redis读写分离.(这个很好做,但是单位大部分项目,还是没有这么用.主要是访问量还是低)
3.单线程处理并发控制
4.最终一致性分库设计.
5.系统基于ES弹性搜索构建,而不是基于数据库构建.(不涉及事务的查询,一律走搜索进行查询)
6.ELK监控事务补偿
最终除了数据库最终一致性的设计,其余都被拍了回来...
用之则行,舍之则藏
人不能逆势而行。。。
最终一致性,无外乎用Translog和Msglog保障用户的帐号余额最终一致.
如果不一致则事务补偿.
-
create table account_diamond(
-
UserID bigint primary key comment '用户ID',
-
DiamondCount int not null default 0 comment '剩余钻石数量',
-
State smallint not null default 1 comment '状态.0表示帐号锁定.1表示正常',
-
CheckSum varchar(32) not null comment '用户ID和剩余数量的校验',
-
CreateTime timestamp not null default current_timestamp comment '创建时间',
-
UpdateTime timestamp not null default current_timestamp on update current_timestamp comment '更新时间'
-
) comment '帐号钻石表';
-
-
-
create table translog(
-
TransLogID bigint not null,
-
Params Text not null comment '参数信息',
-
SenderUserID bigint not null comment '发送方用户ID',
-
CreateTime timestamp not null default current_timestamp comment '创建时间',
-
CreateYear year not null,
-
key(CreateTime),
-
primary key(TransLogID,CreateYear)
-
) comment '事务表'
-
PARTITION BY RANGE(CreateYear)
-
(
-
PARTITION p2016 VALUES LESS THAN (year('2016-01-01')),
-
PARTITION p2017 VALUES LESS THAN (year('2017-01-01')),
-
PARTITION p2018 VALUES LESS THAN (year('2018-01-01')),
-
PARTITION p2019 VALUES LESS THAN (year('2019-01-01')),
-
PARTITION p2020 VALUES LESS THAN (year('2020-01-01'))
-
);
-
-
-
create table msglog(
-
TransLogID bigint not null,
-
Params Text not null comment '参数信息',
-
ReceiverUserID bigint not null,
-
CreateTime timestamp not null default current_timestamp comment '创建时间',
-
CreateYear year not null,
-
primary key(TransLogID,ReceiverUserID,CreateYear)
-
) comment '消息表'
-
PARTITION BY RANGE(CreateYear)
-
(
-
PARTITION p2016 VALUES LESS THAN (year('2016-01-01')),
-
PARTITION p2017 VALUES LESS THAN (year('2017-01-01')),
-
PARTITION p2018 VALUES LESS THAN (year('2018-01-01')),
-
PARTITION p2019 VALUES LESS THAN (year('2019-01-01')),
-
PARTITION p2020 VALUES LESS THAN (year('2020-01-01'))
-
);
-
因为时间短,我在原来分库的架构上,做了一些简化.
原来的设计,是有队列的.
我开始用全局的TransLogID mod 3 写日志.
如果成功,则分为两个Callable(发送方增加余额的任务和接收方扣减金额的任务)挂到线程池队列中处理.
最后,更新Redis发送方的缓存.
后来发现,这个结构无法获取发送方真实的帐号余额.因为有一部分应该扣减的金额,还在线程池队列中等待处理.
当时开会讨论的时候,领导们居然没人注意到这个问题,通过了这个方案.
这个问题如果不能处理,就有点丢人现眼了.
第一版补救..原来按照TransLogID mod 3 找到数据库,写Translog表
那么现在改为 发送方ID mod 3 找到数据库,写Translog表.
这样,最终处理的时候,发送方的帐号表,Msglog表和Translog表都会落在同一个数据库上.
如果给Translog表,增加一个状态字段,表明这个事务正在处理 还是 已经完成.
发送方的扣减过程,以更新translog的状态,作为事务完成的标志.
这样发送方的事务完成标志由translog记录,就和msglog没有关系了.
msglog仅仅记录接收方的情况.最终用于比对
这样,统计发送方余额的时候,我可以锁定发送方的帐号,和正在处理的Translog记录,就可以得到用户的真实余额。
translog中未处理的记录,就是在线程池队列中已经扣减的金额
第二版补救
后来我分析了一下场景
检查帐号余额需要一个当前读的select,锁定用户的帐号信息和未处理的translog信息.
写translog 需要一个Insert
发送方任务处理的时候,
一个update更新帐号余额,记录两个业务日志
update translog状态.
这个过程,几个行锁,三个insert,两个update,一个select一共6个SQL
与其这样,不如改为同步扣减发送方帐号,异步处理接收方信息.
发送方的扣减过程如下
1.调用check_diamondaccount_exist
检查钻石帐号是否存在
是否有足够钻石余额
帐号是否被锁定
数据是否被篡改
2.如果通过第一步的检查,扣减发送方的钻石帐号
3.写入事务日志表,调用insert_translog.并且提交数据库事务
4.JAVA组成一个异步任务,用于增加接收方的票帐号
5.更新Redis 发送方的钻石帐号余额
一次当前读,锁定一行用户钻石帐号记录
两次Insert业务日志
一次update,修改帐号余额
一次Insert translog 记录
相对于之前的设计,少了一个SQL,并且还少了记录的锁定,translog不用锁定读了.
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/29254281/viewspace-2113781/
,如需转载,请注明出处,否则将追究法律责任。