mysql的事务隔离级别与锁的关系、sql日志、Spring事务的特性

数据库事务介绍

什么是数据库事务?
事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。
事务最经典也经常被拿出来说例子就是转账了。
假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。

# 开启一个事务
START TRANSACTION;
# 多条 SQL 语句
SQL1,SQL2...
## 提交事务
COMMIT;

关系性数据库需要遵循ACID规则

  1. 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  2. 一致性: 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
  3. 隔离性: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  4. 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

原子性由undo log日志来保证,它记录了事务需要回滚的日志信息,事务回滚时撤销已经执行的sql
隔离性由MVCC来保证
持久性由内存+redo log来保证,mysql修改数据同时在内存和redolog记录这次操作,宕机时可以从redolog中恢复
一致性由其他三大特性保证

什么是事务的隔离级别?

首先看两个丢失更新的例子:
第一种丢失更新
第一类丢失更新
如下两个事务,A事务先查询账户余额有1000元,存入500元使其账户变为1500,同时B事务进行转账并提交了事务,使账户金额变为500,而事务A最后因为某些原因,没有提交事务,而是回滚了事务,将账户金额重新设置为1000。但实际上账户已经被转走了500元,这就是第一类丢失更新。

在这里插入图片描述 总结:A事务update、B事务update同一条数据,A事务回滚

第二种:

在这里插入图片描述
注意T5时刻提交的事务。因为在事务1中,无法感知事务2的操作,这样它就不知道事务2已经修改过了数据,因此它依旧认为只是发生了一笔业务,所以库存变为了99,而这个结果又是一个错误的结果。这样,T5时刻事务1提交的事务,就会引发事务2提交结果的丢失,我们把这样的多个事务都提交引发的丢失更新称为第二类丢失更新。这是我们互联网系统需要关注的重点内容。为了克服这些问题,数据库提出了事务之间的隔离级别的概念

为了达到事务的四大特性,数据库定义了4种不同的事务隔离级别,由低到高依次为Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。

在这里插入图片描述

隔离级别隔离级别的值导致的问题
Read-Uncommitted0导致脏读
Read-Committed1避免脏读,允许不可重复读和幻读
Repeatable-Read2避免脏读,不可重复读,允许幻读
Serializable3串行化读,事务只能一个一个执行,避免了脏读、不可重复读、幻读。执行效率慢,使用时慎重

下面介绍四种隔离级别:
(1)Read Uncommitte读,读取未提交内容) 在该隔离级别,所有事务都可以看到其他未提交事务的执行结果,即在未提交读级别,事务中的修改,即使没有提交,对其他事务也都是可见的,该隔离级别很少用于实际应用。读取未提交的数据,也被称之为脏读(Dirty Read)。该隔离级别最低,并发性能最高。

(2)Read Committed(提交读,读取提交内容) 这是大多数数据库系统的默认隔离级别。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。
(3)Repeatable Read(可重复读) 可重复读可以确保同一个事务,在多次读取同样数据的时候,得到同样的结果。可重复读解决了脏读的问题,不过理论上,这会导致另一个棘手的问题:幻读(Phantom Read)MySQL数据库中的InnoDB和Falcon存储引擎通过MVCC(Multi-Version Concurrent Control,多版本并发控制)机制解决了该问题。需要注意的是,多版本只是解决不可重复读问题,而加上间隙锁(也就是它这里所谓的并发控制)才解决了幻读问题。

(4)Serializable(可串行化、序列化) 这是最高的隔离级别,它通过强制事务排序,强制事务串行执行,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能出现大量的超时现象和锁竞争。实际应用中也很少用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑用该级别。这是花费代价最高但是最可靠的事务隔离级别。

脏读幻读不可重复读的示例?

❃❃❃脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。即一事务对数据进行了增删改,但未提交,另一事务可以读取到未提交的数据。如果第一个事务这时候回滚了,那么第二个事务就读到了脏数据。

读未提交一般是比较危险的隔离级别,会发生脏读,在我们实际应用中采用得不多。为了克服脏读的问题,数据库隔离级别还提供了读写提交(read commited)的级别

❃❃❃不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。即一个事务中发生了两次读操作,第一次读操作和第二次操作之间,另外一个事务对数据进行了修改,这时候两次读取的数据是不一致的。

❃❃❃幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。
即第一个事务对一定范围的数据进行批量修改,第二个事务在这个范围增加一条数据,这时候第一个事务就会丢失对新增数据的修改。
发生脏读示例
在这里插入图片描述

克服脏读示例
在这里插入图片描述

