事务是逻辑上的一组操作,要么都执行,要么都不执行。
事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。
事务的四大特性:
(1)原子性(Atomicity): 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
(2)一致性(Consistency): 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
(3)隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
(4)持久性(Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。这就会导致许多问题:
(1)脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
(2)丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
(3)不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
(4)幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
SQL 标准定义了四个隔离级别:
MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。
这里需要注意的是:与 SQL 标准不同的地方在于 InnoDB 存储引擎在 REPEATABLE-READ(可重读) 事务隔离级别下使用的是Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server) 是不同的。所以说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要求,即达到了 SQL标准的 SERIALIZABLE(可串行化) 隔离级别。因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 READ-COMMITTED(读取提交内容) ,但是你要知道的是InnoDB 存储引擎默认使用 REPEAaTABLE-READ(可重读) 并不会有任何性能损失。
InnoDB 存储引擎在 分布式事务 的情况下一般会用到 SERIALIZABLE(可串行化) 隔离级别。
数据库中操作一个事务是怎么完成的呢?
如果提交事务前后服务器宕机会产生什么影响?
如事务在上面的第9步完成前,服务器停电了!此时客户端不会返回成功状态的,也就是失败了。重启电脑后,因为磁盘日志文件中没记录这个事务的提交,所有这些更改都不见了,与客户端预想的一样,失败了。
如果到了第9步完成(9、10、11应该算同时完成),停电了!内存中修改的数据还没写入磁盘,全丢了!但是不要紧,日志已经写入了。重启电脑及数据库服务后,数据库不会立即可用。数据库会先扫描一遍日志,发现这个事物提交了,但数据还没写入。于是重新把这个事务中的SQL操作在内存中“执行”一遍,这样数据出来了,随时可以写入磁盘中的数据文件。等待所有的这种情况都处理后,数据库就可以正常使用了!
当数据库有并发事务的时候,可能会产生数据的不一致,这时候需要一些机制来保证访问的次序,锁机制就是这样的一个机制。
(1)在Read Uncommitted级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突;
(2)在Read Committed级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁;
(3)在Repeatable Read级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁。
(4)SERIALIZABLE 是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成。
**锁的粒度就是要锁住的范围是多大,也就是锁的级别。**在关系型数据库中,可以按照锁的粒度把数据库锁分为行级锁(INNODB引擎)、表级锁(MYISAM引擎)和页级锁(BDB引擎 )。
MyISAM和InnoDB存储引擎使用的锁:
(1)MyISAM采用表级锁(table-level locking)。
(2)InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁
行级锁,表级锁和页级锁对比:
(1)行级锁:行级锁是Mysql中锁定粒度最小的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。
特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
(3)页级锁 页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。
特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般
(2)表级锁 表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。
特点:开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。
锁的粒度取决于具体的存储引擎,InnoDB实现了行级锁,页级锁,表级锁。他们的加锁开销从大到小,并发能力也是从大到小。
从锁的类别上来讲,有共享锁和排他锁。
(1)共享锁: 又叫做读锁。当用户要进行数据的读取时,对数据加上共享锁。多个事务可以查看但无法修改和删除数据。共享锁可以同时加上多个,也就是说,当数据库已经被别人增加了读锁的时候,其他新来的事务也可以读数据,但是不能写。
select *** lock in share model
共享锁可以通过 lock in share model
来实现,对指定的数据行加锁
(2)排他锁: 又叫做写锁。当用户要进行数据的写入时,对数据加上排他锁。既能读数据,又能修改数据。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。也就是说,如果数据库已经被别人增加了排他写锁,那么后面的事务是无法再获得该数据库的任何锁的。
select *** for update
排他锁可以通过 for update
对指定数据行进行加锁
InnoDB是基于索引来完成行锁
select * from tab_with_index where id = 1 for update;
for update 可以根据条件来完成行锁锁定,并且 id 是有索引键的列,如果 id 不是索引键那么InnoDB将完成表锁,并发将无从谈起
(1)Record lock:单个行记录上的锁
比如:select id from tb_student id = 1
这就是给一个唯一的数据行加锁,所以使用的就是记录锁
(2)Gap lock:间隙锁,锁定一个范围,不包括记录本身
如果语句中id列没有建立索引或者是非唯一索引时,则语句会产生间隙锁。
比如:
间隙锁可以实现可重复读。
(3)Next-key lock:record+gap 锁定一个范围,包含记录本身
比如在进行范围查询的时候,就是使用next-key lock;当查询的索引含有唯一属性时,将next-key lock降级为record key
相关知识点:
(1)innodb对于行的查询使用next-key lock
(2)Next-locking keying为了解决Phantom Problem幻读问题
(3)当查询的索引含有唯一属性时,将next-key lock降级为record key
(4)Gap锁设计的目的是为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生
(5)有两种方式显式关闭gap锁:(除了外键约束和唯一性检查外,其余情况仅使用record lock) A. 将事务隔离级别设置为RC B. 将参数innodb_locks_unsafe_for_binlog设置为1
常见的解决死锁的方法:
(1)如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
(2)在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
(3)对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
如果业务处理不好可以用分布式事务锁或者使用乐观锁
数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。
(1)悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。**比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。**在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
(2)乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:乐观锁一般会使用版本号机制或CAS算法实现。
乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
两种锁的使用场景:
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
乐观锁的缺点:
乐观锁包含了CAS的一些缺点:
(1)ABA问题:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。
(2)循环时间长开销大:自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。
(3)只能保证一个共享变量的原子操作
(1)主键:数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。
(2)外键:在一个表中存在的另一个表的主键称此表的外键。