Spring事务管理实现原理及MySQL InnoBD引擎行锁概述

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring事务管理实现原理及MySQL InnoBD引擎行锁概述相关的知识,希望对你有一定的参考价值。

Spring实现事务管理的机制

Spring事务管理是基于AOP编程思想实现,Spring框架被广泛使用的原因之一,就是提供了强大的事务管理机制。

AOP是什么?我们常说的AOP并不是指一种开发技术,而是一种编程思想,AOP的核心概念就是面向切面编程,实现可插拔,降低程序之前的耦合性,提高重用性。

Spring AOP 基于动态代理实现,一种是JDK动态代理,另一种是CGLIB动态代理。

spring2.5之前声明式事务可以这样实现:

?<!-- 声明使用的spring事务管理器-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
    <property name="dataSource" ref="dataSource" />  
</bean>  
<tx:advice id="txAdvice" transaction-manager="txManager">  
    <tx:attributes>  
        <tx:method name="get*" read-only="true" />
        <tx:method name="add*" propagation="REQUIRED"/>
    </tx:attributes>  
</tx:advice>  
<aop:config proxy-target-class="true">  
    <aop:advisor advice-ref="txAdvice"   
    pointcut="execution(* com.junlong.demo.service.DemoService.*(..))"/>  
</aop:config>

 

在spring2.5之后声明式事务,增加了<tx:annotation-driven />事务注解驱动,这样可以更灵活的控制在哪声明事务,并且Spring2.5之后的一个核心主题就是全面的支持注解方式

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean> 
<tx:annotation-driven />

 

几个核心的注意事项

一、Spring事务管理基于动态代理的两种方式(JDKProxy、CGLIBProxy)

<tx:annotation-driven transaction-manager="transactionManager" />

1.如果代理对象实现了接口则采用JDK动态代理实现,JDK动态代理基于接口进行代理的,它是通过在运行时创建一个接口的实现类来完成对目标对象的代理,所以使用@Transactional 注解的类想要基于JDK动态代理进行事务控制必须实现一个接口。

Spring默认会在JDK动态代理和CGLIB动态代理之间进行切换,如果被代理的类实现了接口则会使用JDK动态代理,否则会使用CGLIB动态代理。

如果想强制使用基于类的代理,可以设置proxy-target-class=true,这样Spring会使用CGLIB代理,在使用CGLIB代理时@Transactional必须写在具体的实现类上或者实现方法上,因为CGLIB动态代理是在运行时对代理对象进行继承扩展子类,所以声明final的类是无法使用CGLIB代理的。

 

<xsd:attribute name="proxy-target-class" type="xsd:boolean" default="false">
    <xsd:annotation>
        <xsd:documentation>
            <![CDATA[
                Are class-based (CGLIB) proxies to be created? By default, standard
                Java interface-based proxies are created.
            
                Note: Class-based proxies require the @Transactional annotation to be
                defined on the concrete class. Annotations in interfaces will not work
                in that case (they will rather only work with interface-based proxies)!
            ]]>
        </xsd:documentation>
    </xsd:annotation>
</xsd:attribute>

2.官方文档介绍@Transactional 注解只有在public的方法上生效,这是因为Spring事务注解驱动是基于Spring AOP(也就是默认的方式是基于动态代理实现)实现的。

3.同一个java类内部方法调用本类内部的其他方法并不会引起事务行为,即使被调用方法使用@Transactional注解进行修饰,这也是因为Spring实现事务代理是默认是基于Spring AOP(也就是默认的方式是基于动态代理实现)实现的。

二、Spring事务管理实现的两种方式(动态代理、静态编织)