在T3时刻,由于采用了读写提交的隔离级别,因此事务2不能读取到事务1中未提交的库存1,所以扣减库存的结果依旧为1,然后它提交事务,则库存在T4时刻就变为了1。T5时刻,事务1回滚,因为第一类丢失更新已经克服,所以最后结果库存为1,这是一个正确的结果。但是读写提交也会产生下面的问题,如下表所描述的场景(不可重复读)。
不可重复读
在这里插入图片描述
可重复读(解决不可重复读问题)
在这里插入图片描述
可以看到,事务2在T3时刻尝试读取库存,但是此时这个库存已经被事务1事先读取,所以这个时候数据库就阻塞它的读取,直至事务1提交,事务2才能读取库存的值。此时已经是T5时刻,而读取到的值为0,这时就已经无法扣减了,显然在读写提交中出现的不可重复读的场景被消除了。但是这样也会引发新的问题的出现,这就是幻读。

幻读例子
在这里插入图片描述
这便是幻读现象。可重复读和幻读,是读者比较难以理解的内容,这里稍微论述一下。首先这里的笔数不是数据库存储的值,而是一个统计值,商品库存则是数据库存储的值,这一点是要注意的。也就是幻读不是针对一条数据库记录而言,而是多条记录,例如,这51笔交易笔数就是多条数据库记录统计出来的。而可重复读是针对数据库的单一条记录,例如,商品的库存是以数据库里面的一条记录存储的,它可以产生可重复读,而不能产生幻读。


也许你会有一个疑问,都全部消除丢失更新不就好了吗,为什么只是在不同的程度上压制丢失更新呢?其实这个问题是从两个角度去看的,一个是数据的一致性,另一个是性能。数据库现有的技术完全可以避免丢失更新,但是这样做的代价,就是付出锁的代价在互联网中,系统不单单要考虑数据的一致性,还要考虑系统的性能。试想,在互联网中使用过多的锁,一旦出现商品抢购这样的场景,必然会导致大量的线程被挂起和恢复,因为使用了锁之后,一个时刻只能有一个线程访问数据,这样整个系统就会十分缓慢,当系统被数千甚至数万用户同时访问时,过多的锁就会引发宕机,大部分用户线程被挂起,等待持有锁事务的完成,这样用户体验就会十分糟糕。因为用户等待的时间会十分漫长,一般而言,互联网系统响应超过 5 秒,就会让用户觉得很不友好,进而引发用户忠诚度下降的问题。所以选择隔离级别的时候,既需要考虑数据的一致性避免脏数据,又要考虑系统性能的问题。因此数据库的规范就提出了 4 种隔离级别来在不同的程度上压制丢失更新。

mysql默认的隔离级别

这里需要注意的是:Mysql 默认采用的 REPEATABLE_READ隔离级别
Oracle默认采用的 READ_COMMITTED隔离级别
事务隔离机制的实现基于锁机制和并发调度。其中并发调度使用的是MVVC(多版本并发控制),通过保存修改的旧版本信息来支持并发一致性读和回滚等特性。

因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容):,但是你要知道的是InnoDB 存储引擎默认使用 REPEATABLE-READ(可重读)并不会有任何性能损失。
InnoDB 存储引擎在 分布式事务 的情况下一般会用到SERIALIZABLE(可串行化)隔离级别。

Innodb是如何实现事务的

Innodbi通过 Buffer Pool,LogBuffer,. Redo Log,. Undo Log来实现事务,以一个 updatei语句为例:

1.Innodb在收到一个 updatei语句后,会先根条件找到数据所在的页,并将该页缓存在 Buffer Pool中

2.执行 updatei语句,修改 Buffer Pool中的数据,也就是内存中的数据
3.针对 updatei语句生成一个 RedoLog对象,并存入 LogBuffer中
4.针对 updatei语句生成 undolog日志,用于事务回滚
5.如果事务提交,那么则把 RedoLogX对象进行久化,后续还有其他机制将Buffer Pool中所改的数据页持久化到磁盘中
6.如果事务回滚,则利用 undolog日志进行回滚

每启动一个mysql程序,就会获得一个单独的数据库连接,每个数据库连接都有一个全局变量@@txx_isolation,表示当前事务隔离级别
查看当前事务隔离级别:select @@tx_isolation
设置当前mysql连接的隔离级别:

set transaction isolation level read commited

设置数据库系统全局的隔离级别

set global transaction isolation level read commited

Mysql锁有哪些

