一讲到事物大家都知道,事物的其中一个特点:事务中的多个命令要么全部执行成功(提交)、要么全部执行失败(回滚),但是要是讲到了事物的并发性,以及事物在并发过程中的安全问题,可能就总说纷纭了
比如这些:
有的人说用了事物,你的所有操作就是安全的,因为事物会加锁,加锁不就并发安全嘛。(不能说全错,也不能说全对)很多人都这么认为,他们都这么跟我说的。分不清乐观锁,悲观锁。不知道什么时候用乐观锁,什么时候用悲观锁,
有的知道用锁,但是用是编程语言级别的锁比如java中的syac...(太长拼不来)和C#中的lock中的锁(除非你的单应用,不然还是有区别的),并将他们混为一谈(确实他们概念上是差不多的,但是应用场景不同,锁只是一种思想,不是特有的),有的救认为悲观锁比乐观锁好。分不清行锁,表锁,读锁,写锁,丢失更新等。
这些问题我希望通过下面的介绍你们能自行解答。当然最后还会给出总结。
下面先引入事物的概念:
引用自:https://www.cnblogs.com/dear_diary/p/11106609.html
什么是事务?
事务,即数据库事务。是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
通常,事务的正确执行会使数据库从一种状态转换为另一种状态。
事务的特性(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)的隔离性和悲观锁的区别了
相同点和不同点:
相同点是保证数据只能被一个事物修改,这个讲的很抽象。详细点说:悲观锁是通过阻止其它事物读取同一个份数据来做到的,REPEATABLE_READ(乐观锁)是通过阻塞其它要修改同一份数据的事物。比如a事物读取用户余额,在将用户余额-1,为了防止其它事物修改它(这条记录),悲观锁在读取用户余额同时给数据加悲观锁(for update,至于这是行及锁还是表级锁,取决于你的过滤条件),b事物也要去做这件事情,但由于a事物给它加了锁(读锁),事物b必须等待a事物的完成(提交/回滚/超时),你才能获取到用户当前余额,进而才能对用户余额减一(这是不是并发性能不好了,两个事物读同一个数据必须得等待,但是安全了是吧)这个锁类似(注意是类似C#的lock和java的sync,C#的lock是内存对象锁,db悲观锁是数据库级别的锁,一个数据库可能有多个应用,应用之间的内存是不共享的,你锁不住其它应用的内存对象)。而REPEATABLE_READ与悲观锁做法不同,REPEATABLE_READ虽然也能保证同一个数据只能被一个事物修改(相同点),但是这个数据可以同时被多个事物读取(悲观锁只能被一个事物读取),直到被其中一个事物(a)修改的那一刻,其它事物要修改这个事物(a)的事物才需要等待(写锁)。比如:如果a事物修改了用户余额,b也去修改这个用于的余额,b事物必须等待a事物(提交/回滚/超时)后才能修改。显然他们的不同点是:锁的时机和方式不同,一个获取到了,就不给别人获取(悲观锁),另一个是一个人修改了就不然别人改了(乐观锁,REPEATABLE_READ这个隔离级别使用的就是了乐观锁的原理)。这个过程用mysql控制台开启事物可以模拟。
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执行到,dbcontext.update(user);之后dbcontext.commit();之前时,事物B执行到了var user = dbcontext.getuser(1);,假设user.balace初始值是100,
在事物A执行到我说的位置时,数据库中这个用户的余额是100,A事物函数内存中已经是99了,而在事物B的函数内存中(已经执行读取了,此时A任然由于阻塞还未提交)
假设现在事物的隔离级别是默认的(REPEATABLE_READ),那么事物B中的user.balace读出来的还是是100(因为事物A未提交),现在A,B都执行完了(都提交了,B肯定得等A提交完,提及完了B才能提交)。
我们分析数据库的情况,此时user(id=1)的用户余额一定是99,并且账单上出现了两条记录两条(你的余额-1),账单有两条正常,但是余额是99就不正常了,造成了数据部一致
我们这这种情况叫做更新丢失。显然,虽然REPEATABLE_READ内置乐观锁,但是这个内置的锁会产生新的问题
防止更新丢失的解决方法
1,悲观锁
2,不要将数据加载到内存(update user balace=balace-1 where id=1),或者给数据加上一个版本号,(update user balance =balace-1,version=uuid() where id=1 and version=@old_version)
3,将事物隔离级别设置为串行化
现在知道了读锁,谢锁,悲观锁,乐观锁,编程语言锁,之间的关系了吧
。这里留下一个疑问
update user balace=balace-1 where id=1 为什么这种方式可以防止更新丢失,而不用判断最后的受影响行数?
update user balance =balace-1,version=uuid() where id=1 and version=@old_version 这种写法必须判断最后的受影响的行数来决定是否继续(及数据是否已经变脏了)。
显然造成丢失更新的问题是由于REPEATABLE_READ(乐观锁)本身的缺陷,EPEATABLE_READ允许其它事物读,直到修改的那一刻才进行阻塞,阻塞完了才能继续,而由于这个阻塞会导致。可在读完之后,数据已经被其它事物修改了,而你任然允许我提交。