数据库-事务和锁

事务

所谓事务是用户定义的一个数据库操做系列,这些操做要么所有执行,要么所有不执行,是一个不可分割的工做单位。例如在关系数据库中,一个事务能够是一条sql语句、一组sql语句或整个程序。html

给个栗子:sql

小IT在网上购物,其付款过程至少包括如下几步数据库操做:数据库

  1. 更新客户所购商品的库存信息;
  2. 生成订单而且保存到数据库;
  3. 更新用户相关信息,例如购物数量等;

正常状况下,操做顺利进行,最终交易成功,那么与交易相关的全部数据库信息也成功更新。可是,若是在这一系列过程当中任何一个环节出了差错,例如在更新商品库存信息时发生异常、该顾客银行账户存款不足等,都将致使交易失败。一旦交易失败,数据库中全部信息都必须保持交易前的状态不变,好比最后一步更新用户信息时失败而致使交易失败,那么必须保证这笔失败的交易不影响数据库的状态--库存信息没有被更新、用户也没有付款,订单也没有生成。不然,数据库的信息将会一片混乱而不可预测。服务器

数据库事务正是用来保证这种状况下交易的平稳性和可预测性的技术。数据结构

事务的ACID特性

A(Atomicity)原子性

事务必须是原子工做单元;对于其数据修改,要么全都执行,要么全都不执行。一般,与某个事务关联的操做具备共同的目标,而且是相互依赖的。若是系统只执行这些操做的一个子集,则可能会破坏事务的整体目标。原子性消除了系统处理操做子集的可能性。并发

C(Consistency)一致性

事务在完成时,必须使全部的数据都保持一致状态。在相关数据库中,全部规则都必须应用于事务的修改,以保持全部数据的完整性。事务结束时,全部的内部数据结构(如 B 树索引或双向链表)都必须是正确的。某些维护一致性的责任由应用程序开发人员承担,他们必须确保应用程序已强制全部已知的完整性约束。例如,当开发用于转账的应用程序时,应避免在转账过程当中任意移动小数点。post

I(Isolation)隔离性

指的是在并发环境中,当不一样的事务同时操纵相同的数据时,每一个事务都有各自的完整数据空间。由并发事务所作的修改必须与任何其余并发事务所作的修改隔离。事务查看数据更新时,数据所处的状态要么是另外一事务修改它以前的状态,要么是另外一事务修改它以后的状态,事务不会查看到中间状态的数据性能

D(Durability)持久性

指的是只要事务成功结束,它对数据库所作的更新就必须永久保存下来。即便发生系统崩溃,从新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。测试

事务的ACID特性是由关系数据库管理系统(RDBMS,数据库系统)来实现的。数据库管理系统采用日志来保证事务的原子性、一致性和持久性。日志记录了事务对数据库所作的更新,若是某个事务在执行过程当中发生错误,就能够根据日志,撤销事务对数据库已作的更新,使数据库退回到执行事务前的初始状态。数据库管理系统采用锁机制来实现事务的隔离性。当多个事务同时更新数据库中相同的数据时,只容许持有锁的事务更新数据,其余事务必须等待,直到前一个事务释放了锁,其余事务才有机会更新该数据。优化

完整的事务结构

BEGIN a transaction;//设置事务的起始点

COMMIT a transaction;//提交事务,使事务提交的数据成为持久不可更改的部分

ROLLBACK a transaction;//撤销一个事务,回滚,使之成为事务开始前的状态

SAVE a transaction;//创建标签,用做部分回滚,使之恢复到标签初的状态

事务的语法

BEGIN TRAN[SACTION] [<transaction_name>|<@transaction variable>][WITH MARK['<description>']][;]

COMMIT [TRAN[SACTION][<transaction_name>|<@transaction varible>]][;]

ROLLBACK TRAN[SACRION][<transaction name>|<save point name>|<@transaction varible>|<@save point varible>][;]

SAVE TRAN[SACTION][<save point name>|<@svae point varible>][;]

"[]"里面是需补充的部分。

给个栗子:

下面整个示例,先来建一张表(使用SqlServer)以下:

  