加锁的目的:
数据库是一个多用户使用的共享资源。当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况。若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。锁是用于管理对公共资源的并发控制。也就是说在并发的情况下,会出现资源竞争,所以需要加锁。
加锁解决了 多用户环境下保证数据库完整性和一致性。
使用锁的对象是事务,事务用来锁定数据库的对象是表、页、行。并且一般锁定的对象仅在事务commit或rollback后进行释放(不同事务隔离级别释放的时间可能不同)。

锁的分类
图片
锁粒度
一、行锁
行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。有可能会出现死锁的情况,出现死锁的解决办法就是必须有一方事务回滚或者同时回滚。另外,行级锁按照使用方式分为共享锁和排他锁。

声明数据库行锁
begin tran tran1
select * from MPHead with(rowlock,updlock) where id=1;
waitfor delay ‘00:00:10’;
commit tran;

二、表锁
表级锁是mysql锁中粒度最大的一种锁,表示当前的操作对整张表加锁,资源开销比行锁少,不会出现死锁的情况,但是发生锁冲突的概率很大。被大部分的mysql引擎支持,MyISAM和InnoDB都支持表级锁,但是InnoDB默认的是行级锁。
Mysql的表级别锁分为两类:元数据锁(Metadata Lock,MDL)、表锁。

【元数据锁(Metadata Lock,MDL)】
元数据锁(MDL) 不需要显式使用,在访问一个表的时候会被自动加上。这个特性需要MySQL5.5版本以上才会支持,当对一个表做增删改查的时候,该表会被加MDL读锁当对表做结构变更的时候,加MDL写锁

MDL锁规则:
1、读锁之间不互斥
2、读写锁、写锁之间是互斥的,为了保证表结构变更的安全性,所以如果要多线程对同一个表加字段等表结构操作,就会变成串行化,需要进行锁等待
3、MDL的写锁优先级比MDL读锁的优先级高
4、MDL的锁释放必须要等到事务结束才会释放

MDL锁的例子

在这里插入图片描述

若没有MDL锁的保护,则事务2可以直接执行DDL操作,并且导致事务1出错,5.1版本即是如此。5.5版本加入MDL锁就在于保护这种情况的发生,由于事务1开启了查询,那么获得了MDL锁,锁的模式为SHARED_READ(读锁模式),事务2要执行DDL,则需获得EXCLUSIVE锁(写锁),两者互斥,所以事务2需要等待。

四、页锁
页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。

按锁粒度分类:
1.行锁:锁某行数据,锁粒度最小,并发度高
2.表锁:锁整张表,锁粒度最大,并发度低
3.间隙锁:锁的是一个区间
还可以分为
1.共享锁:也就是读锁,一个事务给某行数据加了读,其他事务也可以读,但是不能写
2.排它锁:也就是写锁,一个事务给某行数据加了锁,其他事务不能读,也不能写
还可以分为:
1.乐观锁:并不会真正的去锁某行记录,而是通过一个版本号来实现的
2.悲观锁:上面所的行锁、表锁等都是悲观锁
锁的分类如图:
在这里插入图片描述

在事务的隔离级别实现中,就需要利用锁来解决幻读

for update

1.for update的使用场景

2.for update如何使用

3.for update的锁表

4.for update的注意点

5.for update的疑问点

for update的使用场景

如果遇到存在高并发并且对于数据的准确性很有要求的场景,是需要了解和使用for update的。
比如涉及到金钱、库存等。一般这些操作都是很长一串并且是开启事务的。如果库存刚开始读的时候是1,而立马另一个进程进行了update将库存更新为0了,而事务还没有结束,会将错的数据一直执行下去,就会有问题。所以需要for upate 进行数据加锁防止高并发时候数据出错。

记住一个原则:一锁二判三更新


for update如何使用
使用姿势:

select` `* from table where xxx ``for` `update

for update的锁表

InnoDB默认是行级别的锁,当有明确指定的主键时候,是行级锁。否则是表级别。

例子: 假设表foods ,存在有id跟name、status三个字段,id是主键,status有索引。

例1: (明确指定主键,并且有此记录,行级锁)

SELECT * FROM foods WHERE ``id``=1 FOR UPDATE; ``SELECT * FROM foods WHERE ``id``=1 and name=’咖啡色的羊驼’ FOR UPDATE;

例2: (明确指定主键/索引,若查无此记录,无锁)

SELECT * FROM foods WHERE id=-1 FOR UPDATE;

例3: (无主键/索引,表级锁)

SELECT * FROM foods WHERE name=’咖啡色的羊驼’ FOR UPDATE;