<tx:annotation-driven transaction-manager="transactionManager" mode="proxy" />
<xsd:attribute name="mode" default="proxy">
    <xsd:annotation>
        <xsd:documentation>
        <![CDATA[
            Should annotated beans be proxied using Spring‘s AOP framework,
            or should they rather be weaved with an AspectJ transaction aspect?
        
            AspectJ weaving requires spring-aspects.jar on the classpath,
            as well as load-time weaving (or compile-time weaving) enabled.
        
            Note: The weaving-based aspect requires the @Transactional annotation to be
            defined on the concrete class. Annotations in interfaces will not work
            in that case (they will rather only work with interface-based proxies)!
        ]]>
        </xsd:documentation>
    </xsd:annotation>
    <xsd:simpleType>
        <xsd:restriction base="xsd:string">
            <xsd:enumeration value="proxy"/>
            <xsd:enumeration value="aspectj"/>
        </xsd:restriction>
    </xsd:simpleType>
</xsd:attribute>

 

mode模式默认采用代理模式(mode="proxy"),在proxy模式中采用AOP的动态代理来进行事务管理,是在运行期间基于JDK动态代理或者GCLIB动态代理来实现,在代理模式中(mode="proxy")只有A类中方法调用另外一个B类声明@Transactional的方法才会被代理截获进行事务控制,在A类本类中调用本类的其他带有@Transactional的方法不会经过动态代理。

然而mode="aspectj"则采用静态编织的方式(aspectj是一种编译技术)其需要spring-aspects.jar,是在编译期间或者载入期间对代理对象进行修改字节码织入事务实现,所以不管你的方法的可见度(public、private)是什么都可以实现事务管理,这跟proxy动态代理是完全不同的。

<tx:annotation-driven />,其实是Spring自定义标签的扩展,Spring配置文件中的标签都是基于XML Schema实现 ,spring的实现:http://www.springframework.org/schema/tx/spring-tx-4.1.xsd


 

Propagation Spring事务传播行为

事务传播行为 描述
REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择
SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行
MANDATORY 支持当前事务,如果没有抛出异常
REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起
NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
NEVER 以非事务方式执行,如果当前存在事务,则抛出异常
NESTED 执行在一个嵌套的事务,如果当前事务存在,执行类似REQUIRED操作

 

 

 

 

 

 

 

readOnly 

如果事务是只读的使用该属性,默认他是false

timeout

默认为底层事务系统的默认超时

Isolation

指定事务隔离级别,默认使用数据库底层配置的事务隔离级别

参考资料:

spring2.5开始对事务注解驱动的支持 https://spring.io/blog/2008/01/28/spring-2-5-s-comprehensive-annotation-support

spring4.2.6对事务的介绍  http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#transaction-declarative-annotations 

《Spring3.x企业应用开发实战》

Spring Framework2.5 中文开发参考手册 http://shouce.jb51.net/spring/ 

配套代码:

https://github.com/chenjunlong/spring-transaction-manager

 


 

mysql InnoDB存储引擎锁概述

排它锁    

排它锁又称为写锁((eXclusive lock,简记为X锁)),若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。

在更新操作(INSERT、UPDATE 或 DELETE)过程中始终应用排它锁。

共享锁

共享锁又称为读锁(Share lock,简记为S锁),若事务T对数据对象A加上S锁,则其它事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。


MySQL InnoBD存储引擎行锁的三种算法

Record Lock:单个行记录上的锁

Gap Lock:间隙锁,锁定一个范围,但不包含记录本身

Next-Key Lock:Gap Lock + Record Lock,锁定一个范围,并且锁定记录本身,Record Lock总是会去锁住索引记录,如果表没有设置任何一个索引,会使用隐式的主键进行锁定。

InnoDB存储引擎默认的事务隔离级别REPEATABLE READ,在事务隔离级别下,采用Next-key Locking的方式进行加锁。

而在,READ COMMITTED下,仅采用Record Lock。

然而在 REPEATABLE READ 级别 InnoDB存储引擎下,查询使用含有唯一属性时,会对Next-Key Lock 进行优化,将其降级为Record Lock,即锁住索引本身,而不是一个范围。


一致性非锁定读

一致性的非锁定读是指InnoDB存储引擎通过行多版本控制方式读取当前执行的时间数据库中行的数据。

