并发编程(四):也谈谈数据库的锁机制

http://www.2cto.com/database/201403/286730.html

1. 数据库并发的问题

数据库带来的并发问题包括:

  1. 丢失更新。

  2. 未确认的相关性(脏读)。

  3. 不一致的分析(非重复读)。

4. 幻像读。

详细描述如下:

1.1.丢失更新

当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,会发生丢失更新问题。每个事务都不知道其它事务的存在。最后的更新将重写由其它事务所做的更新,这将导致数据丢失。   

e.g.事务A和事务B同时修改某行的值,

事务A将数值改为1并提交事务B将数值改为2并提交。

这时数据的值为2,事务A所做的更新将会丢失。

看下面一段sql:

?


1

2

3

select old_attributes  from table where primary_key = ? ---step1

attributes = merge(old_attributes,new_attributes)       ----step2

update table set attributes_column = attributes where primary_key = ?   ----step3

如何解决呢?基本两种思路,一种是悲观锁,另外一种是乐观锁; 简单的说就是一种假定这样的问题是高概率的,最好一开始就锁住,免得更新老是失败;另外一种假定这样的问题是小概率的,最后一步做更新的时候再锁住,免得锁住时间太长影响其他人做有关操作。

1.1.1 悲观锁

  a)传统的悲观锁法(不推荐):

  以上面的例子来说明,在弹出修改工资的页面初始化时(这种情况下一般会去从数据库查询出来),在这个初始化查询中使用select ……for update nowait, 通过添加for update nowait语句,将这条记录锁住,避免其他用户更新,从而保证后续的更新是在正确的状态下更新的。然后在保持这个链接的状态下,在做更新提交。当然这个有个前提就是要保持链接,就是要对链接要占用较长时间,这个在现在web系统高并发高频率下显然是不现实的。

  b)现在的悲观锁法(推荐优先使用):

  在修改工资这个页面做提交时先查询下,当然这个查询必须也要加锁(select ……for update nowait),有人会说,在这里做个查询确认记录是否有改变不就行了吗,是的,是要做个确认,只是你不加for update就不能保证你在查询到更新提交这段时间里这条记录没有被其他会话更新过,所以这种方式也需要在查询时锁定记录,保证在这条记录没有变化的基础上再做更新,若有变化则提示告知用户。

1.1.2. 乐观锁

  a)旧值条件(前镜像)法:

  就是在sql更新时使用旧的状态值做条件,SQL大致如下 Update table set col1 = newcol1value, col2 = newcol2value…。 where col1 = oldcol1value and col2 = oldcol2value…。,在上面的例子中我们就可以把当前工资作为条件进行更新,如果这条记录已经被其他会话更新过,则本次更新了0行,这里我们应用系统一般会做个提示告知用户重新查询更新。这个取哪些旧值作为条件更新视具体系统实际情况而定。(这种方式有可能发生阻塞,如果应用其他地方使用悲观锁法长时间锁定了这条记录,则本次会话就需要等待,所以使用这种方式时最好统一使用乐观锁法。)

  b)使用版本列法(推荐优先使用):

  其实这种方式是一个特殊化的前镜像法,就是不需要使用多个旧值做条件,只需要在表上加一个版本列,这一列可以是NUMBER或 DATE/TIMESTAMP列,加这列的作用就是用来记录这条数据的版本(在表设计时一般我们都会给每个表增加一些NUMBER型和DATE型的冗余字段,以便扩展使用,这些冗余字段完全可以作为版本列用),在应用程序中我们每次操作对版本列做维护即可。在更新时我们把上次版本作为条件进行更新。在对一行进行更新的时候 限制条件=主键+版本号,同时对记录的版本号进行更新。

  伪代码如下:

?


1

2

3

4

5

start transaction;

select attributes, old_version from table where primary_key = ?

attribute Merge operations

update table set version = old_verison + 1 , attributes_column = attributes_value where primary_key = ? and version = old_version

commit;

事务提交以后,看最后一步更新操作的记录更新数是否为1,如果不是,则在业务上提示重试。(表明此时更新操作的并发度较高。)

在用户并发数比较少且冲突比较严重的应用系统中选择悲观锁b方法,其他情况首先乐观锁版本列法。

SQL Server中指定锁:

?


1

2

SELECT * FROM table WITH (HOLDLOCK) ----其他事务可以读取表,但不能更新删除

SELECT * FROM table WITH (TABLOCKX) -----其他事务不能读取表,更新和删除

不同的数据库锁的类型有差别,具体需要查询各自的api doc。

1.2.未确认的相关性(脏读 DirtyRead)

  当一个事务读取另一个事务尚未提交的修改时,产生脏读。e.g.