例4: (主键/索引不明确,表级锁)

SELECT * FROM foods WHERE ``id``<>’3’ FOR UPDATE; ``SELECT * FROM foods WHERE ``id` `LIKE ‘3’ FOR UPDATE;

for update的注意点

1.for update 仅适用于InnoDB,并且必须开启事务,在begin与commit之间才生效。

2.要测试for update的锁表情况,可以利用MySQL的Command Mode,开启二个视窗来做测试。

for update的疑问点

当开启一个事务进行for update的时候,另一个事务也有for update的时候会一直等着,直到第一个事务结束吗?

答:会的。除非第一个事务commit或者rollback或者断开连接,第二个事务会立马拿到锁进行后面操作。

如果没查到记录会锁表吗?

答:会的。表级锁时,不管是否查询到记录,都会锁定表。

获取innodb行级锁的争用情况:

show status like ‘%innodb_row_lock%’;

什么是间隙锁?(Next-Key)

当使用范围条件而不是相等条件检索数据的时候,并请求共享或排它锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,称为“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙(Next-Key)锁。间隙锁是InnoDB中行锁的一种,但是这种锁锁住的不止一行数据,它锁住的是多行,是一个数据范围。间隙锁的主要作用是为了防止出现幻读(Phantom Read),用在Repeated-Read(简称RR)隔离级别下。在Read-Commited(简称RC)下,一般没有间隙锁(有外键情况下例外,此处不考虑)。间隙锁还用于恢复和复制。

间隙锁的出现主要集中在同一个事务中先DELETE后INSERT的情况下,当通过一个条件删除一条记录的时候,如果条件在数据库中已经存在,那么这个时候产生的是普通行锁,即锁住这个记录,然后删除,最后释放锁。如果这条记录不存在,那么问题就来了,数据库会扫描索引,发现这个记录不存在,这个时候的DELETE语句获取到的就是一个间隙锁,然后数据库会向左扫描,扫到第一个比给定参数小的值,向右扫描,扫描到第一个比给定参数大的值,然后以此为界,构建一个区间,锁住整个区间内的数据,一个特别容易出现死锁的间隙锁诞生了。

在MySQL的InnoDB存储引擎中,如果更新操作是针对一个区间的,那么它会锁住这个区间内所有的记录,例如UPDATE XXX WHERE ID BETWEEN A AND B,那么它会锁住A到B之间所有记录,注意是所有记录,甚至如果这个记录并不存在也会被锁住,在这个时候,如果另外一个连接需要插入一条记录到A与B之间,那么它就必须等到上一个事务结束。典型的例子就是使用AUTO_INCREMENT ID,由于这个ID是一直往上分配的,因此,当两个事务都INSERT时,会得到两个不同的ID,但是这两条记录还没有被提交,因此,也就不存在,如果这个时候有一个事务进行范围操作,而且恰好要锁住不存在的ID,就是触发间隙锁问题。所以,MySQL中尽量不要使用区间更新。

InnoDB除了通过范围条件加锁时使用间隙锁外,如果使用相等条件请求给一个不存在的记录加锁,那么InnoDB也会使用间隙锁! 间隙锁也存在副作用,它会把锁定范围扩大,有时候也会带来麻烦。如果要关闭,那么一是将会话隔离级别改到RC下,或者开启innodb_locks_unsafe_for_binlog(默认是OFF)。**间隙锁只会出现在辅助索引(二级索引)上,唯一索引和主键索引是没有间隙锁。**间隙锁(无论是S还是X)只会阻塞INSERT操作。 在MySQL数据库参数中,控制间隙锁的参数是innodb_locks_unsafe_for_binlog,这个参数的默认值是OFF,也就是启用间隙锁,它是一个布尔值,当值为TRUE时,表示DISABLE间隙锁。

mysql的日志

MySQL日志主要包含:错误日志、查询日志、慢查询日志、事务日志、二进制日志。

1.1 错误日志:

在MySQL数据库中,错误日志功能是默认开启的,而且无法被关闭。默认情况,错误日志存储在mysql数据库的数据文件中。错误日志文件通常的名称为hostname.err(hostname表示服务器的主机名)。

错误日志可以自己配置,错误日志可以通过log-error和log-warnings来定义,其中log-error:配置是否启用错误日志功能和错误日志的存储位置?log-warning:配置是否将警告信息也定义至错误日志中?

错误日志记录信息:服务器启动关闭信息、运行错误信息、时间调度器运行一个事件时产生的信息、在服务器上启动进程产生的信息。