BEGIN TRAN Tran_Money    --开始事务

DECLARE @tran_error int;
SET @tran_error = 0;
    BEGIN TRY 
        UPDATE tb_Money SET MyMoney = MyMoney - 30 WHERE Name = '刘备';
        SET @tran_error = @tran_error + @@ERROR;
        --测试出错代码,看看刘备的钱减小,关羽的钱是否会增长
        --SET @tran_error = 1;
        UPDATE tb_Money SET MyMoney = MyMoney + 30 WHERE Name = '关羽';
        SET @tran_error = @tran_error + @@ERROR;
    END TRY

BEGIN CATCH
    PRINT '出现异常,错误编号:' + convert(varchar,error_number()) + ',错误消息:' + error_message()
    SET @tran_error = @tran_error + 1
END CATCH

IF(@tran_error > 0)
    BEGIN
        --执行出错,回滚事务
        ROLLBACK TRAN;
        PRINT '转帐失败,取消交易!';
    END
ELSE
    BEGIN
        --没有异常,提交事务
        COMMIT TRAN;
        PRINT '转帐成功!';
    END

本栗子来源于SQL Server 事务语法

数据库和操做系统同样,是一个多用户使用的共享资源。当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的状况。若对并发操做不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。加锁是实现数据库并 发控制的一个很是重要的技术。在实际应用中常常会遇到的与锁相关的异常状况,当两个事务须要一组有冲突的锁,而不能将事务继续下去的话,就会出现死锁,严重影响应用的正常执行。 
在数据库中有两种基本的锁类型:排它锁(Exclusive Locks,即X锁)和共享锁(Share Locks,即S锁)。当数据对象被加上排它锁时,其余的事务不能对它读取和修改。加了共享锁的数据对象能够被其余事务读取,但不能修改。数据库利用这两 种基本的锁类型来对数据库的事务进行并发控制。 

死锁的几种状况

死锁的第一种状况 
一个用户A 访问表A(锁住了表A),而后又访问表B;另外一个用户B 访问表B(锁住了表B),而后企图访问表A;这时用户A因为用户B已经锁住表B,它必须等待用户B释放表B才能继续,一样用户B要等用户A释放表A才能继续,这就死锁就产生了。 

解决方法: 
这种死锁比较常见,是因为程序的BUG产生的,除了调整的程序的逻辑没有其它的办法。仔细分析程序的逻辑,对于数据库的多表操做时,尽可能按照相同的顺序进 行处理,尽可能避免同时锁定两个资源,如操做A和B两张表时,老是按先A后B的顺序处理, 必须同时锁定两个资源时,要保证在任什么时候刻都应该按照相同的顺序来锁定资源。 

死锁的第二种状况 
用户A查询一条纪录,而后修改该条纪录;这时用户B修改该条纪录,这时用户A的事务里锁的性质由查询的共享锁企图上升到独占锁,而用户B里的独占锁因为A 有共享锁存在因此必须等A释放掉共享锁,而A因为B的独占锁而没法上升的独占锁也就不可能释放共享锁,因而出现了死锁。这种死锁比较隐蔽,但在稍大点的项目中常常发生。如在某项目中,页面上的按钮点击后,没有使按钮马上失效,使得用户会屡次快速点击同一按钮,这样同一段代码对数据库同一条记录进行屡次操做,很容易就出现这种死锁的状况。 