1.Mary的原工资为1000, 财务人员将Mary的工资改为了8000(但未提交事务) 2.Mary读取自己的工资 ,发现自己的工资变为了8000,欢天喜地!

3.而财务发现操作有误,回滚了事务,Mary的工资又变为了1000

像这样,Mary记取的工资数8000是一个脏数据。

解决办法:如果在第一个事务提交前,任何其他事务不可读取其修改过的值,则可以避免该问题。

1.3.不一致的分析(不可重复读 non-repeatable read)

 同一查询在同一事务中多次进行,由于其他提交事务所做的修改或删除,每次返回不同的结果集,此时发生非重复读。e.g.

在事务1中,Mary 读取了自己的工资为1000,操作并没有完成 在事务2中,这时财务人员修改了Mary的工资为2000,并提交了事务.在事务1中,Mary 再次读取自己的工资时,工资变为了2000

解决办法:如果只有在修改事务完全提交之后才可以读取数据,则可以避免该问题。

1.4.幻像读 phantom read    

 同一查询在同一事务中多次进行,由于其他提交事务所做的插入操作,每次返回不同的结果集,此时发生幻像读。当对某行执行插入或删除操作,而该行属于某个事务正在读取的行的范围时,会发生幻像读问题。事务第一次读的行范围显示出其中一行已不复存在于第二次读或后续读中,因为该行已被其它事务删除。同样,由于其它事务的插入操作,事务的第二次或后续读显示有一行已不存在于原始读中。

  e.g.目前工资为1000的员工有10人。

事务1,读取所有工资为1000的员工。 这时事务2向employee表插入了一条员工记录,工资也为1000。 事务1再次读取所有工资为1000的员工 共读取到了11条记录。
解决办法:如果在操作事务完成数据处理之前,任何其他事务都不可以添加新数据,则可避免该问题

讨论加锁机制,还不要了解一下数据库的隔离机制。

2. 数据库隔离机制

谈到数据库隔离机制,就不得不先说事务transaction。数据库事务有严格的定义,它必须同时满足4个特性:原子性(Atomic)、一致性(Consistency)、隔离性(Isolation)和持久性(Durabiliy),简称为ACID。

原子性:保证事务中的所有操作全部执行或全部不执行。例如执行转账事务,要么转账成功,要么失败。成功,则金额从转出帐户转入到目的帐户,并且两个帐户金额将发生相应的变化;失败,则两个账户的金额都不变。不会出现转出帐户扣了钱,而目的帐户没有收到钱的情况。
一致性:保证数据库始终保持数据的一致性——事务操作之前是一致的,事务操作之后也是一致的,不管事务成功与否。如上面的例子,转账之前和之后数据库都保持数据上的一致性。
隔 离性:多个事务并发执行的话,结果应该与多个事务串行执行效果是一样的。在并发数据操作时,不同的事务拥有各自的数据空间,其操作不会对对方产生干扰。隔离允许事务行为独立或隔离于其他并发运行的事务。通过控制隔离,每个事务在其行动时间里都像是修改数据库的惟一事务。一个事务与其他事务隔离的程度称为隔离级别。数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
持久性:持久性表示事物操作完成之后,对数据库的影响是持久的,即使数据库因故障而受到破坏,数据库也应该能够恢复。通常的实现方式是采用日志。
ANSI/ISO SQL92标准定义了一些数据库操作的隔离级别。每种隔离级别指定当前事务执行时所不允许的交互作用类型,即事务间是否相互隔离,或它们是否可以读取或更新被另一事务所使用的信息。较高隔离级别包括由较低级别所施加的限制。

定义的4种隔离级别:

Read Uncommited

可以读取未提交记录。此隔离级别,不会使用,忽略。
Read Committed (RC)

快照读忽略,本文不考虑。

针对当前读,RC隔离级别保证对读取到的记录加锁 (记录锁),存在幻读现象。
Repeatable Read (RR)

快照读忽略,本文不考虑。

针对当前读,RR隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入 (间隙锁)
Serializable

从MVCC并发控制退化为基于锁的并发控制。不区别快照读与当前读,所有的读操作均为当前读,读加读锁 (S锁),写加写锁 (X锁)。

Serializable隔离级别下,读写冲突,因此并发度急剧下降,因此不建议使用。

不同的隔离等级对应的将会导致的数据库并发的问题总结如下:因此,对于不同的隔离等级,需要在事务中主动加锁,以避免这些并发的问题。

3. 数据库的锁机制

各种大型数据库所采用的锁的基本理论是一致的,但在具体实现上各有差别。

SQL Server更强调由系统来管理锁。在用户有SQL请求时,系统分析请求,自动在满足锁定条件和系统性能之间为数据库加上适当的锁,同时系统在运行期间常常自动进行优化处理,实行动态加锁。

