ITPub博客

首页 > 数据库 > MySQL > 最终一致性落地实践

最终一致性落地实践

原创 MySQL 作者:壹頁書 时间:2016-06-04 01:59:55 0 删除 编辑
http://blog.itpub.net/29254281/viewspace-1819422/

单位最近赶工,要做一款直播APP
本来关于后端,我建议了很多设计
包括
1.应用Redis键空间/事件通知功能,做两级缓存(Redis,JVM)
2.Redis读写分离.(这个很好做,但是单位大部分项目,还是没有这么用.主要是访问量还是低)
3.单线程处理并发控制
4.最终一致性分库设计.
5.系统基于ES弹性搜索构建,而不是基于数据库构建.(不涉及事务的查询,一律走搜索进行查询)
6.ELK监控事务补偿

最终除了数据库最终一致性的设计,其余都被拍了回来...
用之则行,舍之则藏
人不能逆势而行。。。

最终一致性,无外乎用Translog和Msglog保障用户的帐号余额最终一致.
如果不一致则事务补偿.
  1. create table account_diamond(  
  2.     UserID bigint primary key comment '用户ID',  
  3.     DiamondCount int not null default 0 comment '剩余钻石数量',  
  4.     State smallint not null default 1 comment '状态.0表示帐号锁定.1表示正常',  
  5.     CheckSum varchar(32) not null comment '用户ID和剩余数量的校验',  
  6.     CreateTime timestamp not null default current_timestamp comment '创建时间',  
  7.     UpdateTime timestamp not null default current_timestamp on update current_timestamp comment '更新时间'    
  8. ) comment '帐号钻石表';  
  9.   
  10.   
  11. create table translog(  
  12.     TransLogID bigint not null,  
  13.     Params Text not null comment '参数信息',  
  14.     SenderUserID bigint not null comment '发送方用户ID',  
  15.     CreateTime timestamp not null default current_timestamp comment '创建时间',  
  16.     CreateYear year not null,  
  17.     key(CreateTime),  
  18.     primary key(TransLogID,CreateYear)  
  19. ) comment '事务表'  
  20. PARTITION BY RANGE(CreateYear)  
  21. (  
  22. PARTITION p2016 VALUES LESS THAN (year('2016-01-01')),  
  23. PARTITION p2017 VALUES LESS THAN (year('2017-01-01')),  
  24. PARTITION p2018 VALUES LESS THAN (year('2018-01-01')),  
  25. PARTITION p2019 VALUES LESS THAN (year('2019-01-01')),  
  26. PARTITION p2020 VALUES LESS THAN (year('2020-01-01'))  
  27. );  
  28.   
  29.   
  30. create table msglog(  
  31.     TransLogID bigint not null,  
  32.     Params Text not null comment '参数信息',  
  33.     ReceiverUserID bigint not null,  
  34.     CreateTime timestamp not null default current_timestamp comment '创建时间',  
  35.     CreateYear year not null,  
  36.     primary key(TransLogID,ReceiverUserID,CreateYear)  
  37. ) comment '消息表'  
  38. PARTITION BY RANGE(CreateYear)  
  39. (  
  40. PARTITION p2016 VALUES LESS THAN (year('2016-01-01')),  
  41. PARTITION p2017 VALUES LESS THAN (year('2017-01-01')),  
  42. PARTITION p2018 VALUES LESS THAN (year('2018-01-01')),  
  43. PARTITION p2019 VALUES LESS THAN (year('2019-01-01')),  
  44. PARTITION p2020 VALUES LESS THAN (year('2020-01-01'))  
  45. );  
  46.    

因为时间短,我在原来分库的架构上,做了一些简化.
原来的设计,是有队列的.
我开始用全局的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/ ,如需转载,请注明出处,否则将追究法律责任。

下一篇: Snowflake
请登录后发表评论 登录
全部评论

注册时间:2013-10-19

  • 博文量
    620
  • 访问量
    6342988