解决方法: 
一、对于按钮等控件,点击后使其马上失效,不让用户重复点击,避免对同时对同一条记录操做。 
二、使用乐观锁进行控制。乐观锁大可能是基于数据版本(Version)记录机制实现。即为数据增长一个版本标识,在基于数据库表的版本解决方案中,通常是 经过为数据库表增长一个“version”字段来实现。读取出数据时,将此版本号一同读出,以后更新时,对此版本号加一。此时,将提交数据的版本数据与数 据库表对应记录的当前版本信息进行比对,若是提交的数据版本号大于数据库表当前版本号,则予以更新,不然认为是过时数据。乐观锁机制避免了长事务中的数据库加锁开销(用户A和用户B操做过程当中,都没有对数据库数据加锁),大大提高了大并发量下的系统总体性能表现。Hibernate 在其数据访问引擎中内置了乐观锁实现。须要注意的是,因为乐观锁机制是在咱们的系统中实现,来自外部系统的用户更新操做不受咱们系统的控制,所以可能会造 成脏数据被更新到数据库中。 
三、使用悲观锁进行控制。悲观锁大多数状况下依靠数据库的锁机制实现,如Oracle的Select … for update语句,以保证操做最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销每每没法承受。如一个金融系统, 当某个操做员读取用户的数据,并在读出的用户数据的基础上进行修改时(如更改用户帐户余额),若是采用悲观锁机制,也就意味着整个操做过程当中(从操做员读 出数据、开始修改直至提交修改结果的全过程,甚至还包括操做员中途去煮咖啡的时间),数据库记录始终处于加锁状态,能够想见,若是面对成百上千个并发,这样的状况将致使灾难性的后果。因此,采用悲观锁进行控制时必定要考虑清楚。 

死锁的第三种状况 
若是在事务中执行了一条不知足条件的update语句,则执行全表扫描,把行级锁上升为表级锁,多个这样的事务执行后,就很容易产生死锁和阻塞。相似的情 况还有当表中的数据量很是庞大而索引建的过少或不合适的时候,使得常常发生全表扫描,最终应用系统会愈来愈慢,最终发生阻塞或死锁。 
解决方法: 
SQL语句中不要使用太复杂的关联多表的查询;使用“执行计划”对SQL语句进行分析,对于有全表扫描的SQL语句,创建相应的索引进行优化。 

整体上来讲,产生内存溢出与锁表都是因为代码写的很差形成的,所以提升代码的质量是最根本的解决办法。有的人认为先把功能实现,有BUG时再在测试阶段进 行修正,这种想法是错误的。正如一件产品的质量是在生产制造的过程当中决定的,而不是质量检测时决定的,软件的质量在设计与编码阶段就已经决定了,测试只是对软件质量的一个验证,由于测试不可能找出软件中全部的BUG。

如何避免死锁

1 使用事务时,尽可能缩短事务的逻辑处理过程,及早提交或回滚事务; 
2 设置死锁超时参数为合理范围,如:3分钟-10分种;超过期间,自动放弃本次操做,避免进程悬挂; 
3 全部的SP都要有错误处理(经过@error) 
4 通常不要修改SQL SERVER事务的默认级别。不推荐强行加锁 
5 优化程序,检查并避免死锁现象出现; 
1)合理安排表访问顺序 
2)在事务中尽可能避免用户干预,尽可能使一个事务处理的任务少些。 
3)采用脏读技术。脏读因为不对被访问的表加锁,而避免了锁冲突。在客户机/服务器应用环境中,有些事务每每不容许读脏数据,但在特定的条件下,咱们能够用脏读。 
4)数据访问时域离散法。数据访问时域离散法是指在客户机/服务器结构中,采起各类控制手段控制对数据库或数据库中的对象访问时间段。主要经过如下方式实 现: 合理安排后台事务的执行时间,采用工做流对后台事务进行统一管理。工做流在管理任务时,一方面限制同一类任务的线程数(每每限制为1个),防止资源过多占 用; 另外一方面合理安排不一样任务执行时序、时间,尽可能避免多个后台任务同时执行,另外,避免在前台交易高峰时间运行后台任务 
5)数据存储空间离散法。数据存储空间离散法是指采起各类手段,将逻辑上在一个表中的数据分散到若干离散的空间上去,以便改善对表的访问性能。主要经过如下方法实现: 第一,将大表按行或列分解为若干小表; 第二,按不一样的用户群分解。 
6)使用尽量低的隔离性级别。隔离性级别是指为保证数据库数据的完整性和一致性而使多用户事务隔离的程度,SQL92定义了4种隔离性级别:未提交读、 提交读、可重复读和可串行。若是选择太高的隔离性级别,如可串行,虽然系统能够因实现更好隔离性而更大程度上保证数据的完整性和一致性,但各事务间冲突而死锁的机会大大增长,大大影响了系统性能。 
7)使用Bound Connections。Bound connections 容许两个或多个事务链接共享事务和锁,并且任何一个事务链接要申请锁如同另一个事务要申请锁同样,所以能够容许这些事务共享数据而不会有加锁的冲突。 
8)考虑使用乐观锁定或使事务首先得到一个独占锁定。  