SQLite采用粗放型的锁。当一个连接要写数据库,所有其它的连接被锁住,直到写连接结束了它的事务。SQLite有一个加锁表,来帮助不同的写数据库都能够在最后一刻再加锁,以保证最大的并发性。

MySQL数据库由于其自身架构的特点,存在多种数据存储引擎,每种存储引擎所针对的应用场景特点都不太一样,为了满足各自特定应用场景的需求,每种存储引擎的锁定机制都是为各自所面对的特定场景而优化设计,所以各存储引擎的锁定机制也有较大区别。

对于一般的用户而言,通过系统的自动锁定管理机制基本可以满足使用要求。 但是涉及到写操作,还是一定要理解隔离机制和并发可能带来的问题,在事务中或者SQL中加入锁机制。对于数据库的死锁,一般数据库系统都会有一套机制去解锁,一般不会造成数据库的瘫痪,但解锁的过程会造成数据库性能的急速下降,反映到程序上就会造成程序的反应性能的下降,并且会造成程序有的操作失败。

在实际开发中,要充分考虑所有可能的并发可能,既不能加作用的锁,又要保证数据处理的正确性。因此,深刻理解锁有非常重要的现实意义。

3.1 快照读VS当前读

多版本的并发控制协议——MVCC (Multi-Version Concurrency Control) 最大的好处,相信也是耳熟能详:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,这也是为什么现阶段几乎所有的RDBMS都支持了MVCC。

与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control。

在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。

在一个支持MVCC并发控制的系统中,哪些读操作是快照读?哪些操作又是当前读呢?以MySQL InnoDB为例:
快照读:简单的select操作,属于快照读,不加锁。(当然,也有例外,下面会分析)
当前读:
特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。
所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取 记录加锁。其中,除了第一条语句,对读取记录加S锁 (共享锁)外,其他的操作,都加的是X锁 (排它锁)。注:这个语句的加锁是数据库完成的。

3.2 当前读的加锁

为什么将 插入/更新/删除 操作,都归为当前读?可以看看下面这个 更新 操作,在数据库中的执行流程:

 从图中,可以看到,一个Update操作的具体流程。当Update SQL被发给MySQL后,MySQL Server会根据where条件,读取第一条满足条件的记录,然后InnoDB引擎会将第一条记录返回,并加锁 (current read)。待MySQL Server收到这条加锁的记录之后,会再发起一个Update请求,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此,Update操作内部,就包含了一个当前读。同理,Delete操作也一样。Insert操作会稍微有些不同,简单来说,就是Insert操作可能会触发Unique Key的冲突检查,也会进行一个当前读。

:根据上图的交互,针对一条当前读的SQL语句,InnoDB与MySQL Server的交互,是一条一条进行的,因此,加锁也是一条一条进行的。先对一条满足条件的记录加锁,返回给MySQL Server,做一些DML操作;然后在读取下一条加锁,直至读取完毕。

传统RDBMS加锁的一个原则,就是2PL (二阶段锁):Two-Phase Locking。相对而言,2PL比较容易理解,说的是锁操作分为两个阶段:加锁阶段与解锁阶段,并且保证加锁阶段与解锁阶段不相交。下面,仍旧以MySQL为例,来简单看看2PL在MySQL中的实现。

从上图可以看出,2PL就是将加锁/解锁分为两个完全不相交的阶段。加锁阶段:只加锁,不放锁。解锁阶段:只放锁,不加锁。

时间: 2024-08-09 05:40:27

并发编程(四):也谈谈数据库的锁机制的相关文章

数据库并发事务控制四:postgresql数据库的锁机制二:表锁

在博文<数据库并发事务控制四:postgresql数据库的锁机制 > http://blog.csdn.net/beiigang/article/details/43302947 中后面提到: 常规锁机制可以参考pg的官方手册,章节和内容见下面 13.3. Explicit Locking http://www.postgresql.org/docs/9.4/static/explicit-locking.html 这节分为:表锁.行锁.页锁.死锁.Advisory锁(这个名字怎么翻译好???

数据库并发事务控制四:postgresql数据库的锁机制

并发控制是DBMS的关键技术,并发控制技术也称为同步机制,其实现通常依赖于底层的并发控制机制.操作系统提供了多种同步对象,如事件 Event.互斥锁 Mutex和条件变量 Cond.信号量Semaphore.读写锁 RWLock.自旋锁 Spinlock等.数据库管理系统自己实现封锁主要是考虑: 锁语义加强:OS只提供排它锁.为了提高并发度,数据库至少需要共享锁和排它锁,即读锁和写锁: 锁的功能增强:数据库提供视图监测封锁情况和进行死锁检测: 可靠性的考虑:例如,在某个持锁进程任意一点回滚时,数

