数据库开发中的并发问题

一些术语

1. 并发

在数据库中,在相同的时间段内,超过两个(含)以上的会话事务对共享数据进行存取操作(包括查询、插入、修改、删除等)即形成并发。
= + 并发 = 共享数据 + 对共享数据的操作

2. 并发冲突/问题

如果多会话事务并发操作共享数据时出现了问题,则称发生了并发冲突。
= + 并发冲突 = 共享数据 + 对共享数据的问题操作

3. 并发一致性

如果多会话事务并发操作的共享数据正确,则称并发一致性。指数据在多会话事务并发运行时的一致性。多会话事务并发运行时,要保证操作共享数据的正确性。

数据业务一致性:指数据在业务逻辑上的一致性。一事务内部SQL操作系列,要保证相关数据在业务逻辑上的一致性。
= + 并发一致性 = 共享数据 + 对共享数据的正确操作

4. 并发性

在保证数据正确的前提下, 在相同的时间段内,同时运行的程序数量。
= + 加大并发性 = 有并发冲突的会话事务串行运行 + 无并发冲突的会话事务并发运行

数据库事物

当用户与数据库建立会话之后,便可以与数据库进行相应权限的操作,而这就必然涉及到了事物。

事物是一组具有ACID属性的SQL命令组成的单个逻辑工作单元。事务是一个逻辑操作序列,这些逻辑操作要么都执行,要么都不执行,它是一个不可分割的工作单位。数据库事务只能保证单线程数据的正确性。

事物具有四个基本属性ACID,即原子性、一致性、隔离性和持久性。

原子性(Atomic)

指事务中的SQL操作要么全部成功(提交事务写日志),要么全部失败(回滚事务)。想要保证事务的原子性,就意味着需要在操作发生异常时,对该事务所有之前执行过的操作进行回滚。回滚是通过回滚日志(Undo Log)实现的。简单的说,回滚日志就是记录了你所有操作的逆操作,在需要回滚时,就把这个事务的回滚日志里的操作全部执行一次。需要注意的是原子性是由事务日志保证的,与开发人员无关。开发人员只需要选择事物的提交模式以及事物的开始和结束时机。

隔离性(Isolation)

指该会话事务内部的SLQ操作及操作的数据库对象对并发的其它会话事务是隔离的。事务的隔离级别是由数据库提供的,数据库隔离级别在服务器端保证客户端用户一定不会发生哪些并发冲突。服务器端采用的技术手段有隐式锁、多版本(MVCC)。开发人员可以选择使用哪种隔离级别并且指定显示锁。隔离级别有RU,RC,RR和S,这主要是用于数据库多用户并发控制,与单用户关系不大。

持久性(Durability)

指事务一旦被提交,对数据库的改变是永久的。数据一定会被写入到数据库中并持久储存起来。当事务被提交后就无法再回滚。持久性是由事务日志保证的。也需要开发人员commit来保证。

一致性(Consistency)

事务的一致性是指在没有并发其他事务下(单用户),事务执行前和执行后的数据都要满足数据库约束(完整性约束(列约束、行约束)和自定义约束(触发器、存储过程))和业务逻辑约束。在关系型数据库中,所有的规则必须应用到事务的修改上,以便维护所有数据的完整性。

事物一致性基本可以理解为是事务对数据完整性约束的遵循。这些约束可能包括主键约束、外键约束或是一些用户自定义约束(触发器等)。事务执行的前后都是合法的数据状态,不会违背任何的数据完整性,这就是“一致”的意思。

同时这个含义中也隐含着对开发者的要求,就是不能写出错误的事务逻辑,比如银行的转账不能只加钱不减钱,这是应用层面的一致性要求。

并发产生的冲突

读不一致性

读不一致性本质上是读写操作的不一致性

  1. 脏读:一个事务读取了另一个事务未提交的数据。当事务A查询事物B修改后但是未提交的数据时,事物B有可能会因为某些情况进行回滚,此时事物A读到的数据就是错的,这是的数据也称之为脏数据。它是对一条记录而言的。脏读本质上是读写操作的冲突,解决办法是写完之后再读。
  2. 不可重复读:一个事务两次读取同一个数据,两次读取的数据不一致。当事务A查询第一次是,事物B虽然修改了数据但是没有提交,此时事物A查询的还是之前未修改的值,但是当事物B提交后,事物A第二次查询,此时查询到的值是事物B修改后的值,这次查询的值不一样,即读取的数据不一致。它是对一条记录而言的。不可重复读本质上是读写操作的冲突,解决办法是读完再写。
  3. 幻象读:一个事务两次读取一个范围的记录,两次读取的记录数不一致。假设事务A第一次查询一段范围的rows中的某数据时,此时有5列。但是之后又另外一个事物B增加了一列,当事物A再次进行相同的查询时,发现有6列,这就是所谓的幻读。。它是对多条记录而言的。幻读本质上是读写操作的冲突,解决办法是读完再插入/删除。
