事务概述
事务是作为单个逻辑操作单元的一系列操作。事务可以包含一条或多条sql语句,所有的语句被当做一个操作单元,要么全部成功、要么全部失败(即作为一个原子操作)。
数据库中的事务要满足ACID特性:
- Atomicity:原子性,整个事务要么全部执行成功,要么全部执行失败;
- Consistency:一致性,数据库总是从一个一致性状态转为另外一个一致性状态,不存在中间状态;
- Isolation:隔离性,事务在提交之前所做的修改能否为其他事务可见;针对不同的隔离性,有不同的隔离级别;
- Durability:持久性,事务一旦提交,所做出的修改将永久有效(即修改不会丢失);
在MySQL中,事务是通过‘事务日志’来实现,具体可分为‘redo log’和‘undo log’。
redo log概述
MySQL在执行事务时,先将事务中的sql语句涉及到的所有数据库操作记录到‘redo log’中,然后将操作同步到数据库文件中(在实际修改数据库前,先把操作记录到日志中)。这样即使数据修改到一半被打断(如当机等),也能通过日志将剩余的操作同步到数据库中。
redo能够实现原子性(A),保证一个事务中所有sql被当成一个执行单元。‘redo log’是物理日志,其中记录的是数据库对页的操作,而非逻辑上的增删改查;因此重做日志具有幂等性。
‘redo log’由两部分组成:
- ‘redo log buffer’(重做日志缓冲区):存放在内存中
- ‘redo log file’(重做日志文件):存放在硬盘上,为一段连续的空间,以加快读写速度。
多个日志记录文件组成一个log group(重做日志组),当组中的第一个logfile被写满时,就写下一个;当组内的所有logfile都写满时,就重新从第一个logfile写(覆盖原来的)。为了保证进一步的安全,日志可存储在raid1等冗余设备上。
undo log概述
‘undo log’可看成数据修改前的备份。若事务执行过程中,有一条sql无法成功执行,需要进行回滚时,就需要根据‘undo log’进行撤销,将所有修改过的数据从逻辑上恢复到事务开始前的样子。
‘undo log’是逻辑日志,若前面inert 10
条记录,则undo时即delete 10
条;因此不具有幂等性。
事务控制语句
配置参数
MySql中(innodb引擎时),通过show global variables like 'innodb%log%'
查看与日志相关的配置参数:
- innodb_log_file_size:redo log file文件的大小;
- innodb_log_files_in_group:重做日志组中的有多少个redo log file;
- innodb_log_group_home_dir:日志组文件所在路径;
- innodb_mirrored_log_groups:日志组镜像的数量(没有镜像时为1);
- innodb_flush_log_at_trx_commit:事务提交以后,是否立即将redo从内存中刷到磁盘中。
- 1(默认):事务提交时刷到磁盘上;
事务提交
->log buffer
->OS buffer
->log file
; - 0:提交时并不会刷到磁盘,但会每秒自动刷一次;
- 2:提交时,只会写入到系统内存(OS buffer)中,每秒从系统缓存中刷到磁盘中;
- 1(默认):事务提交时刷到磁盘上;
控制语句
默认情况时,事务是自动提交的(执行sql语句后,自动执行commit):show global variable like 'autocommit%'
show session variable like 'autocommit%'
可通过控制语句,显式地控制事务:
start transaction / begin
:显式地开始一个事务(存储过程中要使用start transaction,避免与begin…end混淆);commit / commit work
:提交事务;rollback / rollback work
:回滚事务,撤销所有未提交的修改,并结束当前事务;savepoint
标识符:创建一个事务保存点,以便回滚到当前点(而非回滚整个事务);rollback to savepoint-标识符
:回滚到指定保存点;release savepoint-标识符
:删除一个保存点;
事务隔离级别
对于MySQL服务器,可以有若干个客户端与其连接(每个连接称为一个会话Session)。不同的会话可以同时发送各种请求(事务),为了避免事务之间互相影响,并提高系统的并发处理能力,提出了各种隔离级别。
隔离级别简介
事务通过锁机制满足隔离性,事务的隔离级别决定了事务间的隔离性:
READ-UNCOMMITED
:读未提交,能看到其他事务中未提交的修改(即脏读);READ-COMMITTED
:读提交,只要其他事务提交了,在当前事务中即可见;会出现‘不可重读’(两次读取的同一条目的内容可能不同)和‘幻读’(两次读取的条目数可能不同,多出或少了)问题;REPEATABLE-READ
(默认):可重复读,事务中对同一条记录看到的内容始终相同(在事务中,即使前后两条查询之间,其他事务对数据做了修改,读取到的内容不会改变);会出现‘幻读’问题;SERIALIZABLE
:串行化,做完一个事务再做其他事务,写事务会阻塞所有读事务;因此也就失去了并发能力。
show variable like 'tx_isolation'
可查看设置的隔离级别,my.cnf配置文件中设定transaction_isolation=REPEATABLE-READ
。
innodb中采用一致性非锁定读
机制(‘可重读’和‘读提交’隔离级别下)提高数据库并发性:读取数据时(使用非锁定读),若当前行被施加了排他锁,则不会等待锁释放,而是去读取一个快照数据。
因此会引起幻读
现象:即同一个事务中,前后两条查询语句看到的数据可能不同(多出、或减少条目)。
隔离级别越高,隔离性越强,遇到的问题越少,并发越弱:
标题 | 脏读 | 不可重读 | 幻读 |
---|---|---|---|
读未提交 | √ | √ | √ |
读提交 | × | √ | √ |
可重读 | × | × | √ |
串行化 | × | × | × |
MVCC并发控制
MVCC(Multi-Version Concurrency Control)多版本并发控制,用于‘可重复读’和‘读已提交’隔离级别的处理。通过保存数据库某个时间的快照,来实现的。
- 每行数据都存在一个版本,每次数据更新时更新该版本;
- 修改时复制(Copy)出当前版本随意修改,保证事务间无干扰;
- 保存时比较版本号,成功再覆盖原记录,失败则放弃;
每一行数据保存两个隐藏的列:当前行创建时的版本号和删除时的版本号(未删除时为空)。版本号为系统的版本号,新事务开始时自动递增(事务开始时系统版本号为事务的版本号)。
增删改查时的版本号处理
插入数据时(事务版本设为1):记录的版本号为当前事务的版本号
id | name | create-version | delete-version |
---|---|---|---|
1 | oldname | 1 |
更新数据时(事务版本设为2):先标记旧的数据为已删除(删除版本号为当前事务版本号),然后插入一条新的记录(更新后的数据);
id | name | create-version | delete-version |
---|---|---|---|
1 | oldname | 1 | 2 |
1 | new-name | 2 |
删除数据时(事务版本设为3):删除版本号记为当前事务版本号
id | name | create-version | delete-version |
---|---|---|---|
1 | new-name | 2 | 3 |
查询操作:只有符合条件的记录才会被查询出来
- 删除版本号未指定(空)或大于当前版本号,即事务开始时未删除的行;
- 创建版本号小于或等于当前版本号,即当前事务或之前已插入(或修改)的行;
锁类型简介
为保证数据访问的一致性、有效性,就需要锁;同时锁冲突也是影响数据库并发性能的一个重要因素:
- 表级锁:访问数据库时,锁定整表数据;开销小,加锁快;锁的粒度大,冲突概率高,并发度低;
- 行级锁:访问数据库时,锁定整行数据;开销大,加锁慢;会出现死锁,锁粒度小,冲突概率低,并发度高;
- 页锁:访问数据库时,锁定一页数据;介于前两者之间;会出现死锁,并发度一般;
数据库的增删改操作默认都会加排他锁,而查询不会加任何锁。
- 共享锁(悲观锁的一种):对某一资源加共享锁,自身可以读该资源,其他人也可以读该资源(也可以再继续加共享锁,即多个共享锁可共存),但无法修改。要想修改就必须等所有共享锁都释放完之后。
- 排他锁:对某一资源加排他锁,自身可以进行增删改查,其他人无法进行任何操作。
悲观锁与乐观锁是两种常见的资源并发锁设计思路。通常所说的“一锁二查三更新”即使用的是悲观锁。
- 悲观锁:先获取锁,再进行业务操作。每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这时别人想拿这个数据就会阻塞等待。
- 乐观锁:先进行业务操作,不到万不得已不去拿锁。每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
乐观锁在不发生取锁失败的情况下开销比悲观锁小,但是一旦发生失败回滚开销则比较大,因此乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。经常产生冲突,上层需要不断重试情况,乐观锁反倒是降低了性能,这种情况下用悲观锁就比较合适。