欢迎访问昆山宝鼎软件有限公司网站! 设为首页 | 网站地图 | XML | RSS订阅 | 宝鼎邮箱 | 后台管理


新闻资讯

MENU

软件开发知识

应该怎么选择? 有兴趣或有见解的同学可以留言回复或私信~~ 参考 http:// 次  来源:劳务派遣管理系统 时间:2018-04-26

原文出处: hebaodan

线上账务系统余额并发更新问题记录

某电商平台,某天线上用户报bug说账户余额信息与生意业务流水对不上。可以认为是数据库并发更新问题,由此定位出详细原因,并给出办理方案。

问题现象

场景描写

线上账务系统,在按时结算给卖家钱时,昆山软件开发,且高并发量的环境下,呈现提现x元(假设当前用户余额为x元)余额为0后,再转入该账户一笔钱(假设为y元),功效账户余额变为了x+y 元,导致用户余额错误。 ps:账户余额的改观都是在事务中update的

情况说明

mysql5.7 + innodb,事务断绝级别是REPEATABLE-READ

场景模仿

我们简化下线上的数据布局,举办场景模仿。 数据表如下:

‘账户主表’

CREATE TABLE user (

uid int(11) NOT NULL COMMENT '范例id+自增序列',

name varchar(32) DEFAULT NULL,

PRIMARY KEY (uid)

) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='账户主表'

‘账户余额明细表’

CREATE TABLE user_account (
uid int(11) NOT NULL,
amount decimal(19,4) DEFAULT 0 COMMENT '账户余额',
PRIMARY KEY (uid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='账户余额明细表'

账户范例设置

CREATE TABLE user_conf (

type_id int(11) NOT NULL, description varchar(32) DEFAULT NULL COMMENT '范例描写', PRIMARY KEY (type_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='账户范例设置'

详细数据为:

select * from user;

+-------+------+ | uid | name | +-------+------+

| 10001 | a |

| 10002 | b |

select * from user_account;

+-------+----------+ | uid | amount | +-------+----------+

| 10001 | 10.0000 |

| 10002 | 108.9900 |

select * from user_conf;

+---------+--------------+ | type_id | description | +---------+--------------+

| 100 | 外部账户 |

| 200 | 内部账户 |

模仿提现(即余额减)和入账(即余额加)并发操纵的事务如下:

session1-提现10元 session2-入账20元
begin;
select description from user_conf where type_id = 100;
select * from user where uid = 10001 for update; // user表用来做互斥
select amount from user_account where uid = 10001; // 10.00
begin;
select description from user_conf where type_id = 100;
select * from user where uid = 10001 for update; // wating
//wating
update user_account set amount = 0.00 where uid = 10001;
commit;
拿到锁
select amount from user_account where uid = 10001; //10.00
入账20元,昆山软件开发,代码中计较后应该为30元
update user_account set amount = 30.00 where uid = 10001;
commit;

问题呈现了,后头再查询该用户余额为30元,即用户提现的10元未反应在余额中

原因定位

熟悉mysql的同学或者已经知道问题是由REPEATABLE-READ断绝级别下快照读导致。

详细表明:

RR级别下,第一次读操纵会生成快照,对付可见性来说,只有当第一次读之前其他事务提交的修改和本身的修改可见,其他的均不行见。

官网文档:https://dev.mysql.com/doc/refman/5.7/en/glossary.html snapshot A representation of data at a particular time, which remains the same even as changes are committed by other transactions.

With REPEATABLE READ isolation level, the snapshot is based on the time when the first read operation is performed.

可见性道理

可参考文章:http://hedengcheng.com/?p=148

回到上述模仿场景中,session2在sql语句select description from user_conf where type_id = 100; 时已生成快照,固然session1提交了,但仍然不行见,导致并发更新问题。

别的,开启事务后,SELECT … FOR UPDATE 是不会生成快照的,各人可自行尝试

办理方案

方案一

将REPEATABLE-READ断绝级别改为READ-COMMITTED,这样即能看到最新提交的数据。

方案二

在读’账户余额明细表’user_account 的时候加 for update,这样会 1.强制读该行记录的最新版本数据,2.且若其他事务未commit,昆山软件开发,才干务将阻塞,担保串行更新

方案三

延时生成快照。开启事务后,首先就通过user表做互斥,直接for update加锁,针对多个事务并发更新即变为串行。

附:定位进程

  1. 针对上报bug用户,查询其生意业务流水明细与余额改观明细,确认账务存在问题
  2. 查询账务系统近几天是否有上线改观,查抄无
  3. 拉取账务数据库mysql general log,找到并发更新的两个事务session
  4. 查询数据库配置的断绝级别为RR,查询应用数据库毗连池设置即session的断绝级别未设置,回收数据库设置
  5. 确认由RR级别导致(虽然也可以认为是代码问题导致)
  6. 确认是一个月前账务系统分库分表上线,改用其他毗连池且未配置session断绝级别。而之前是有设置session的断绝级别为READ-COMMITTED。

延伸思考