写不一致性

写不一致性本质上是写写操作的不一致性
丢失修改即一个事务的更新覆盖了另一个事务的更新。事务A和事务B需要对同一个row的元素进行修改。A和B同时读到该row的的数据,分别修改,后提交的事务B覆盖了事务A的更新。更新丢失本质上是写写操作的冲突,解决办法是一个一个地写。
防止丢失修改的并发控制类型常见的有三种方法:

  1. 保守式并发控制(锁) - 在从获取记录直到记录在数据库中更新的这段时间内,该行对用户不可用。
  2. 开放式并发控制(原始值) - 只有当实际更新数据时,该行才对其他用户不可用。更新将在数据库中检查该行并确定是否进行了任何更改。如果试图更新已更改的记录,则将导致并发冲突。
  3. 最后的更新生效 - 只有当实际更新数据时,该行才对其他用户不可用。但是,不会将更新与初始记录进行比较;而只是写出记录,这可能就改写了自上次刷新记录后其他用户所进行的更改。

多用户并发控制的手段

服务器端

隔离级别

事物的隔离级别是指一个会话事务对数据库的存取与并发的另一个会话事务的隔离程度称为隔离级别。数据库规定了多种事务隔离级别, 不同隔离级别对应不同的干扰程度, 隔离级别越高, 并发一致性就越好, 但并发性越弱。

  1. 读未提交(RU)
    具有Read uncommitted隔离级别的事务,允许读取未被其他事务提交的变更。当隔离级别设置为Read uncommitted时,就可能出现脏读、不可重复读、幻读。(丢失修改也会出现。)选择Read uncommitted的原因是,在只读的历史数据库中,可以提高效率。(因为不存在更改,也根本就不会存在脏读)
    如何避免脏读,请看下一个隔离级别。
  2. 读已提交(RC)
    具有Read committed隔离级别的事务,确保只允许读取已经被其他事务提交的变更。
    当隔离级别设置为Read committed时,避免了脏读,但是可能出现不可重复读、幻读。(丢失修改也会出现。)大多数数据库的默认隔离级别就是Read committed,比如Sql Server , Oracle。如何解决不可重复读这一问题,请看下一个隔离级别。
  3. 可重复读(RR)
    具有Repeatable read隔离级别的事务,可以确保多次从一个记录中读取相同的值,在这个事务持续期间,禁止其他事务对这条记录进行更新。隔离级别设置为Repeatable read时,可以避免脏读、不可重复读。(一般可以避免丢失修改,由数据库厂商决定。)但是可能出现幻读。MySQL的默认隔离级别就是Repeatable read。
  4. 串行化(S)
    具有Serializable隔离级别的事务,可以确保从一个表中读取相同的行数,在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作,所有并发问题都可以避免,但性能十分低。
    Serializable是最高的事务隔离级别,提供了最高程度的隔离性,同时代价也花费最高,性能很低,这个隔离级别通常会降低并发性,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻像读和丢失修改。如果事务隔离级别设置为SERIALIZABLE,具有SERIALIZABLE隔离级别的事务开始之后,不会看到数据库中其它会话事务作出的任何修改,直到提交SERIALIZABLE事务为止。

锁是用于多用户并发控制的。数据库有两种基本的锁模式,排它锁(Exclusive Locks,即X锁)和共享锁(Share Locks,即S锁)。当数据对象被加上排它锁时,其他会话的事务不能对它加共享锁读取和加排它锁修改。加了共享锁的数据对象可以被其他会话的事务加共享锁读取,但不能加排它锁修改。Oracle数据库利用这两种基本的锁模式来对数据库的事务进行并发控制。

其中DML锁(data locks,数据锁,Data Manipulation Language),用于保护并发情况下数据的并发一致性,DML锁主要包括TM锁(Table Manager 表级锁总称)和TX锁(Transaction Exclusive),其中TM锁为表级锁,TX锁称为事务锁或行级锁。

锁有显示锁和隐式锁之分,属于悲观控制模型

  • 隐式锁:由隔离级别决定DQL命令数据库自动加的锁,DML命令必加排它锁。
  • 而显式锁:在SQL命令中显式加的锁。例如:select … for update语句所加的TX锁。

锁的阻塞是指假如两个会话都要对一个字段的值做修改,第一个会话持有锁,第二个会话申请锁,如果出现锁互斥,等第一个会话提交解锁后它才能持有锁,然后才能进行修改。第二个会话在等待锁的期间,就是被阻塞状态。如果第一个会话持有锁很长时间不提交,可能会导致其它多个会话在申请该对象的锁时,都被阻塞或出现超时异常。阻塞是由于资源不足引起的排队等待现象。