Java并发编程系列(三)-locks锁机制类

Java中的锁可以分为"同步锁"和JUC包里面的锁(locks包) 同步锁:即通过synchronized关键字来进行同步,实现对竞争资源的互斥访问的锁.Java 1.0版本中就已经支持同步锁了.同步锁的原理是,对于每一个对象,有且仅有一个同步锁:不同的线程能共同访问该同步锁.但是,在同一个时间点,该同步锁能且只能被一个线程获取到.这样,获取到同步锁的线程就能进行CPU调度,从而在CPU上执行:而没有获取到同步锁的线程,必须进行等待,直到获取到同步锁之后才能继续运行. locks包:相

Python并发编程03/僵尸孤儿进程,互斥锁,进程之间的通信

目录 Python并发编程03/僵尸孤儿进程,互斥锁,进程之间的通信 1.昨日回顾 2.僵尸进程和孤儿进程 2.1僵尸进程 2.2孤儿进程 2.3僵尸进程如何解决? 3.互斥锁,锁 3.1互斥锁的应用 3.2Lock与join的区别 4.进程之间的通信 进程在内存级别是隔离的 4.1基于文件通信 (抢票系统) 4.2基于队列通信 Python并发编程03/僵尸孤儿进程,互斥锁,进程之间的通信 1.昨日回顾 1.创建进程的两种方式: 函数, 类. 2.pid: os.getpid() os.get

并发事务的丢失更新及数据锁机制

在事务的隔离级别内容中,能够了解到两个不同的事务在并发的时候可能会发生数据的影响.细心的话可以发现事务隔离级别章节中,脏读.不可重复读.幻读三个问题都是由事务A对数据进行修改.增加,事务B总是在做读操作.如果两事务都在对数据进行修改则会导致另外的问题:丢失更新.这是本博文所要叙述的主题,同时引出并发事务对数据修改的解决方案:锁机制. 1.丢失更新的定义及产生原因. 丢失更新就是两个不同的事务(或者Java程序线程)在某一时刻对同一数据进行读取后,先后进行修改.导致第一次操作数据丢失.可以用图表表

Sql Server之旅——第十四站 深入的探讨锁机制

原文:Sql Server之旅--第十四站 深入的探讨锁机制 上一篇我只是做了一个堆表让大家初步的认识到锁的痉挛状态,但是在现实世界上并没有这么简单的事情,起码我的表不会没有索引对吧,,,还 有就是我的表一定会有很多的连接过来,10:1的读写,很多码农可能都会遇到类似神乎其神的死锁,卡住,读不出来,插不进入等等神仙的事情导致性 能低下,这篇我们一起来探讨下. 一: 当select遇到性能低下的update会怎么样? 1. 还是使用原始的person表,插入6条数据,由于是4000字节,所以两条数

python并发编程之多进程(二):互斥锁(同步锁)&amp;进程其他属性&amp;进程间通信(queue)&amp;生产者消费者模型

一,互斥锁,同步锁 进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的, 竞争带来的结果就是错乱,如何控制,就是加锁处理 part1:多个进程共享同一打印终端 #并发运行,效率高,但竞争同一打印终端,带来了打印错乱 from multiprocessing import Process import os,time def work(): print('%s is running' %os.getpid()) time.sleep(2) print('

[数据库事务与锁]详解四: 数据库的锁机制

注明: 本文转载自http://www.hollischuang.com/archives/898 数据库的读现象浅析中介绍过,在并发访问情况下,可能会出现脏读.不可重复读和幻读等读现象,为了应对这些问题,主流数据库都提供了锁机制,并引入了事务隔离级别的概念. 并发控制 在计算机科学,特别是程序设计.操作系统.多处理机和数据库等领域,并发控制(Concurrency control)是确保及时纠正由并发操作导致的错误的一种机制. 数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存

Java 并发编程(四):如何保证对象的线程安全性

本篇来谈谈 Java 并发编程:如何保证对象的线程安全性. 01.前言 先让我吐一句肺腑之言吧,不说出来会憋出内伤的.<Java 并发编程实战>这本书太特么枯燥了,尽管它被奉为并发编程当中的经典之作,但我还是忍不住.因为第四章"对象的组合"我整整啃了两周的时间,才啃出来点肉丝. 读者朋友们见谅啊.要怪只能怪我自己的学习能力有限,真读不了这种生硬无趣的技术书.但是为了学习,为了进步,为了将来(口号喊得有点大了),只能硬着头皮上. 请随我来,我尽量写得有趣点. 02.线程安全类