数据库开发者程序员思维

数据库事务与锁详解

2016-09-09  本文已影响755人  aluomaidi

什么是事务(Transaction)?

是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。 事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性。事务是数据库运行中的一个逻辑工作单位,由DBMS中的事务管理子系统负责事务的处理。

举个例子加深一下理解:同一个银行转账,A转1000块钱给B,这里存在两个操作,一个是A账户扣款1000元,两一个操作是B账户增加1000元,两者就构成了转账这个事务。

最后思考一下,怎么样会出现A账户扣款1000元,B账户金额不变?如果你是把两个操作放在一个事务里面,并且是数据库提供的内在事务支持,那就不会有问题,但是开发人员把两个操作放在两个事务里面,而第二个事务失败就会出现中间状态。现实中自己实现的分布式事务处理不当也会出现中间状态,这并不是事务的错,事务本身就是规定不会出现中间状态,是事务实现者做出来的方案有问题。

事务的4个特性

事务并发控制

我们从另外一个方向来说说,如果不对事务进行并发控制,我们看看数据库并发操作是会有那些异常情形,有些使我们可以接受的,有些是不能接受的,注意这里的异常就是特定语境下的,并不一定就是错误什么的。假设有一个order表,有个字段叫count,作为计数用,当前值为100

数据库事务隔离级别

看到上面提到的几种问题,你可能会想,我擦,这么多坑怎么办啊。其实上面几种情况并不是一定都要避免的,具体看你的业务要求,包括你数据库的负载都会影响你的决定。不知道大家发现没有,上面各种异常情况都是多个事务之间相互影响造成的,这说明两个事务之间需要某种方式将他们从某种程度上分开,降低直至避免相互影响。这时候数据库事务隔离级别就粉墨登场了,而数据库的隔离级别实现一般是通过数据库锁实现的。

下面是各种隔离级别对各异常的控制能力:

级别\异常 第一类更新丢失 脏读 不可重复读 第二类丢失更新 幻读
读未提交 Y Y Y Y Y
读已提交 N N Y Y Y
可重复读 N N N N Y
串行化 N N N N N

数据库锁分类

一般可以分为两类,一个是悲观锁,一个是乐观锁,悲观锁一般就是我们通常说的数据库锁机制,乐观锁一般是指用户自己实现的一种锁机制,比如hibernate实现的乐观锁甚至编程语言也有乐观锁的思想的应用。

悲观锁:顾名思义,就是很悲观,它对于数据被外界修改持保守态度,认为数据随时会修改,所以整个数据处理中需要将数据加锁。悲观锁一般都是依靠关系数据库提供的锁机制,事实上关系数据库中的行锁,表锁不论是读写锁都是悲观锁。

悲观锁按照使用性质划分:

悲观锁按照作用范围划分:

乐观锁:顾名思义,就是很乐观,每次自己操作数据的时候认为没有人回来修改它,所以不去加锁,但是在更新的时候会去判断在此期间数据有没有被修改,需要用户自己去实现。既然都有数据库提供的悲观锁可以方便使用为什么要使用乐观锁呢?对于读操作远多于写操作的时候,大多数都是读取,这时候一个更新操作加锁会阻塞所有读取,降低了吞吐量。最后还要释放锁,锁是需要一些开销的,我们只要想办法解决极少量的更新操作的同步问题。换句话说,如果是读写比例差距不是非常大或者你的系统没有响应不及时,吞吐量瓶颈问题,那就不要去使用乐观锁,它增加了复杂度,也带来了额外的风险。

乐观锁实现方式:

乐观锁几种方式的区别:

新系统设计可以使用version方式和timestamp方式,需要增加字段,应用范围是整条数据,不论那个字段修改都会更新version,也就是说两个事务更新同一条记录的两个不相关字段也是互斥的,不能同步进行。旧系统不能修改数据库表结构的时候使用数据字段作为版本控制信息,不需要新增字段,待更新字段方式只要其他事务修改的字段和当前事务修改的字段没有重叠就可以同步进行,并发性更高。

mysql事务隔离级别实战

实践是检验真理的唯一标准,掌握上面的理论之后,我们在数据库上实战一番家里更好地掌握也加深理解,同时有助于解决实际问题。不同数据库很多实现可能不同,这里以mysql为例讲解各种隔离级别下的情况,测试表为user(id,name,gender,passwd,email)。

隔离级别:read-uncommitted

脏读测试流程:

  1. A设置隔离级别为read-uncommitted(注意这里未声明都是session级别,而非全局的),开启事务,查询id=1的记录
  2. B设置隔离级别为read-uncommitted,开启事务,修改id=1的记录,但不提交
  3. A再次查询id=1的记录,和第一次查询的比较一下
  4. B事务回滚,A事务回滚。

A:

这里写图片描述

B:

这里写图片描述

结论:A读到了B没有提交的内容,隔离级别为read-uncommitted的时候出现脏读。

第一类更新丢失测试流程:

  1. A设置隔离级别为read-uncommitted,开启事务,查询id=1的记录
  2. B设置隔离级别为read-uncommitted,开启事务,查询id=1的记录
  3. A修改id=1的记录
  4. B修改id=1的记录
  5. A提交
  6. B回滚
  7. A在查询一次id=1的记录,看看自己的修改是否成功

结论:结果不如我所想的,A的更新成功了,为什么呢?A执行update语句的时候对该条记录加锁了,B这时候根本无法修改直至超时,也就是至少在mysql中在read-uncommitted隔离级别下验证第一类丢失更新,据了解有的数据库好像可以设置不加锁,如果能够不加锁的话则可以实现,也贴一下图吧。

A:

这里写图片描述

B:

这里写图片描述

结论:A的更新丢失,我们希望的结果是3,而实际结果是2,跟java的多线程很像对不对,read-uncommitted隔离模式下会出现第二类丢失更新。

幻读测试流程:

  1. A开启事务,查询user表所有数据
  2. B开启事务,新增一条记录
  3. A再次查询user表所有记录,和第一次作比对
  4. A回滚,B回滚

A:

这里写图片描述

B:

这里写图片描述

结论:A两次查询全表数据结果不同,read-uncommitted隔离模式下会出现幻读。

注:因为后面对这几种异常情况的测试流程基本和上面一样,个别有些差别读者自己注意,另外注意更改隔离级别即可,就能看到对应结果,后面的我只给出进一步能解决的异常测试截图,结论可以参照前面的对照表。

隔离级别:read-committed

脏读测试截图

A:

这里写图片描述

B:

这里写图片描述

结论:A没有读到B没有提交的内容,隔离级别为read-committed的时候不会出现脏读。

隔离级别:repeatable-read

不可重复读测试截图

A:

这里写图片描述

B:

这里写图片描述

结论:A两次读取id=1的数据内容相同,repeatable-read隔离模式下不会出现不可重复读。

隔离级别:Serializable

幻读测试截图

A:

这里写图片描述

B:

这里写图片描述

结论:因为A事务未提交之前,B事务插入操作无法获得锁而超时,Serializable隔离模式下不会出现幻读。

上一篇 下一篇

猜你喜欢

热点阅读