1.2 查询日志:

默认情况,查询日志是关闭的。因为查询日志会记录用户所有的操作,其中还包括增删改查等信息,如果在高并发的环境下会产生大量的信息,导致不必要的磁盘IO,会影响mysql的性能。

1.3 慢日志:

慢查询日志是用来记录执行时间超过指定时间的查询语句。通过慢查询日志,可以查找出哪些查询语句的执行效率很低,以便进行优化。一般建议开启,它对服务器性能影响很小,但是可以记录MySQL服务器上执行很长时间的查询语句。可以帮助我们定义性能问题。

1.4 事务日志:

事务日志(InnoDB特有的日志)可以帮助提高事务的效率。使用事务日志,存储引擎在修改表的数据时只需要修改其内存拷贝,再把改修改行为记录到持久在硬盘上的事务日志中,而不用每次都将修改的数据本身持久到磁盘。事务日志采用追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序I/O,而不像随机I/O需要在磁盘的多个地方移动磁头,所以采用事务日志的方式相对来说要快得多。事务日志持久以后,内存中被修改的数据在后台可以慢慢的刷回到磁盘。目前大多数的存储引擎都是这样实现的,我们通常称之为预写式日志,修改数据需要写两次磁盘。如果数据的修改已经记录到事务日志并持久化,但数据本身还没有写回磁盘,此时系统崩溃,存储引擎在重启时能够自动恢复这部分修改的数据。具有的恢复方式则视存储引擎而定。

1.5 二进制日志:

二进制日志也叫作变更日志,主要用于记录修改数据或有可能引起数据改变的mysql语句,并且记录了语句发生时间、执行时长、操作的数据等等。所以说通过二进制日志可以查询mysql数据库中进行了哪些变化。一般大小体积上限为1G。

查看日志信息?

mysql> show global variables like '%log%';
 查看慢日志超时时间
mysql> show global variables like 'long%';

其中这个慢查询时间并不是只表示语句自身执行超过10秒还包含由于其他资源被征用造成阻塞的查询执行时间或其他原因等都被记录到慢查询中。所以这个慢查的时长表示从查询开始到查询结束中间包含可能的任何原因所经历的所有时间。

可参考:
MySQL日志查看详解

undolog

(4)undo log:undo log是回滚日志,如果事务回滚,则需要依赖undo日志进行回滚操作。MySQL在进行事务操作的同时,会记录事务性操作修改数据之前的信息,就是undo日志,确保可以回滚到事务发生之前的状态。innodb的undo log存放在ibdata1共享表空间中,当开启事务后,事务所使用的undo log会存放在ibdata1中,即使这个事务被关闭,undo log也会一直占用空间。为了避免ibdata1共享表空间暴涨,建议将undo log单独存放。innodb_undo_directory参数指定单独存放undo表空间的目录,该参数实例初始化之后不可直接改动(可以通过先停库,修改配置文件,然后移动undo表空间文件的方式去修改该参数);innodb_undo_tablespaces参数指定单独存放的undo表空间个数,推荐设置为大于等于3;innodb_undo_logs参数指定回滚段的个数,默认为128个。MySQL 5.7引入了新的参数innodb_undo_log_truncate,这个参数开启后可在线收缩拆分出来的undo表空间。

(5)临时表空间:存储临时对象的空间,比如临时表对象等,参数innodb_temp_data_file_path可以看到临时表空间的信息,上限设置为5GB。

show variables like ‘%undo%’;

redolog详解

redo log buffer

redo log buffer是一块内存区域,存放将要写入redo log文件的数据。redo log buffer大小是通过设置innodb_log_buffer_size参数来实现的。redo log buffer会周期性地刷新到磁盘的redo log file中。一个大的redo log buffer允许大事务在提交之前不写入磁盘的redo log文件。因此,如果有事务需要update、insert、delete许多记录,则可增加redo log buffer来节省磁盘I/O。 参数innodb_flush_log_at_trx_commit选项控制redo log buffer的内容何时写入redo log file,即控制redo log flush的频率。

innodb_flush_log_at_trx_commit的参数取值及其说明如下:

设置为0:在提交事务时,InnoDB不会立即触发将缓存日志log buffer写到磁盘文件的操作,而是每秒触发一次缓存日志回写磁盘操作,并调用操作系统fsync刷新I/O缓存。

设置为1:每次事务提交时MySQL都会立即把log buffer的数据写入redo log file,并且调用操作系统fsync刷新I/O缓存(刷盘操作)。值为1时,每次事务提交都持久化一次,当然是最安全的,但是数据库性能会受影响,I/O负担重,适合对安全要求极高的交易系统场景(建议配置SSD硬盘提高I/O能力)。