冲突问题

一、脏读

某个事务读取的数据是另外一个事务正在处理的数据。而另外一个事务可能会回滚,形成第一个事务读取的数据是错误的。

二、不可重复读

在一个事务里两次读入数据,但另外一个事务已经更改了第一个事务涉及到的数据,形成第一个事务读入旧数据。

三、幻读

幻读是指当事务不是独立执行时发生的一种现象。例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的所有数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,之后就会发生操做第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉同样。

四、更新丢失

多个事务同时读取某一数据,一个事务成功处理好了数据,被另外一个事务写回原值,形成第一个事务更新丢失。

锁模式

一、共享锁

共享锁(S 锁)容许并发事务在封闭式并发控制下读取 (SELECT)资源。有关详细信息,请参阅并发控制的类型(悲观锁和乐观锁)。资源上存在共享锁(S锁)时,任何其余事务都不能修改数据。读取操做一完成,就当即释放资源上的共享锁(S锁),除非将事务隔离级别设置为可重复读或更高级别,或者在事务持续时间内用锁定提示保留共享锁(S锁)。

二、更新锁(U锁)

更新锁在共享锁和排他锁的结合。更新锁意味着在作一个更新时,一个共享锁在扫描完成符合条件的数据后可能会转化成排他锁。

这里面有两个步骤:

1) 扫描获取Where条件时。这部分是一个更新查询,此时是一个更新锁。

2) 若是将执行写入更新。此时该锁升级到排他锁。不然,该锁转变成共享锁。

更新锁能够防止常见的死锁。

三、排他锁

排他锁(X 锁)能够防止并发事务对资源进行访问。排他锁不与其余任何锁兼容。使用排他锁(X锁)时,任何其余事务都没法修改数据;仅在使用 NOLOCK提示或未提交读隔离级别时才会进行读取操做。

悲观锁

悲观锁是指假设并发更新冲突会发生,因此无论冲突是否真的发生,都会使用锁机制。
悲观锁会完成如下功能:锁住读取的记录,防止其它事务读取和更新这些记录。其它事务会一直阻塞,直到这个事务结束.
悲观锁是在使用了数据库的事务隔离功能的基础上,独享占用的资源,以此保证读取数据一致性,避免修改丢失。

悲观锁可使用Repeatable Read事务,它彻底知足悲观锁的要求。


乐观锁

乐观锁不会锁住任何东西,也就是说,它不依赖数据库的事务机制,乐观锁彻底是应用系统层面的东西。

若是使用乐观锁,那么数据库就必须加版本字段,不然就只能比较全部字段,但由于浮点类型不能比较,因此实际上没有版本字段是不可行的。

事务隔离级别 

数据库事务的隔离级别有4个,由低到高依次为Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别能够逐个解决脏读、不可重复读、幻读这几类问题。

READ UNCOMMITTED-读未提交

Read UnCommitted事务能够读取事务已修改,但未提交的的记录。

Read UnCommitted事务会产生脏读(Dirty Read)。

Read UnCommitted事务与select语句加nolock的效果同样,它是全部隔离级别中限制最少的。

本栗子来源于数据库事务隔离级别

公司发工资了,领导把5000元打到singo的帐号上,可是该事务并未提交,而singo正好去查看帐户,发现工资已经到帐,是5000元整,很是高兴。但是不幸的是,领导发现发给singo的工资金额不对,是2000元,因而迅速回滚了事务,修改金额后,将事务提交,最后singo实际的工资只有2000元,singo空欢喜一场。


 

出现上述状况,即咱们所说的脏读,两个并发的事务,“事务A:领导给singo发工资”、“事务B:singo查询工资帐户”,事务B读取了事务A还没有提交的数据。