例如:

    如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此等待行上的锁释放掉,而是去读取该行数据的一个快照行数据。这种非锁定读机制极大的提高了数据库的并发性,这是InnoDB存储引擎默认的读取方式,即读取不会占用等待数据行的锁。

    在事务隔离级别RC和RR对快照数据的定义有所不同,在RC下对于快照数据,非一致性锁定读总是读取被锁定行最新的一条数据快照(这种做法显然不符合事务的隔离性),而在RR下对于快照数据,非一致性锁定读总是读取事务开始时的行数据快照。


 

一致性锁定读,MySQL显示加锁的语句

在某些业务场景下,需要显示的对数据库读取操作进行加锁操作,来保证业务逻辑数据的一致性,这种做法被称为一致性锁定读。

SELECT...FOR UPDATE 为记录加一个X锁。

SELECT...LOCK IN SHARE MODE 为记录加一个S锁。

必须嵌套在事务中,事务结束锁释放


下面主要介绍X锁和显示的加X锁

存储引擎:InnoDB ,事务隔离级别:REPEATABLE-READ

例:

CREATE TABLE `USER` (

  `ID` int(11) NOT NULL AUTO_INCREMENT,

  `MOBILE` varchar(11) DEFAULT NULL,

  `NAME` varchar(255) DEFAULT NULL,

  PRIMARY KEY (`ID`)

) ENGINE=InnoDB AUTO_INCREMENT=322 DEFAULT CHARSET=utf8;

INSERT INTO `USER` VALUES (‘222‘, ‘13121771001‘, ‘test1001‘);

INSERT INTO `USER` VALUES (‘223‘, ‘13121771002‘, ‘test1002‘);

INSERT INTO `USER` VALUES (‘224‘, ‘13121771003‘, ‘test1003‘);

INSERT INTO `USER` VALUES (‘225‘, ‘13121771004‘, ‘test1004‘);

INSERT INTO `USER` VALUES (‘226‘, ‘13121771005‘, ‘test1005‘);

INSERT INTO `USER` VALUES (‘227‘, ‘13121771006‘, ‘test1006‘);

INSERT INTO `USER` VALUES (‘228‘, ‘13121771007‘, ‘test1007‘);

INSERT INTO `USER` VALUES (‘229‘, ‘13121771008‘, ‘test1008‘);

INSERT INTO `USER` VALUES (‘230‘, ‘13121771009‘, ‘test1009‘);

INSERT INTO `USER` VALUES (‘231‘, ‘13121771010‘, ‘test1010‘);


 

一、以主键为查询条件

下面以主键为条件锁住一行数据

会话A 会话B

BEGIN;

SELECT * FROM USER WHERE ID = 222 FOR UPDATE;

重新打开一个查询分析器,查询相同记录,发现行已被锁

 

BEGIN;

SELECT * FROM USER WHERE ID = 222 FOR UPDATE;

技术分享 技术分享
 

查询不相同记录,发现行未被锁,证明是行锁

 

BEGIN;

SELECT * FROM USER WHERE ID = 223 FOR UPDATE;


技术分享

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

结论:以主键为查询条件的为行锁,不会影响其他行,相同行查询会产生等待。

超时异常: 1205 - Lock wait timeout exceeded; try restarting transaction


 

二、以唯一字段为查询条件(未加唯一约束)

会话A 会话B

BEGIN;

SELECT * FROM `USER` WHERE MOBILE = ‘13121771001‘ FOR UPDATE;

重新打开一个查询分析器,查询不相同记录,发现表以被锁

 

BEGIN;

SELECT * FROM `USER` WHERE MOBILE = ‘13121771002‘ FOR UPDATE;

技术分享 技术分享
   

 

 

 

 

 

 

 

 

 

 

结论:以普通列查询,未加唯一约束的情况下,即使返回唯一一行,也会造成全表锁。


 

三、增加唯一索引

ALTER TABLE `USER` ADD UNIQUE INDEX (`MOBILE`);