设置为2:每次事务提交时MySQL都会把redo log buffer的数据写入redo log file,但是flush(刷到磁盘)操作并不会同时进行,而是每秒执行一次flush(磁盘I/O缓存刷新)。注意,由于进程调度策略问题,并不能保证百分之百的“每秒”。 刷写其实是两个操作,即刷flush)和写(write)。区分这两个概念(两个系统调用)是很重要的。在大多数的操作系统中,把InnoDB的log buffer(内存)写入日志(调用系统调用write),只是简单地把数据移到操作系统缓存(内存)中,并没有实际持久化数据。 通常设置为0的时候,mysqld进程的崩溃会导致上一秒钟的所有事务数据丢失。当该值为2时,表示事务提交时不写入重做日志文件,而是写入文件系统缓存中,当DB数据库发生故障时能恢复数据,如果操作系统也出现宕机,就会丢失掉文件系统没有及时写入磁盘的数据。 设为1当然是最安全的,适合数据安全性要求非常高的而且磁盘I/O写能力足够支持业务,比如订单、交易、充值支付消费系统。如果对数据一致性和完整性要求不高,完全可以设为2,推荐使用带蓄电池后备电源的缓存cache,防止系统断电异常。如果只要求性能,例如高并发写的日志服务器,就设置为0来获得更高性能。

引入Redo日志是为了更好地提升数据库整体性能,其实Redo日志还有以下作用:

· 快速提交。

· 恢复实例。

· 增量备份,以及恢复到某一时间点。

· 复制(在MySQL中使用Binlog复制,也有一些大厂商实现了基于Redo日志的复制)。

redo log 与binlog什么区别

  1. redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。

  2. redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。

  3. redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

redo log 用于保证 crash-safe 能力。innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数我建议你设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。

sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数我也建议你设置成 1,这样可以保证 MySQL 异常重启之后 binlog 不丢失。

spring的事务

Spring事务是Spring AOP的最佳实践之一,Spring管理的事务是逻辑事务,而且物理事务和逻辑事务最大差别就在于事务传播行为, 事务传播行为用于指定在多个事务方法间调用时,事务是如何在这些方法间传播的。
spring事务与数据库事务:
数据库是可以控制事务的传播和隔离级别的,Spring在之上又进一步进行了封装,可以在不同的项目、不同的操作中再次对事务的传播行为和隔离级别进行策略控制。
注意:Spring不仅可以控制事务传播行为(PROPAGATION_REQUIRED等),还可以控制事务隔离级别(ISOLATION_READ_UNCOMMITTED等)。

原文链接:

spring事务管理与数据库事务的关系

spring事务实现原理

spring事务的实现方式原理是什么?
在使用Springt框架的时候,可以有两种事务的实现方式,一种是编程式事务,有用户自己通过代码来控制事务的处理逻辑,还有一种是声明式事务,通过@Transactional注解来实现。
  其实事务的操作本来应该是由数据库来进行控制,但是为了方便用户进行业务逻辑的操作,spring对事务功能进行了扩展实现,一般我们很少会用编程式事务,更多的是通过添加@Transactional注解来进行实现,当添加此注解之后事务的自动功能就会关闭,有spring框架来帮助进行控制。
  其实事务操作是AOP的一个核心体现,当一个方法添加@Transactionali注解之后,spring会基于这个类生成一个代理对象,会将这个代理对象作为bea,当使用这个代理对象的方法的时候,如果有事务处理,那么会先把事务的自动提交给关系,然后去执行具体的业务逻辑,如果执行逻辑没有出现异常,那么代理逻辑就会直接提交,如果出现任何异常情况,那么直接进行回滚操作,当然用户可以控制对哪些异常进行回滚操作。

spring事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的Exception(非运行时异常),它不会回滚。
关于异常:
在这里插入图片描述

所有的数据访问技术都有事务处理机制,这些技术提供了API用来开启事务、提交事务来完成数据操作,或者在发生错误的时候回滚数据。

而Spring的事务机制是用统一的机制来处理不同数据访问技术的事务处理。Spring的事务机制提供了一个PlatformTransactionManager接口,不同的数据访问技术的事务使用不同的接口实现

在事务控制方面,主要有两个分类:

编程式事务:在代码中直接加入处理事务的逻辑,可能需要在代码中显式调用beginTransaction()、commit()、rollback()等事务管理相关的方法