当隔离级别设置为Read uncommitted时,就可能出现脏读,如何避免脏读,请看下一个隔离级别。

READ COMMITTED-读提交

一旦建立共享锁的语句执行完成,该锁顶便释放。

Read Committed是SQL Server的预设隔离等级。

Read Committed只能够防止脏读。

--先建立表: 
CREATE TABLE tb(id int,val int) 
INSERT tb VALUES(1,10) 
INSERT tb VALUES(2,20) 
  
而后在链接1中,执行: 
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
BEGIN TRANSACTION
    SELECT * FROM tb;  --这个SELECT结束后,就会释放掉共享锁 
      
    WAITFOR DELAY '00:00:05'  --模拟事务处理,等待5秒 
      
    SELECT * FROM tb;   --再次SELECT tb表 
ROLLBACK  --回滚事务 
  
在链接2中,执行 
UPDATE tb SET
    val = val + 10 
WHERE id = 2; 
  
-------- 
回到链接1中.能够看到.两次SELECT的结果是不一样的. 
由于在默认的READ COMMITTED隔离级别下,SELECT完了.就会立刻释放掉共享锁. 

singo拿着工资卡去消费,系统读取到卡里确实有2000元,而此时她的老婆也正好在网上转帐,把singo工资卡的2000元转到另外一帐户,并在 singo以前提交了事务,当singo扣款时,系统检查到singo的工资卡已经没有钱,扣款失败,singo十分纳闷,明明卡里有钱,为什么......

出现上述状况,即咱们所说的不可重复读 ,两个并发的事务,“事务A:singo消费”、“事务B:singo的老婆网上转帐”,事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。

当隔离级别设置为Read committed 时,避免了脏读,可是可能会形成不可重复读。

大多数数据库的默认级别就是Read committed,好比Sql Server , Oracle。如何解决不可重复读这一问题,请看下一个隔离级别。

REPEATABLE READ-重复读

REPEATABLE READ事务不会产生脏读,而且在事务完成以前,任何其它事务都不能修改目前事务已读取的记录。

其它事务仍能够插入新记录,但必须符合当前事务的搜索条件——这意味着当前事务从新查询记录时,会产生幻读(Phantom Read)。

当隔离级别设置为Repeatable read 时,能够避免不可重复读。当singo拿着工资卡去消费时,一旦系统开始读取工资卡信息(即事务开始),singo的老婆就不可能对该记录进行修改,也就是singo的老婆不能在此时转帐。

虽然Repeatable read避免了不可重复读,但还有可能出现幻读 。

singo的老婆工做在银行部门,她时常经过银行内部系统查看singo的信用卡消费记录。有一天,她正在查询到singo当月信用卡的总消费金额 (select sum(amount) from transaction where month = 本月)为80元,而singo此时正好在外面胡吃海塞后在收银台买单,消费1000元,即新增了一条1000元的消费记录(insert transaction ... ),并提交了事务,随后singo的老婆将singo当月信用卡消费的明细打印到A4纸上,却发现消费总额为1080元,singo的老婆很诧异,觉得出 现了幻觉,幻读就这样产生了。

注:Mysql的默认隔离级别就是Repeatable read。

SERIALIZABLE-序列化

SERIALIZABLE能够防止除更新丢失外全部的一致性问题,即:

1.语句没法读取其它事务已修改但未提交的记录。

2.在当前事务完成以前,其它事务不能修改目前事务已读取的记录。

3.在当前事务完成以前,其它事务所插入的新记录,其索引键值不能在当前事务的任何语句所读取的索引键范围中。

SNAPSHOT

Snapshot事务中任何语句所读取的记录,都是事务启动时的数据。

这至关于事务启动时,数据库为事务生成了一份专用“快照”。在当前事务中看到不其它事务在当前事务启动以后所进行的数据修改。

Snapshot事务不会读取记录时要求锁定,读取记录的Snapshot事务不会锁住其它事务写入记录,写入记录的事务也不会锁住Snapshot事务读取数据。