重新打开查询分析器

 

会话A 会话B

BEGIN;

SELECT * FROM `USER` WHERE MOBILE = ‘13121771001‘ FOR UPDATE

BEGIN;

SELECT * FROM `USER` WHERE MOBILE = ‘13121771002‘ FOR UPDATE

技术分享 技术分享

 

 

 

 

 

 

结论:使用唯一索引和主键是相同效果。


 

四、没有唯一行对INSERT的影响

查询不到唯一行,会导致表锁

会话A 会话B

BEGIN;

SELECT * FROM `USER` WHERE ID = 1 FOR UPDATE;

BEGIN;

INSERT INTO `USER` VALUES (1, ‘13121771000‘, ‘test1000‘);

技术分享 技术分享

 

 

 

 

 

 

 

 

结论:查询不到数据行的情况下,导致insert会失败,这是因为MySQL无法将其视为行级锁,会锁住整表


 

五、存在唯一行对INSERT的影响

会话A 会话B

BEGIN;

SELECT * FROM `USER` WHERE ID = 222 FOR UPDATE;

BEGIN;

INSERT INTO `USER` VALUES (1, ‘13121771000‘, ‘test1000‘);

技术分享 技术分享

 

 

 

 

 

 

 

结论:行锁的情况下,新增数据行不受影响


 

六、没有唯一行对Update的影响

where条件无索引,所有行被锁

 

会话A 会话B

修改该行数据:

技术分享

 

查询其他数据行:

BEGIN;

SELECT * FROM `USER` WHERE ID = 223 FOR UPDATE;

 

UPDATE `USER` SET `NAME` = ‘test_*‘ WHERE `NAME` = ‘test1001‘; 技术分享

 

 

 

 

 

 

 

 

 

 

where字段是唯一索引或者主键只会锁住一行数据

 

会话A 会话B

BEGIN;

UPDATE `USER` SET  `NAME` = ‘demo1‘ WHERE ID = 222;

BEGIN;

SELECT * FROM `USER` WHERE ID = 223 FOR UPDATE;

技术分享

技术分享

 

 

 

 

 

 

 

 

 

在RR级别下InnoDB数据存储引擎行锁的算法默认采用 NEXT-Key Lock,而在RC级别下使用的是 Record Lock,举例说明

 

会话A 会话B
技术分享 技术分享

技术分享

技术分享

技术分享

事务未结束,该阶段没有提交

技术分享

此时会话B,由于RR事务隔离级别的特性,采用Next-Key Lock(前面介绍了,该算法是 Gap Lock + Record Lock) 算法,会锁定一个区间

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

会话A 会话B
技术分享 技术分享
技术分享 技术分享

技术分享

 

事务未结束,该阶段没有提交

技术分享

 

可以正常插入新记录,证明此时RC级别默认采用的是行锁机制

 

COMMIT;

技术分享

 

该操作论证了,RC事务隔离级别下,该事务是中出现不可重复读的现象

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

参考资料:

排它锁 http://baike.baidu.com/link?url=_qX1F30WiR0pLBcXyuLOW4rosho8sZP50aGwKkwoHVRDy1wDH6kTlzR5ov4gT5HbIAb3BS7q2LyjbBYrqHK2-K

共享锁 http://baike.baidu.com/link?url=iXk1XXZUbukRI9TkY_frxzjLfI4U7ZX5Oc_ThdLz79WiRrzwa1mYyt2Wdn9naObSy_z-mnGXn1-jhxvDhjLLiq

《MySQL技术内幕InnoDB存储引擎第2版》

 

以上是关于Spring事务管理实现原理及MySQL InnoBD引擎行锁概述的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot事务的使用(编程式声明式)及 @Transactional 工作原理失效处理

Mysql事务隔离级别及ACID实现原理

Spring嵌套事务原理

Spring Boot -- Spring AOP原理及简单实现

JDBC与Spring事务及事务传播性原理解析-上篇

JDBC与Spring事务及事务传播性原理解析-上篇