connetion.autoCommit(false);
------
----
---
connction.commint()
catch(){
connction.rollback();
}

声明式事务:在方法的外部添加注解或者直接在配置文件中定义,将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。spring的AOP恰好可以完成此功能:事务管理代码的固定模式作为一种横切关注点,通过AOP方法模块化,进而实现声明式事务。

spring事务隔离级别就是数据库的隔离级别外加一个默认级别

Spring中的隔离级别

常量解释
ISOLATION_DEFAULT这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别。另外四个与 JDBC 的隔离级别相对应。
ISOLATION_READ_UNCOMMITTED这是事务最低的隔离级别,它充许另外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。
ISOLATION_READ_COMMITTED保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。
ISOLATION_REPEATABLE_READ这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。
ISOLATION_SERIALIZABLE这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。

可重复读

事务A先进行数据查询,事务B进行一次数据修改,事务A再次查询数据 数据不变,事务B进行数据提交,事务A再进行一次查询,数据是不改变的。

读已提交

事务A先进行数据查询,事务B进行一次事务修改,事务A再次查询数据 数据不变,事务B进行数据提交,事务A再进行一次查询,数据是改变的。

spring事务的传播行为

在这里插入图片描述

spring事务的配置

@Transactional的使用:

其中该注解有几个属性:

1、超时属性(timeout)
指定事务等待的最长时间(秒)
当前事务访问数据时,有可能访问的数据被别的数据进行加锁的处理,那么此时事务就必须等待,如果等待时间过长给用户造成的体验感差。

2、设置事务只读(readOnly)
readonly:只会设置在查询的业务方法中
connection.setReadOnly(true) 通知数据库,当前数据库操作是只读,数据库就会对当前只读做相应优化
当将事务设置只读 就必须要你的业务方法里面有增删改。

	如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性;
	如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持(如:设置不可重复度、幻影读级别)。
	对于只读事务,可以指定事务类型为readonly,即只读事务。由于只读事务不存在数据的修改,因此数据库将会为只读事务提供一些优化手段 

3、异常属性设置
当前事务出现的那些异常就进行回滚或者提交。

默认对于RuntimeException 及其子类 采用的是回滚的策略。
默认对于Exception 及其子类 采用的是提交的策略。
1、设置哪些异常不回滚(noRollbackFor)
2、设置哪些异常回滚(rollbackFor )

@Transactional(timeout = 3,
rollbackFor = {FileNotFoundException.class})

在实战中事务的使用方式
如果当前业务方法是一组 增、改、删 可以这样设置事务
@Transactional
如果当前业务方法是一组 查询 可以这样设置事务
@Transactionl(readOnly=true)
如果当前业务方法是单个 查询 可以这样设置事务
@Transactionl(propagation=propagation.SUPPORTS ,readOnly=true)

使用transactional注解步骤:

基于xml的事务配置jdbcTemplate.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:context="http://www.springframework.org/schema/context"
      xmlns:aop="http://www.springframework.org/schema/aop"
      xmlns:tx="http://www.springframework.org/schema/tx"
      xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context.xsd
      http://www.springframework.org/schema/aop
      https://www.springframework.org/schema/aop/spring-aop.xsd
      http://www.springframework.org/schema/tx
      https://www.springframework.org/schema/tx/spring-tx.xsd
">
   <context:component-scan base-package="cn.tulingxueyuan"></context:component-scan>
   <context:property-placeholder location="classpath:dbconfig.properties"></context:property-placeholder>
   <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
       <property name="username" value="${jdbc.username}"></property>
       <property name="password" value="${jdbc.password}"></property>
       <property name="url" value="${jdbc.url}"></property>
       <property name="driverClassName" value="${jdbc.driverClassName}"></property>
   </bean>
   <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
       <constructor-arg name="dataSource" ref="dataSource"></constructor-arg>
   </bean>
   <bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
       <constructor-arg name="dataSource" ref="dataSource"></constructor-arg>
   </bean>
   <!--事务控制-->
   <!--配置事务管理器的bean-->
   <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
       <property name="dataSource" ref="dataSource"></property>
   </bean>
   <!--
   基于xml配置的事务:依赖tx名称空间和aop名称空间
       1、spring中提供事务管理器(切面),配置这个事务管理器
       2、配置出事务方法
       3、告诉spring哪些方法是事务方法(事务切面按照我们的切入点表达式去切入事务方法)
   -->
   <bean id="bookService" class="cn.tulingxueyuan.service.BookService"></bean>
   <aop:config>
       <aop:pointcut id="txPoint" expression="execution(* cn.tulingxueyuan.service.*.*(..))"/>
       <!--事务建议:advice-ref:指向事务管理器的配置-->
       <aop:advisor advice-ref="myAdvice" pointcut-ref="txPoint"></aop:advisor>
   </aop:config>
   <tx:advice id="myAdvice" transaction-manager="transactionManager">
       <!--事务属性-->
       <tx:attributes>
           <!--指明哪些方法是事务方法-->
           <tx:method name="*"/>
           <tx:method name="checkout" propagation="REQUIRED"/>
           <tx:method name="get*" read-only="true"></tx:method>
       </tx:attributes>
   </tx:advice>