死锁则是因为两个对象在拥有一份资源的情况下申请另一份资源,而另一份资源恰好又是这两对象持有的,导致两对象无法完成操作,且所持资源无法释放,引起死锁。假如说事物A现在需要修改row=1的数据,加了隐式的X锁,而事物B需要修改row=2的数据,也加了隐式的X锁。之后事物A需要修改row=2的数据,因为事物B的锁正占有row=2,所以此时出现锁等待,而当事物B修改row=1的数据时,这是便会产生死锁。

避免阻塞的手段

  1. 总的原则是锁的粒度尽可能小,尽可能减少共享数据;锁的模式尽可能弱,尽可能减少锁的互斥;互斥锁的持续时间尽可能短;
  2. 尽可能减少共享数据,提高会话的并发性。锁的粒度尽可能小(行锁—>表锁,加锁的范围逐渐加大),封锁的粒度越大,并发性就越小,同时系统的开销也就越小;相反,封锁的粒度越小,并发性就越高,系统开销也就越大。
  3. 尽可能减少锁的互斥,提高会话的并发性。锁的模式尽可能弱(不加锁—>加S锁—>加X锁,加锁的类型逐渐加强)
  4. 尽可能减少阻塞。这里有两种方案:1.减少锁的持续时间,事务内部访问某对象的时机。一般多事务要经常访问的表的引用放在事务的末尾,以便将控制锁的持续时间减至最短。2. 尽可能缩短事务(事务本身要短),以便将长期锁减至最少,改善并发性。
  5. 进行事务的分解,分解的原则是事务业务是最小原子操作。对于数据量很大的操作,在保证数据一致性/原子性的条件下,可以将其分成几组提交事务,这样可以避免长时间地占用资源。
  6. 将精心选定的索引添加到表中。这样查询会扫描更少的索引记录,并且因此也可以设置更少的锁定。
  7. 尽量按照主键/索引去查找记录,范围查找增加了锁冲突的可能性,也不要利用数据库做一些额外的计算工作。比如,用select…where…order by rand();这样的语句,由于类似这样的语句用不到索引,因此将导致整个表的数据都被锁住。
  8. 不要把无关的操作放到事务里面。
  9. 优化SQL和表设计,减少同时占用太多资源的情况。比如,减少连接的表,将复杂SQL分解为多个简单的SQL。
  10. 在并发比较高的系统中,不要加显式锁,特别是在事务里加显式锁。例如,select…for update语句。

避免死锁的手段

  1. 减少阻塞的措施均可以避免死锁
  2. 调整访问共享资源的SQL顺序,对于多个会话的事务内部要按相同的固定顺序访问共享资源(多个表对象,多条记录),避免出现死锁。

使用锁的原则:在满足完整性约束、业务需求,解决多事务并发冲突,保证数据正确性的前提下,尽可能减少阻塞和避免死锁,提高事务的并发性,保证程序的并发质量。

多版本

在商业数据库中,通过多版本,select不需要加锁,也不会读到脏数据,不存在读写依赖,写的排它锁不会阻塞读,加大了并发性。这属于乐观控制模型

多版本一般会在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。在版本号方法中,要更新的记录必须具有一个包含日期时间戳或版本号的列。当读取该记录时,日期时间戳或版本号将保存在客户端。然后,将对该值进行部分更新。

客户端

客户端的解决方法是通过记录原始行来解决的,这属于乐观控制模型。首先通过客户端读取数据行,然后where通过记录原始行进行定位。
处理并发的一种方法是仅当 WHERE 子句中的值与记录上的值匹配时才进行更新。该方法的 SQL 表示形式为:
UPDATE Table1 SET Column1 = @newvalue1, Column2 = @newvalue2 WHERE DateTimeStamp = @origDateTimeStamp
或者,可以使用版本号进行比较:
UPDATE Table1 SET Column1 = @newvalue1, Column2 = @newvalue2 WHERE RowVersion = @origRowVersionValue
如果日期时间戳或版本号匹配,则表明数据存储区中的记录未被更改,并且可以安全地使用新值对该记录进行更新。如果不匹配,则将返回错误。您可以编写代码,实现这种形式的并发检查。同时必须编写代码来响应任何更新冲突。为了确保日期时间戳或版本号的准确性,您需要在表上设置触发器,以便在发生对行的更改时,对日期时间戳或版本号进行更新。

发布了100 篇原创文章 · 获赞 142 · 访问量 17万+

猜你喜欢

转载自blog.csdn.net/coder_what/article/details/103637011