一讲到事物大家都知道,事物的其中一个特点:事务中的多个命令要么全部执行成功(提交)、要么全部执行失败(回滚),但是要是讲到了事物的并发安全问题,事物与锁的关系,可就总说纷纭了
比如我曾听到的这些:
有的人说用了事物,你的所有操作就是安全的,因为事物会加锁,加锁不就并发安全嘛,事物能防止并发安全问题。(不能说全错,也不能说全对)。分不清乐观锁,悲观锁,编程语言锁。不知道什么时候用乐观锁,什么时候用悲观锁,有的知道用锁,但是用是编程语言级别的锁比如java中的syac...(太长拼不来)和C#中的lock中的锁(除非你是单应用,不然还是有区别的),并将他们混为一谈(确实他们概念上是差不多的,但是应用场景不同,锁只是一种思想,不是特有的)。
这些问题我希望通过下面的介绍你们能自行解答。当然最后还会给出总结。
什么是事务?
事务,即数据库事务。是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
通常,事务的正确执行会使数据库从一种状态转换为另一种状态。
事务的特性(ACID原则)
- 原子性(atomicity) 即不可分割性,事务要么全执行、要么全不执行。
- 一致性(consistency) 事务的执行使得数据库从一种正确状态转换成另外一种正确状态。
- 隔离性(isolation) 在事务正确提交之前,不允许把事务对该数据的改变提供给任何其他事务。
- 持久性(durability) 事务正确提交之后,其结果将永远保存在数据库之中。
并发状态下事务会产生的问题
并发状态解释为当事务A和事务B对同一资源进行操作时,可能会遇到很多的问题。
脏读(针对未提交数据)
即事务A读到了事务B还没有提交的数据。如果事务A对数据进行了更新,但是事务A并没有提交,但是事务B这个时候看到了事务A没有提交的更新。当事务A进行了回滚,那么刚刚事务B看到的数据就是脏数据。也就是脏读。
例子:
A 给 B 转了100万,但是 A 还没有提交,此时 B 查询自己账户,多了100万。然后 A 发现转错人了,回滚了事物。然后 B 100万就没了。在这个过程中 B 查到了没有提交的数据(多出的100万),这就是脏读。
不可重复读(在一个事务里面读取了两次某个数据,读出来的数据不一致,针对修改操作)
即同一事务在事务执行过程中对同一个数据进行了多次读取,但是每一次读取的数据结果都不相同。原因是在两次读取间隔,数据别其他人修改了,导致了统一事务两次读取结果不一致。
例子:
A 查询银行余额为100万,B 这个时候取走了50万,此时余额变成了50万,A 再一次查询余额,变成了50万。对 A 而言两次结果不一致就是不可重复读。
幻读(在一个事务里面的操作中发现了未被操作的数据,针对增删操作)
即在事务 A 多次读取数据集的过程中中,事务 B 对数据进行了新增操作或者删除操作,导致事务 A 多次读取的数据集不一致。
例子:
A 修改当前公司所有职员信息的时候,B 向其中插入了一个新的职员,这个时候 A 提交的时候发现了一个自己没有修改过的职员的信息,对 A 而言就像是产生了幻觉。
事务的隔离级别
为了应对上面并发情况下出现的问题,事务的隔离级别就产生了。当事务的隔离级别越高的时候,上面的问题就会越少,但是性能消耗也会越大。所以在实际生产过程中,要根据需求去确定隔离级别。
四种隔离级别
READ_UNCOMMITTED
读未提交,即能够读取到没有被提交的数据,所以很明显这个级别的隔离机制无法解决脏读、不可重复读、幻读中的任何一种。
READ_COMMITED
已提交,即能够读到那些已经提交的数据,能够防止脏读,但是无法解决不可重复读和幻读的问题。
REPEATABLE_READ
重复读取,即在数据读出来之后加锁,类似"select * from XXX for update",明确数据读取出来就是为了更新用的,所以要加一把锁,防止别人修改它。REPEATABLE_READ的意思也类似,读取了一条数据,这个事务不结束,别的事务就不可以改这条记录,这样就解决了脏读、不可重复读的问题,但是幻读的问题还是无法解决。
SERLALIZABLE
串行化,最高的事务隔离级别,不管多少事务,挨个运行完一个事务的所有子事务之后才可以执行另外一个事务里面的所有子事务,这样就解决了脏读、不可重复读和幻读的问题了。
这篇文章详细说明了事物的特点,以及详细说明了事物隔离级别。下面我对他进行补充。
首先需要补充一点:
一般DBMS在综合安全问题和性能问题会选择将事物的默认级别设置成: REPEATABLE_READ(可重复读),事物的隔离级别越高,并发性能越差,隔离级别越低,数据一致性越差。
好的,上面说到了REPEATABLE_READ特征是:类似"select * from XXX for update",及类似悲观锁注意我特别的标注了是类似(作者用词也是很准的)。“for update”是悲观锁的写法,这就要谈谈REPEATABLE_READ的隔离性和悲观锁的区别了
相同点和不同点
相同点是:保证数据只能被一个事物修改。不同点是:一个是在读的时候锁,一个是在写的时候锁
悲观锁(读锁)
悲观锁是通过阻止读取同一份数据的事物来做到的。比如a事物读取u用户余额,为了防止其它事物修改它,悲观锁在读取u用户余额的同时给数据加了锁(for update,至于这是行及锁还是表级锁,取决于你的过滤条件),其它事物也要获取u用户余额,但由于a事物给它加了锁(读锁),其它事物必须等待a事物的完成(提交/回滚/超时),才能获取到u用户余额。这个锁类似,注意是类似C#的lock和java的sync,C#的lock是内存对象锁,db悲观锁是数据库级别的锁,一个数据库可能有多个应用,应用之间的内存是不共享的,你锁不住其它应用的内存对象
REPEATABLE_READ的锁(写锁)
REPEATABLE_READ(写锁)是通过阻塞其它要修改这份数据的事物来保证数据只能被一个事物修改。比如:如果a事物修了u用户余额(还未提交),其它要修改u用户的事物都将阻塞,直至a事物完成(提交/回滚/超时),REPEATABLE_READ这个隔离级别使用的就是写锁的原理。这个过程用mysql控制台开启事物可以模拟。
丢失更新
REPEATABLE_READ虽然采用了写锁,但是它真的就安全了吗?这要看你代码怎么写,所以我们必须深入了解它们,假设现在事物的隔离级别是默认的(REPEATABLE_READ),下面看一段伪代码代码
//事物A
dbcontext.beginTransaction();
var user = dbcontext.getuser(1);
user.balance -= 1;
var bill=new {id=1,balace=1};
dbcontext.insert(bill);
dbcontext.update(user);
Thead.Sleep(3000);
dbcontext.commit();
//事物B(和事物A一样)
dbcontext.beginTransaction();
var user = dbcontext.getuser(1);
user.balance -= 1;
var bill=new {id=1,balace=1};
dbcontext.insert(bill);
dbcontext.update(user);
dbcontext.commit();
时刻
事物A执行到Thead.Sleep(3000),事物B执行到了var user = dbcontext.getuser(1);假设user.balace的初始值是100。
过程
在事物A执行到Thead.Sleep(3000)时,数据库中这个用户的还是余额是100,A事物函数内存中的user.balace已经是99了,而在事物B的函数内存中user.balace是100(已经执行读取了单未执行user.balace-=1时是100,此时A由于阻塞还未提交)
现在A,B都执行完了(都提交了,B肯定得等A提交完,提及完了B才能提交)。
结果
我们分析数据库的情况,此时user(id=1)的用户余额一定是99,但是账单上却出现了两条记录两条(你的余额-1),账单有两条是正常的,但是余额是99就是不正常的,造成了数据部一致
我们吧这种情况叫做更新丢失。显然,虽然REPEATABLE_READ内置写锁,但是这个内置的锁会产生新的问题。
防止更新丢失的解决方法
1,悲观锁
2,乐观锁
3,将事物隔离级别设置为串行化
现在知道了读锁,谢锁,悲观锁,乐观锁,编程语言锁,之间的关系了吧
这里留下几个疑问
1.如果不是利用了REPEATABLE_READ这个隔离级别,其它隔离级别下这个写法能防止丢失更新吗?
update user
set balace=@new_balace,version=uuid()
where id=1 and version=@new_version
答:可以,乐观锁不依赖数据库的实现
2.这个写法在REPEATABLE_READ这个隔离级别,和多事物环境下能防止丢失更新吗?
update user
set balace=balace-1
where id=1
答:能REPEATABLE_READ内置写锁,而且没有将用户余额加载到内存