</beans>

基于注解的事务配置

1、开启注解驱动


/**
 * Created by xsls on 2019/6/17.
 */
@EnableTransactionManagement
 @EnableAspectJAutoProxy//(exposeProxy = true)
 @ComponentScan(basePackages = {"com.xx"})
 public class MainConfig {
 @Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername("root");
dataSource.setPassword("123456");
dataSource.setUrl("jdbc:mysql://localhost:3306/spring‐trans");
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
return dataSource;
 }

/**
 * 配置JdbcTemplate Bean组件
 * <bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
 * <property name="dataSource" ref="dataSource" ></property>
* </bean>
* @param dataSource
 * @return
 */
 @Bean
 public JdbcTemplate jdbcTemplate(DataSource dataSource) {
 return new JdbcTemplate(dataSource);
 }
 /**
35 * 配置事务管理器
36 * <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionMana
ger">
37 * <property name="dataSource" ref="dataSource"></property>
38 * </bean>
39 * @param dataSource
40 * @return
41 */
@Bean
 public PlatformTransactionManager transactionManager(DataSource dataSource) {
 return new DataSourceTransactionManager(dataSource);
 }
}

提示:使用@EnableTransactionManagement注解前,请务必保证你已经配置了至少一个PlatformTransactionManager的Bean,否则会报错。(当
然你也可以实现TransactionManagementConfigurer来提供一个专属的,只是我们一般都不这么去做~~~)

2、在你想要加入事务的方法上(或者类(接口)上)标注@Transactional注解

@Component
3 @Transactional(rollbackFor = Exception.class)
4 public class PayServiceImpl implements PayService {
5
6 @Autowired
private AccountInfoDao accountInfoDao;

9 @Autowired
10 private ProductInfoDao productInfoDao;
11
12 public void pay(String accountId, double money) {
13
14 //更新余额
15 int retVal = accountInfoDao.updateAccountBlance(accountId,money);
16
17
18 System.out.println(1/0);
19
20 }
21 }

3、测试

public static void main(String[] args) {
 AnnotationConfigApplicationContext context = new
AnnotationConfigApplicationContext(MainConfig.class);
PayService payService = (PayService) context.getBean("payServiceImpl");
 payService.pay("123456789",10);
 }

就这么简单,事务就生效了(这条数据并没有insert成功~)。

注解驱动事务的原理

在这里插入图片描述

@Target(ElementType.TYPE)
 @Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {
boolean proxyTargetClass() default false;
AdviceMode mode() default AdviceMode.PROXY;
int order() default Ordered.LOWEST_PRECEDENCE;
}

属性和@EnableAspectJAutoProxy注解一个套路。不同之处只在于@Import导入器导入的这个类,不同的是:它导入的是个ImportSelector

TransactionManagementConfigurationSelector
它所在的包为org.springframework.transaction.annotation,jar属于:spring-tx(若引入了spring-jdbc,这个jar会自动导入)

public class TransactionManagementConfigurationSelector extends AdviceModeImportSelector<EnableTransactionManagement> {

	@Override
	protected String[] selectImports(AdviceMode adviceMode) {
		switch (adviceMode) {
			// 很显然,绝大部分情况下,我们都不会使用AspectJ的静态代理的~~~~~~~~
			// 这里面会导入两个类~~~
			case PROXY:
				return new String[] {AutoProxyRegistrar.class.getName(),
						ProxyTransactionManagementConfiguration.class.getName()};
			case ASPECTJ:
				return new String[] {determineTransactionAspectClass()};
			default:
				return null;
		}
	}
	private String determineTransactionAspectClass() {
		return (ClassUtils.isPresent("javax.transaction.Transactional", getClass().getClassLoader()) ?
				TransactionManagementConfigUtils.JTA_TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME :
				TransactionManagementConfigUtils.TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME);
	}

}
  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值