记录一下一个并发导致的脏数据问题(基于MySQL)。问题描述(银行操作员例子):比如A、B操作员同时读取一余额为1000元的账户,A操作员为该账户增加100元,B操作员同时为该账户扣除50元,A先提交,B后提交。最后实际账户余额为1000-50=950元,但本该为1000+100-50 = 1050 。
首先分析下如何会导致问题的出现:
我们来些一个伪代码表示下A操作员操作执行的过程:
User user = userDao.getById(1); ----------(步骤1)
user.setAccount(user.getAccount()+100); ----------(步骤2)
userDao.update(user); ------------------(步骤3)
而B操作员执行的过程为:
User user = userDao.getById(1); ----------(步骤1)
user.setAccount(user.getAccount()-50); ----------(步骤2)
userDao.update(user); ------------------(步骤3)
当A操作的程序执行到步骤1之后,步骤3之前的时候B执行了步骤1 。之后A执行了步骤3,B再执行步骤2跟3 。此时问题就出现了。这就是典型的因为并发而导致脏数据的问题。有人说给代码加事务啊,在方法上加入@Transational就不会出现这问题了嘛。在这我想说这样做是没用的。当然,加事务也是可以解决这个问题的,但是你需要设置事务的隔离级别为SERIALIZABLE(可串行化)才能做到不会有幻读的出现。但是这样做在并发的情况下效率会特别低下(每个对该数据行的请求,包括读跟写的请求都需要等待前面的读跟写全部执行完才能执行)。然而对于MySQL的默认事务隔离级别是REPEATABLE(可重复读),在这个隔离级别下,即使A执行步骤1后给数据行加上了读锁,但是B执行步骤1时也可以对数据行进行读取,此时就出现了幻读的问题。所以单纯的在方法上加入@Transational注解是解决不了问题的。
相关知识点:
事务的四个特性:原子性,一致性,隔离性,持久性
① Serializable (串行化):可避免脏读、不可重复读、幻读的发生。
② Repeatable read (可重复读):可避免脏读、不可重复读的发生。
③ Read committed (读已提交):可避免脏读的发生。
④ Read uncommitted (读未提交):最低级别,任何情况都无法保证。
那么什么是脏读,幻读呢?
1,脏读
脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。例如:用户A向用户B转账100元,对应SQL命令如下
update user set account=account+100 where name=’B’; (此时A通知B)
update user set account=account - 100 where name=’A’;
当只执行第一条SQL时,A通知B查看账户,B发现确实钱已到账(此时即发生了脏读),而之后无论第二条SQL是否执行,只要该事务不提交,则所有操作都将回滚,那么当B以后再次查看账户时就会发现钱其实并没有转。
2,不可重复读
不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发送了不可重复读。
不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询就可能不同,A和B就可能打起来了……
3,虚读(幻读)
幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。
幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。
巴拉巴拉了一大堆,现在来讲讲解决方案(自己的想法,有不足之处请大家帮忙指出~)
对于这个问题我们可以使用乐观锁的方式来实现:在user表中加入一个version字段,每次执行update时version++;
操作员A执行的sql操作:
select id, account,version from user where id="1";
查询结果:id=1, account=1000, version=1
update user
setaccount=account+100, version=version+1
whereid="1" and version=1
select id, account,version from user where id="1";
查询结果:id=1, account=1100, version=2
操作员B执行的sql操作
select id, account, version from user where id="1";
查询结果:id=1, account=1000, version=1
#操作员A已修改成功,实际user.account=1100、user.version=2,操作员B也将版本号加一(version=2)试图向数据库提交数据(account=950),但此时比对数据库记录版本时
发现,操作员B提交的数据版本号为2,数据库记录当前版本也为2,不满足 “提交版本必须大于记录当前版本才能执行更新 “的乐观锁策略,因此,操作员B的提交被驳回。
update userset account=account-50, version=version+1 where id="1" and version=1
select id, account, version from user where id="1";
查询结果:id=1, account=1100, version=2