Spring 事务传播机制 实例讲解
- 对于SQL事务的概念以及ACID性质,可以参见我的另一篇博文 http://kingj.iteye.com/admin/blogs/1675011
- spring的管理的事务可以分为如下2类:
- 逻辑事务 在spring中定义的事务通常指逻辑事务,提供比物理事务更抽象,方便的事务配置管理,但也基于物理事务
- 物理事务 特定于数据库的事务
- spring中支持一下2中事务声明方式
- 编程式事务 当系统需要明确的,细粒度的控制各个事务的边界,应选择编程式事务
- 声明式事务 当系统对于事务的控制粒度较粗时,应该选择申明式事务
- 无论你选择上述何种事务方式去实现事务控制,spring都提供基于门面设计模式的事务管理器供选择,如下是spring事务中支持的事务管理器
事务管理器实现(org.springframework.*) 使用时机 jdbc.datasource.DataSourceTransactionManager 使用jdbc的抽象以及ibatis支持 orm.hibernate.HibernateTransactionManager 使用hibernate支持(默认3.0以下版本) orm.hibernate3.HibernateTransactionManager 使用hibernate3支持 transaction.jta.JtaTransactionManager 使用分布式事务(分布式数据库支持) orm.jpa.JpaTransactionManager 使用jpa做为持久化工具 orm.toplink.TopLinkTransactionManager 使用TopLink持久化工具 orm.jdo.JdoTransactionManager 使用Jdo持久化工具 jms.connection.JmsTransactionManager 使用JMS 1.1+ jms.connection.JmsTransactionManager102 使用JMS 1.0.2 transaction.jta.OC4JJtaTransactionManager 使用oracle的OC4J JEE容器 transaction.jta.WebLogicJtaTransactionManager 在weblogic中使用分布式数据库 jca.cci.connection.CciLocalTransactionManager 使用jrping对J2EE Connector Architecture (JCA)和Common Client Interface (CCI)的支持
UML结构图如下
4、各种事务管理器的定义如下
- JdbcTransactionManager定义如下
- Xml代码
- <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
- <property name="dataSource" ref="dataSource"/>
- </bean>
- hibernate事务管理器配置如下
- Xml代码
- <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
- <property name="sessionFactory" ref="sessionFactory"/><span style="white-space: pre;"> </span>
- </bean>
- hibernate的事务管理器会注入session会话工厂,然后将事务处理委托给当前的transaction对象,事务提交时,调用commit()方法,回滚时调用rollback()方法
- jpa事务管理器配置如下
- Xml代码
- <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
- <span style="white-space: pre;"> </span><property name="entityManagerFactory" ref="entityManagerFactory"/>
- </bean>
- 其余事务管理器可参见spring in action中说明
- 5、申明式事务配置
- spring特有的事务传播行为,spring支持7种事务传播行为,确定客户端和被调用端的事务边界(说得通俗一点就是多个具有事务控制的service的相互调用时所形成的复杂的事务边界控制)下图所示为7钟事务传播机制
传播行为 含义 PROPAGATION_REQUIRED(XML文件中为REQUIRED) 表示当前方法必须在一个具有事务的上下文中运行,如有客户端有事务在进行,那么被调用端将在该事务中运行,否则的话重新开启一个事务。(如果被调用端发生异常,那么调用端和被调用端事务都将回滚) PROPAGATION_SUPPORTS(XML文件中为SUPPORTS) 表示当前方法不必需要具有一个事务上下文,但是如果有一个事务的话,它也可以在这个事务中运行 PROPAGATION_MANDATORY(XML文件中为MANDATORY) 表示当前方法必须在一个事务中运行,如果没有事务,将抛出异常 PROPAGATION_NESTED(XML文件中为NESTED) 表示如果当前方法正有一个事务在运行中,则该方法应该运行在一个嵌套事务中,被嵌套的事务可以独立于被封装的事务中进行提交或者回滚。如果封装事务存在,并且外层事务抛出异常回滚,那么内层事务必须回滚,反之,内层事务并不影响外层事务。如果封装事务不存在,则同PROPAGATION_REQUIRED的一样 PROPAGATION_NEVER(XML文件中为NEVER) 表示当方法务不应该在一个事务中运行,如果存在一个事务,则抛出异常 PROPAGATION_REQUIRES_NEW(XML文件中为REQUIRES_NEW) 表示当前方法必须运行在它自己的事务中。一个新的事务将启动,而且如果有一个现有的事务在运行的话,则这个方法将在运行期被挂起,直到新的事务提交或者回滚才恢复执行。 PROPAGATION_NOT_SUPPORTED(XML文件中为NOT_SUPPORTED) 表示该方法不应该在一个事务中运行。如果有一个事务正在运行,他将在运行期被挂起,直到这个事务提交或者回滚才恢复执行
- 6、spring中的事务隔离级别
- spring的事务隔离级别其实本质上是对SQL92标准的4种事务隔离级别的一种封装,具体参加我的博文:http://kingj.iteye.com/admin/blogs/1675011
- spring的事务隔离级别如下表所示
隔离级别 含义 ISOLATION_DEFAULT 使用数据库默认的事务隔离级别 ISOLATION_READ_UNCOMMITTED 允许读取尚未提交的修改,可能导致脏读、幻读和不可重复读 ISOLATION_READ_COMMITTED 允许从已经提交的事务读取,可防止脏读、但幻读,不可重复读仍然有可能发生 ISOLATION_REPEATABLE_READ 对相同字段的多次读取的结果是一致的,除非数据被当前事务自生修改。可防止脏读和不可重复读,但幻读仍有可能发生 ISOLATION_SERIALIZABLE 完全服从ACID隔离原则,确保不发生脏读、不可重复读、和幻读,但执行效率最低。
- 7、spring事务只读属性
- spring事务只读的含义是指,如果后端数据库发现当前事务为只读事务,那么就会进行一系列的优化措施。它是在后端数据库进行实施的,因此,只有对于那些有可能启动一个新事务的传播行为(REQUIRED,REQUIRES_NEW,NESTED)的方法来说,才有意义。(测试表明,当使用JDBC事务管理器并设置当前事务为只读时,并不能发生预期的效果,即能执行删除,更新,插入操作)
- 8、spring的事务超时
- 有的时候为了系统中关键部分的性能问题,它的事务执行时间应该尽可能的短。因此可以给这些事务设置超时时间,以秒为单位。我们知道事务的开始往往都会发生数据库的表锁或者被数据库优化为行锁,如果允许时间过长,那么这些数据会一直被锁定,影响系统的并发性。
- 因为超时时钟是在事务开始的时候启动,因此只有对于那些有可能启动新事物的传播行为(REQUIRED,REQUIRES_NEW,NESTED)的方法来说,事务超时才有意义。
- 9、事务回滚规则
- spring中可以指定当方法执行并抛出异常的时候,哪些异常回滚事务,哪些异常不回滚事务。
- 默认情况下,只在方法抛出运行时异常的时候才回滚(runtime exception)。而在出现受阻异常(checked exception)时不回滚事务,这个ejb的回滚行为一致。
- 当然可以采用申明的方式指定哪些受阻异常像运行时异常那样指定事务回滚。
- 10、spring申明式事务配置
- 将aop,tx命名空间添加到当前的spring配置文件头中
- 定义一个事务AOP通知
- Xml代码
- <tx:advice id="txAdvice" transactionManager="transactionManager">
- <tx:attributes>
- <tx:method name="add*" propagation="REQUIRED"/>
- </tx:attributes>
- </tx:advice><span style="white-space: pre;"> </span>
- 定义一个事务切面,即应该在哪些类的哪些方法上面进行事务切入
- Xml代码
- <aop:config>
- <aop:advisor pointcut="execution(* *..zx.spring.UserService*.*(..))||execution(* *..spring.ServiceFacade.*(..))||execution(* *..spring.BookService.*(..))" advice-ref="txAdvice"/>
- </aop:config>
- 11、结合具体的代码实现讲解spring的7种事务传播机制效果
- 准备环境
- 我们采用JDBC+ORACLE实现具体操作,首先搭建好spring的开发环境,配置好数据源,建立好Test
- 创建数据库结构,创建一个用户表和Book表
- Sql代码
- CREATE TABLE T_USER(
- ID INT,
- NAME VARCHAR2(200)
- );
- CREATE TABLE T_BOOK(
- ID INT ,
- NAME VARCHAR2(200)
- );
- 搭建好的结构如下
- Xml代码
- <!-- 定义数据源 -->
- <bean id="ams" class="com.mchange.v2.c3p0.ComboPooledDataSource"
- destroy-method="close">
- <property name="driverClass" value="${jdbc.ams.driver}" />
- <property name="jdbcUrl" value="${jdbc.ams.url}" />
- <property name="user" value="${jdbc.ams.username}" />
- <property name="password" value="${jdbc.ams.password}" />
- <property name="initialPoolSize" value="${initialSize}" />
- <property name="minPoolSize" value="${minPoolSize}" />
- <property name="maxPoolSize" value="${maxActive}" />
- <property name="acquireIncrement" value="${acquireIncrement}" />
- <property name="maxIdleTime" value="${maxIdleTime}" />
- </bean>
- <!-- 定义jdbc模板类-->
- <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
- <property name="dataSource" ref="ams"/>
- </bean>
- 在spring包下面建立3个service
- BookService
- Java代码
- package com.zx.spring;
- import org.springframework.jdbc.core.JdbcTemplate;
- public class BookService {
- public static final String ADD_BOOK="insert into t_book(id,name) values(1,'duck-j2ee')";
- private JdbcTemplate jdbcTemplate;
- public void addBook() throws Exception{
- this.jdbcTemplate.execute(ADD_BOOK);
- throw new RollbackException("跳出执行");
- }
- public JdbcTemplate getJdbcTemplate() {
- return jdbcTemplate;
- }
- public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
- this.jdbcTemplate = jdbcTemplate;
- }
- }
- UserService
- Java代码
- package com.zx.spring;
- import org.springframework.jdbc.core.JdbcTemplate;
- public class UserService {
- public static final String ADD_USER="insert into t_user(id,name) values(1,'duck')";
- private BookService bs;
- private JdbcTemplate jdbcTemplate;
- public void addUser()throws Exception {
- this.bs.addBook();
- this.jdbcTemplate.execute(ADD_USER);
- }
- public JdbcTemplate getJdbcTemplate() {
- return jdbcTemplate;
- }
- public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
- this.jdbcTemplate = jdbcTemplate;
- }
- public BookService getBs() {
- return bs;
- }
- public void setBs(BookService bs) {
- this.bs = bs;
- }
- }
- 创建一个ServiceFacade门面,将UserService和BookService包装起来
- Java代码
- package com.zx.spring;
- public class ServiceFacade {
- private BookService bs;
- private UserService us;
- public BookService getBs() {
- return bs;
- }
- public void setBs(BookService bs) {
- this.bs = bs;
- }
- public UserService getUs() {
- return us;
- }
- public void setUs(UserService us) {
- this.us = us;
- }
- public void addUserBook()throws Exception{
- bs.addBook();
- us.addUser();
- }
- }
- 上面我们配置了3个service接口,并在spring中配置了申明式事务。
- Xml代码
- <aop:config>
- <aop:advisor pointcut="execution(* *..zx.spring.UserService*.*(..))||execution(* *..spring.ServiceFacade.*(..))||execution(* *..spring.BookService.*(..))" advice-ref="txAdvice"/>
- </aop:config>
- 我们在一个pointcut中定义了3个aspectj方式的切入点,即对这3个类的所有方法进行事务切入。
接下来我们开始配置不同的事务传播机制,来看看效果。 - 到此,准备工作大功告成,接下来我们来对7中传播机制做一个详细解释。
- 一 :我们来对单个方法的事务传播机制进行一个了解
- REQUIRED, REQUIRES_NEW
- junit代码
- Java代码
- @Test
- public void testAddBook()throws Exception{
- BookService bs=(BookService)this.getBean("bookService");
- bs.addBook();
- }
- addBook()代码
- Java代码
- public void addBook() throws Exception{
- this.jdbcTemplate.execute(ADD_BOOK);
- throw new RuntimeException("throw runtime exception in outter transaction");
- }
- 执行junit后,控制台如下
- Java代码
- 1、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_book(id,name) values(1,'duck-j2ee')]
- 2、[DEBUG,DataSourceTransactionManager,main] Initiating transaction rollback
- 3、[DEBUG,DataSourceTransactionManager,main] Rolling back JDBC transaction on Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@a2b392]
- 可以知道,当addBook()方法的事务传播机制为REQUIRED, REQUIRES_NEW ,并且抛出运行时异常时,将会回滚事务。
- 当addBook()方法抛出受检查的异常时,将不会回滚事务。
- addBook()方法如下:
- Java代码
- public void addBook() throws Exception{
- this.jdbcTemplate.execute(ADD_BOOK);
- throw new Exception("throw runtime exception in outter transaction");
- }
- Java代码
- 1、[DEBUG,DataSourceTransactionManager,main] Creating new transaction with name [com.zx.spring.BookService.addBook]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
- 2、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_book(id,name) values(1,'duck-j2ee')]
- 3、[DEBUG,DataSourceTransactionManager,main] Initiating transaction commit
- 4、[DEBUG,DataSourceTransactionManager,main] Committing JDBC transaction on Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@4310d0]
- 第3行可知,事务提交,不回滚
- MANDATORY
- 代码同上所示,只是事务传播机制改为 MANDATORY
- Xml代码
- <tx:advice id="txAdvice" transaction-manager="txManager">
- <tx:attributes>
- <tx:method name="addUser" propagation="REQUIRED" />
- <tx:method name="addBook" propagation="MANDATORY"/>
- </tx:attributes>
- </tx:advice>
- 运行junit后,由于单个方法执行没有指定任何事务传播机制,因此抛出异常。
- Java代码
- org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
- at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:357)
- NESTED
- NESTED内嵌事务,如果没有外层事务,则新建一个事务,行为同REQUIRED一样
- NEVER
- NEVER不会以事务方式运行,执行junit代码后,控制台如下
- Java代码
- 1、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_book(id,name) values(1,'duck-j2ee')]
- 2、[DEBUG,DataSourceUtils,main] Registering transaction synchronization for JDBC Connection
- 3、[DEBUG,DataSourceTransactionManager,main] Should roll back transaction but cannot - no transaction available
- 由于发生运行时异常,事务本应该回滚,但是在第三行可以知道,由于事务传播机制为NEVER,因此找不到事务进行回滚,数据库只添加了一条记录。
- SUPPORTS
- 单个方法 调用时supports行为同NEVER一样,不会创建事务,只是如果有事务,则会加入到当前事务中去,具体的行为下面有分析。
- NOT_SUPPORTED
- 单个方法被执行时,不会创建事务。如果当前有事务,将封装事务挂起,知道该方法执行完成再恢复封装事务,继续执行。
- REQUIRED, REQUIRES_NEW
- 1、我们将UserService和BookService中的addUser(),addBook()方法分别配置为REQUIRED,并在addBook()方法中抛出运行时异常。
- Xml代码
- <tx:advice id="txAdvice" transaction-manager="txManager">
- <tx:attributes>
- <tx:method name="addUser" propagation="REQUIRED"/>
- <tx:method name="addBook" propagation="REQUIRED"/>
- </tx:attributes>
- </tx:advice>
Xml代码- Java代码
- 1、public void addUser()throws Exception {
- 2、 this.bs.addBook();
- 3、 this.jdbcTemplate.execute(ADD_USER);
- }
- Java代码
- 4、public void addBook() throws Exception{
- 5、 this.jdbcTemplate.execute(ADD_BOOK);
- 6、 throw new RuntimeException("跳出执行");
- }
- 测试用例如下
- Java代码
- @Test
- public void testAddUser()throws Exception{
- UserService us=(UserService)this.getBean("userService");
- us.addUser();
- }
- 执行前数据库如下
- 执行后,console控制台输出如下:
- Java代码
- [DEBUG,DataSourceTransactionManager,main]
- Creating new transaction with name [com.zx.spring.UserService.addUser]:
Java代码- PROPAGATION_REQUIRED,ISOLATION_DEFAULT
上面输出可以知道,spring自动给addUser()方法切入了事务,事务隔离级别为数据库默认级别。
- 我们再观察控制台输出
- Java代码
- 1、[DEBUG,DataSourceTransactionManager,main] Acquired Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@1973fc5] for JDBC transaction
- 2、[DEBUG,DataSourceTransactionManager,main] Switching JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@1973fc5] to manual commit
- 3、[DEBUG,DataSourceTransactionManager,main] Participating in existing transaction
- 4、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_book(id,name) values(1,'duck-j2ee')]
- 5、[DEBUG,DataSourceTransactionManager,main] Participating transaction failed - marking existing transaction as rollback-only
- 6、[DEBUG,DataSourceTransactionManager,main] Setting JDBC transaction [com.mchange.v2.c3p0.impl.NewProxyConnection@1973fc5] rollback-only
- 7、[DEBUG,DataSourceTransactionManager,main] Initiating transaction rollback
- 8、[DEBUG,DataSourceTransactionManager,main] Rolling back JDBC transaction on Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@1973fc5]
- 第一行jdbc事务管理器从c3p0连接池中获取一个链接
- 第二行设置jdbc事务提交方式为手动提交
- 代码执行到方法addUser()中第一行时,由于addUser()方法的事务级别为REQUIRED的因此,事务管理器开始了一个事务。执行到第二行addBook()时,由于addBook()方法的事务传播行为为REQUIRED的,我们知道REQUIRED方式是如果有一个事务,则加入事务中,如果没有,则新建一个事务。由控制台输出的第3行可以知道,addBook方法加入到了addUser方法的事务当中去,接着第4行执行了插入t_book语句,由于addBook()方法在第6行时,抛出了运行时异常,因此当前事务失败,可从控制台输出第5行得知。
- 由于addUser()和addBook()方法共享了一个事务,在addBook()方法中又抛出了运行时异常,因此事务必须回滚,这由数据库查询可知。
- 如果我们将addBook()方法中抛出的运行时异常改为checked异常的话,会是什么结果呢?
- Java代码
- 7、public void addBook() throws Exception{
- 8、 this.jdbcTemplate.execute(ADD_BOOK);
- 9、 throw new Exception("跳出执行");
- }
- Java代码
- 9、[DEBUG,DataSourceTransactionManager,main] Acquired Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@1973fc5] for JDBC transaction
- 10、[DEBUG,DataSourceTransactionManager,main] Switching JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@1973fc5] to manual commit
- 11、[DEBUG,DataSourceTransactionManager,main] Participating in existing transaction
- 12、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_book(id,name) values(1,'duck-j2ee')]
- 13、[DEBUG,DataSourceTransactionManager,main] Initiating transaction commit
- 14、[DEBUG,DataSourceTransactionManager,main] Releasing JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@457d21] after transaction
- 由控制台输出第13行可以知道,除了addBook()方法中抛出的检查异常被忽略之外,其它的同上面的一致。再看数据库可以知道,addBook()方法和被执行了,addUser()方法被抛出的检查异常终止调用。
- 如果我们给addUser()方法指定rollback-for属性,那么addBook()方法的事务回回滚吗?
- Java代码
- public void addBook() throws Exception{
- this.jdbcTemplate.execute(ADD_BOOK);
- throw new Exception("跳出执行");
- }
- Java代码
- <tx:advice id="txAdvice" transaction-manager="txManager">
- <tx:attributes>
- <tx:method name="addUser" propagation="REQUIRED" <span style="color: #ff0000;"><span style="color: #888888;">rollback-for="Exception"</span></span>/>
- <tx:method name="addBook" propagation="REQUIRED"/>
- </tx:attributes>
- </tx:advice>
- 控制台输出如下
- Java代码
- 15、[DEBUG,DataSourceTransactionManager,main] Participating in existing transaction
- 16、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_book(id,name) values(1,'duck-j2ee')]
- 17、[DEBUG,DataSourceTransactionManager,main] Initiating transaction rollback
- 18、[DEBUG,DataSourceTransactionManager,main] Rolling back JDBC transaction on Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@121d383]
可见,当指定了rollback-for属性时,只要抛出了指定的异常,事务就会回滚。
- 2、上面我们讨论了两个方法都指定为事务传播机制为REQUIRED,那么我们来改变以下addBook()方的事务传播机制改为NEVER ,来看看它们的效果
- Xml代码
- <tx:advice id="txAdvice" transaction-manager="txManager">
- <tx:attributes>
- <tx:method name="addUser" propagation="REQUIRED" />
- <tx:method name="addBook" propagation="NEVER"/>
- </tx:attributes>
- </tx:advice>
执行Junit测试后发现,控制台输出如下:
- Java代码
- org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'
- at org.springframework.transaction.support.AbstractPlatformTransactionManager.handleExistingTransaction(AbstractPlatformTransactionManager.java:399)
- at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:347)
- at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:335)
Existing transaction found for transaction marked with propagation 'never',也就是说addBook不应该在事务中运行,但是addUser这个客户端调用者却有一个事务,因此报错。
- 3、我们接着将addBook()方法的事务传播机制改为MANDATORY
- Xml代码
- <tx:advice id="txAdvice" transaction-manager="txManager">
- <tx:attributes>
- <tx:method name="addUser" propagation="REQUIRED" />
- <tx:method name="addBook" propagation="MANDATORY"/>
- </tx:attributes>
- </tx:advice>
执行junit后,控制台输出如下
- Java代码
- [DEBUG,DataSourceTransactionManager,main] Participating in existing transaction
- [DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_book(id,name) values(1,'duck-j2ee')]
- [DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_user(id,name) values(1,'duck')]
- [DEBUG,DataSourceTransactionManager,main] Initiating transaction commit
可以知道当前和REQUIRED一样的传播行为。
- 4、我们将addBook()方法的事务传播机制改为NESTED-内嵌事务,那么传播机制之间会怎么互相影响呢?
- Xml代码
- <tx:advice id="txAdvice" transaction-manager="txManager">
- <tx:attributes>
- <tx:method name="addUser" propagation="REQUIRED" />
- <tx:method name="addBook" propagation="NESTED"/>
- </tx:attributes>
- </tx:advice>
Xml代码- addBook()方法如下,依然抛出(checked-exception)检查异常。
Java代码- public void addBook() throws Exception{
- this.jdbcTemplate.execute(ADD_BOOK);
- throw new RuntimeException("跳出执行");
- }
Java代码- addUser()方法如下,在方法体中捕获addBook()抛出的异常。如果不捕获异常,addUser()方法将会被终止。
Java代码- public void addUser()throws Exception {
- try {
- this.bs.addBook();
- }catch(Exception e) {
- e.printStackTrace();
- }
- this.jdbcTemplate.execute(ADD_USER);
- }
执行junit后,控制台输出如下
- Java代码
- 1、[DEBUG,DataSourceTransactionManager,main] Creating new transaction with name [com.zx.spring.UserService.addUser]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
- 2、[DEBUG,DataSourceTransactionManager,main] Creating nested transaction with name [com.zx.spring.BookService.addBook]
- 3、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_book(id,name) values(1,'duck-j2ee')]
- 4、[DEBUG,DataSourceTransactionManager,main] Rolling back transaction to savepoint
- 5、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_user(id,name) values(1,'duck')]
- 6、[DEBUG,DataSourceTransactionManager,main] Initiating transaction commit
- 7、[DEBUG,DataSourceTransactionManager,main] Committing JDBC transaction on Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@10e164e]
- 由上面的输出可以,第一行为执行addUser()方法,事务管理器开始了一个事务,
- 第二行执行到addBook()方法,由于addBook()方法的事务传播机制为NESTED内嵌事务,因此,开始一个新的事务。
- 第三行可以知道插入语句,由于addBook()方法内部抛出RuntimeException,因此内部嵌套事务回滚到外层事务创建的保存点。
- 注意这个地方,我们抛出的是运行时异常,如果我们抛出受检查的异常,那么spring会默认的忽略此异常。下面我会详细阐述。
- 如果内层事务抛出检查异常,那么外层事务将忽略此异常,但是会产生一个问题。那就是:外层事务使用jdbc的保存点API来实现嵌套事务,
- 但是数据库不一定支持。我做测试的是oracle数据库,jdbc事务管理器在内层事务抛出检查异常后,将会在内层事务结束后,释放外层事务
- 创建的保存点,这是时候数据库不一定支持。因此可能会抛出如下异常:
- Java代码
- java.sql.SQLException: 不支持的特性
- at oracle.jdbc.driver.DatabaseError.throwSqlException(DatabaseError.java:112)
- at oracle.jdbc.driver.DatabaseError.throwSqlException(DatabaseError.java:146)
- at oracle.jdbc.driver.DatabaseError.throwSqlException(DatabaseError.java:208)
- 第五行可以知道,外层事务开始执行,第六行可知外层事务提交。
- 总结可知:对于NESTED内层事务而言,内层事务独立于外层事务,可以独立递交或者回滚。
- 如果我们在addUser方法内部抛出一个运行时异常,那么会怎么样呢?
- Java代码
- public void addUser()throws Exception {
- this.bs.addBook();
- this.jdbcTemplate.execute(ADD_USER);
- throw new RuntimeException("throw runtime exception in outter transaction");
- }
- Java代码
- public void addBook() throws Exception{
- this.jdbcTemplate.execute(ADD_BOOK);
- }
- 执行junit后,控制台输入如下
- Java代码
- 1、[DEBUG,DataSourceTransactionManager,main] Creating new transaction with name [com.zx.spring.UserService.addUser]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
- 2、[DEBUG,DataSourceTransactionManager,main] Creating nested transaction with name [com.zx.spring.BookService.addBook]
- 3、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_book(id,name) values(1,'duck-j2ee')]
- 4、[DEBUG,DataSourceTransactionManager,main] Releasing transaction savepoint
- 5、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_user(id,name) values(1,'duck')]
- 6、[DEBUG,DataSourceTransactionManager,main] Initiating transaction rollback
- 7、[DEBUG,DataSourceTransactionManager,main] Rolling back JDBC transaction on Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@1622a94]
- 第一行在addUser()方法执行后,事务管理器创建一个新的事务。
- 第二上和上面一样,由于addBook()方法是NETSTED的内嵌传播机制,因此新建一个事务。
- 执行插入,释放保存点。
- 执行插入t_user插入,但是此时抛出了一个运行时异常,外层事务回滚,那么内层事务是否回滚呢?我们看以下数据库记录。
- t_user表数据为空,事务回滚了
t_book表数据也为空,证明内层事务回滚了 - 由上述结果可知,如果对于一个内嵌事务来说,外层事务的回滚必将导致内层事务回滚。
- 5、我们再将addBook()方法的事务传播机制该为REQUIRES_NEW,来看看会有什么有趣的事情发生?
- Xml代码
- <tx:advice id="txAdvice" transaction-manager="txManager">
- <tx:attributes>
- <tx:method name="addUser" propagation="REQUIRED" />
- <tx:method name="addBook" propagation="REQUIRES_NEW"/>
- </tx:attributes>
- </tx:advice>
- Java代码
- public void addUser()throws Exception {
- this.bs.addBook();
- this.jdbcTemplate.execute(ADD_USER);
- }
- Java代码
- public void addBook() throws Exception{
- this.jdbcTemplate.execute(ADD_BOOK);
- //throw new RuntimeException("throw runtime exception in outter transaction");
- }
- 执行junit后,控制台输出如下
- Java代码
- 1、[DEBUG,DataSourceTransactionManager,main] Creating new transaction with name [com.zx.spring.UserService.addUser]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
- 2、[DEBUG,DataSourceTransactionManager,main] Suspending current transaction, creating new transaction with name [com.zx.spring.BookService.addBook]
- 3、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_book(id,name) values(1,'duck-j2ee')]
- 4、[DEBUG,DataSourceTransactionManager,main] Initiating transaction commit
- 5、[DEBUG,DataSourceTransactionManager,main] Committing JDBC transaction on Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@5b96c2]
- 由上可知,第一行执行addUser()方法创建一个事务,
- 第二行阻塞addUser()方法,并创建一个新的事务,执行插入t_book表,提交内层事务和外层事务。
- 或许有的读者会问,在下面addUser()方法中由于第一行和第二行是顺序执行,因此不能说明说明问题,那么我们将addUser()方法中的1、2行代码调换,在看效果:
- Java代码
- public void addUser()throws Exception {
- 1、 this.bs.addBook();
- 2、 this.jdbcTemplate.execute(ADD_USER);
- }
- 兑换后的代码
- Java代码
- public void addUser()throws Exception {
- this.jdbcTemplate.execute(ADD_USER);
- this.bs.addBook();
- }
- 在来看控制台输出
- Java代码
- 1、[DEBUG,DataSourceTransactionManager,main] Creating new transaction with name [com.zx.spring.UserService.addUser]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
- 2、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_user(id,name) values(1,'duck')]
- 3、[DEBUG,DataSourceTransactionManager,main] Suspending current transaction, creating new transaction with name [com.zx.spring.BookService.addBook]
- 4、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_book(id,name) values(1,'duck-j2ee')]
- 5、[DEBUG,DataSourceTransactionManager,main] Initiating transaction commit
- 6、[DEBUG,DataSourceTransactionManager,main] Committing JDBC transaction on Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@1b7ae22]
- 7、[DEBUG,DataSourceTransactionManager,main] Resuming suspended transaction after completion of inner transaction
- 8、[DEBUG,DataSourceTransactionManager,main] Initiating transaction commit
- 9、[DEBUG,DataSourceTransactionManager,main] Committing JDBC transaction on Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@1fa681c]
- 由第一、二行可知道正在执行插入t_user表操作,而到第3行中我们可以知道,插入t_user表的事务被挂起,并且新建了一个事务来插入t_book表
- t_book表插入事务提交后,到第7行可知,前一个事务t_user插入操作被恢复,并提交前一个操作。
- 如果我们在addBook()方法中抛出运行时异常,来看看会有什么有趣的事情发生?
- addBook()方法代码如下
- Java代码
- public void addBook() throws Exception{
- this.jdbcTemplate.execute(ADD_BOOK);
- throw new RuntimeException("throw runtime exception in outter transaction");
- }
- addUser()方法代码如下
- Java代码
- public void addUser()throws Exception {
- this.jdbcTemplate.execute(ADD_USER);
- this.bs.addBook();
- }
- 执行junit后,控制台输出如下
- Java代码
- 1、[DEBUG,DataSourceTransactionManager,main] Creating new transaction with name [com.zx.spring.UserService.addUser]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
- 2、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_user(id,name) values(1,'duck')]
- 3、[DEBUG,DataSourceTransactionManager,main] Suspending current transaction, creating new transaction with name [com.zx.spring.BookService.addBook]
- 4、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_book(id,name) values(1,'duck-j2ee')]
- 5、[DEBUG,DataSourceTransactionManager,main] Initiating transaction rollback
- 6、[DEBUG,DataSourceTransactionManager,main] Rolling back JDBC transaction on Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@10d0b72]
- 7、[DEBUG,DataSourceTransactionManager,main] Releasing JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@10d0b72] after transaction
- 8、[DEBUG,DataSourceTransactionManager,main] Resuming suspended transaction after completion of inner transaction8、
- 9、[DEBUG,DataSourceTransactionManager,main] Initiating transaction rollback
- 10、[DEBUG,DataSourceTransactionManager,main] Rolling back JDBC transaction on Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@44b361]
- 11、[DEBUG,DataSourceTransactionManager,main] Releasing JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@44b361] after transaction
- 第一行可知,执行addUser()方法时,从链接池获取一个新链接,创建一个封装事务,执行t_user表插入。
- 第三行可知,t_user插入事务被挂起,一直到第7行,插入t_book表事务被回滚
- 第8行可知,t_user事务恢复,但是此时该封装事务被回滚。我们再看数据库.
- t_user表数据和t_book表数据均为空
- 由此我们可以知道,对于REQUIRES_NEW事务传播机制,如果被调用端抛出运行时异常,则被调用端事务回滚,那么调用端的事务到底是回滚还是提交呢?
- 如果调用段代码捕获了被调用端抛出的运行时异常,那么调用端事务提交,不回滚
- 我们将addUser()调用端代码该成如下(捕获addBook()抛出的运行时异常)
- Java代码
- public void addUser()throws Exception {
- this.jdbcTemplate.execute(ADD_USER);
- try {
- this.bs.addBook();
- }catch(Exception e) {
- e.printStackTrace();
- }
- }
- 执行junit后,控制台信息如下
- Java代码
- 1、[DEBUG,DataSourceTransactionManager,main] Creating new transaction with name [com.zx.spring.UserService.addUser]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
- 2、[DEBUG,DataSourceTransactionManager,main] Acquired Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@1bcec19] for JDBC transaction
- 3、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_user(id,name) values(1,'duck')]
- 4、[DEBUG,DataSourceTransactionManager,main] Suspending current transaction, creating new transaction with name [com.zx.spring.BookService.addBook]
- 5、[DEBUG,DataSourceTransactionManager,main] Acquired Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@ba507b] for JDBC transaction
- 6、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_book(id,name) values(1,'duck-j2ee')]
- 7、[DEBUG,DataSourceTransactionManager,main] Initiating transaction rollback
- 8、[DEBUG,DataSourceTransactionManager,main] Rolling back JDBC transaction on Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@ba507b]
- 9、[DEBUG,DataSourceTransactionManager,main] Resuming suspended transaction after completion of inner transaction
- 10、java.lang.RuntimeException: throw runtime exception in outter transaction
- at com.zx.spring.BookService.addBook(BookService.java:11)
- 11、[DEBUG,DataSourceTransactionManager,main] Initiating transaction commit
- 12、[DEBUG,DataSourceTransactionManager,main] Committing JDBC transaction on Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@1bcec19]
- 由上面的输出可以知道,1-3行从连接池获取一个链接,开始执行插入事务
- 执行addBook()方法时,因其事务传播属性为REQUIRES_NEW,则将上一个事务阻塞
- 第6-8行可知,addBook()方法抛出运行时异常,新事务被回滚
- 第9行恢复执行上一个插入t_user表事务,并捕获到addBook()抛出的异常,自此addUser()方法未抛出任何运行时异常,提交事务。
- 如果调用端未捕获被调用端抛出的运行时异常,那么调用端事务回滚,不提交
- 我们将addUser()方法调用端改成如下(不捕获addBook()抛出的运行时异常,直接抛出)
- Java代码
- public void addUser()throws Exception {
- this.jdbcTemplate.execute(ADD_USER);
- this.bs.addBook();
- }
- 执行junit后,控制台输出如下:
- Java代码
- 1、[DEBUG,DataSourceTransactionManager,main] Creating new transaction with name [com.zx.spring.UserService.addUser]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
- 2、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_user(id,name) values(1,'duck')]
- 3、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_book(id,name) values(1,'duck-j2ee')]
- 4、[DEBUG,DataSourceTransactionManager,main] Initiating transaction rollback
- 5、[DEBUG,DataSourceTransactionManager,main] Rolling back JDBC transaction on Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@63a721]
- 6、[DEBUG,DataSourceTransactionManager,main] Resuming suspended transaction after completion of inner transaction
- 7、[DEBUG,DataSourceTransactionManager,main] Initiating transaction rollback
- 8、[DEBUG,DataSourceTransactionManager,main] Rolling back JDBC transaction on Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@1706da8]
- 由上述第1-5行可知,插入t_user表事务被挂起,同时插入t_book事务被回滚,因为抛出了运行时异常。
- 6-8行插入t_user事务被回滚,因为addUser()方法的事务传播界别为REQUIRED,因此在抛出了运行时异常的情况下,会回滚事务。
- 那么,为什么会造成上面两种截然不同的结果呢?因为addUser()方法被声明为REQUIRED传播机制,只要它抛出运行时异常,均会回滚事务。
- 如果调用段代码捕获了被调用端抛出的运行时异常,那么调用端事务提交,不回滚
- 6、 我们再将addBook()方法的事务传播机制该为SUPPORTS,来看看会有什么有趣的事情发生?
- 将addBook()方法的事务机制改为SUPPORTS
- Xml代码
- <tx:advice id="txAdvice" transaction-manager="txManager">
- <tx:attributes>
- <tx:method name="addUser" propagation="REQUIRED" />
- <tx:method name="addBook" propagation="SUPPORTS"/>
- </tx:attributes>
- </tx:advice>
- addUser()方法
- Java代码
- public void addUser()throws Exception {
- this.bs.addBook();
- this.jdbcTemplate.execute(ADD_USER);
- }
- addBook()方法
- Java代码
- public void addBook() throws Exception{
- this.jdbcTemplate.execute(ADD_BOOK);
- throw new RuntimeException("throw runtime exception in outter transaction");
- }
- 执行junit后,控制台输出如下
- Java代码
- 1、[DEBUG,DataSourceTransactionManager,main] Creating new transaction with name [com.zx.spring.UserService.addUser]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
- 2、[DEBUG,DataSourceTransactionManager,main] Participating in existing transaction
- 3、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_book(id,name) values(1,'duck-j2ee')]
Java代码- 4、[DEBUG,DataSourceTransactionManager,main] Participating transaction failed - marking existing transaction as rollback-only
- 5、[DEBUG,DataSourceTransactionManager,main] Setting JDBC transaction [com.mchange.v2.c3p0.impl.NewProxyConnection@19e09a4] rollback-only
- 6、[DEBUG,DataSourceTransactionManager,main] Initiating transaction rollback
- 7、[DEBUG,DataSourceTransactionManager,main] Rolling back JDBC transaction on Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@19e09a4]
- 由第二行可知,addBook()方法的加入到了当前addUser()方法的事务中,第4行可知,addBook()方法抛出运行时异常,此时addUser()方法的事务被标记为rollback,整个事务都
- 将回滚。如果addUser()方法没有任何事务,那么addBook()方法也不会在事务环境中执行。不管是否抛出异常,sql都将执行。
- 如果addBook()方法抛出受检查的异常,那么此异常将忽略,整个addUser()方法的事务将提交。addBook()方法也不会尝试去回滚事务
- 7、 我们再将addBook()方法的事务传播机制该为NOT_SUPPORTED,会怎么样呢?
- 将addBook()方法的事务机制该为NOT_SUPPORTED
- Xml代码
- <tx:advice id="txAdvice" transaction-manager="txManager">
- <tx:attributes>
- <tx:method name="addUser" propagation="REQUIRED" />
- <tx:method name="addBook" propagation="NOT_SUPPORTED"/>
- </tx:attributes>
- </tx:advice>
- addBook()方法
- Java代码
- public void addBook() throws Exception{
- this.jdbcTemplate.execute(ADD_BOOK);
- throw new RuntimeException("throw runtime exception in outter transaction");
- }
- addUser()方法
- Java代码
- public void addUser()throws Exception {
- this.bs.addBook();
- this.jdbcTemplate.execute(ADD_USER);
- }
- 执行junit后,控制台输出如下
- Java代码
- 1、[DEBUG,DataSourceTransactionManager,main] Creating new transaction with name [com.zx.spring.UserService.addUser]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
- 2、[DEBUG,DataSourceTransactionManager,main] Suspending current transaction
- 3、[DEBUG,JdbcTemplate,main] Executing SQL statement [insert into t_book(id,name) values(1,'duck-j2ee')]
- 4、[DEBUG,DataSourceUtils,main] Fetching JDBC Connection from DataSource
- 5、[DEBUG,DataSourceTransactionManager,main] Should roll back transaction but cannot - no transaction available
- 6、[DEBUG,DataSourceTransactionManager,main] Resuming suspended transaction after completion of inner transaction
- 7、[DEBUG,DataSourceTransactionManager,main] Initiating transaction rollback
- 8、[DEBUG,DataSourceTransactionManager,main] Rolling back JDBC transaction on Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@137d4a4]
- 由上可知,第二行是将addUser()方法的事务挂起,开始执行addBook()代码,
- 第5行可知,addBook()方法抛出运行时异常,需要回滚事务,但是又没有事务来回滚,因此t_book表数据被插入
- 由于addBook()抛出异常,在addUser()方法中未捕获该异常,因此对addUser()方法的事务传播机制REQUIRED来说,抛出了运行时异常,addUser()方法回滚
- 如果将addUser()方法该为如下:
- Java代码
- public void addUser()throws Exception {
- try {
- this.bs.addBook();
- }catch(Exception e) {
- e.printStackTrace();
- }
- this.jdbcTemplate.execute(ADD_USER);
- }
- 那么addUser()方法将会提交,addBook()方法将插入一条数据到t_book表中,
- 如果addBook()方法抛出了受检查异常,addBook()方法将忽略此异常,不尝试任何事务回滚,同样即使在addUser()方法中不捕获addBook()方法抛出的受检查异常,addUser()方法也会提交事务,而忽略此异常。
今天对spring事务7中传播机制之间的作用进行了一个回顾,这里只涉及了jdbc事务管理器的特性,可能会有各种疏忽,希望各位读者拍砖。
转自:http://kingj.iteye.com/blog/1680350#bc2378204
Spring 事务传播机制 实例讲解相关推荐
- java spring 事务传播_spring事务传播机制实例讲解
天温习spring的事务处理机制,总结如下 对于SQL事务的概念以及ACID性质,可以参见我的另一篇博文 http://kingj.iteye.com/admin/blogs/1675011 spri ...
- spring上下文是什么意思_Java程序员只会CRUD连Spring事务传播机制都不懂?
AQS到底有什么用?难道就真的只是为了面试吗? 当然不是说AQS没用,如果你不是做基础架构或者中间件开发,你很难感受到AQS的威力.当然,学习很多时候,需要的是正向反馈,学了太多造火箭的东西,面试完就 ...
- 原创 | CRUD更要知道的Spring事务传播机制
来自:肥朝 AQS到底有什么用?难道就真的只是为了面试吗? 当然不是说AQS没用,如果你不是做基础架构或者中间件开发,你很难感受到AQS的威力.当然,学习很多时候,需要的是正向反馈,学了太多造火箭的东 ...
- Spring事务传播机制以及事务嵌套
Spring事务传播机制以及事务嵌套 Spring事务传播机制 事务嵌套场景 情景0: 场景1:不同类中,开启事务的方法调用没有开启事务的方法 场景2:不同类中,methodA方法嵌套methodB方 ...
- spring 事务传播行为实例分析
Spring事务传播行为: spring特有的事务传播行为,spring支持7种事务传播行为,确定客户端和被调用端的事务边界(说得通俗一点就是多个具有事务控制的service的相互调用时所形成的复杂的 ...
- Spring事务传播特性实例解析
背景介绍 目前系统正在进行代码重构前期预研工作,目标采用spring控制事务以减少开发代码量,提高开发效率.同时避免开发人员编码控制事务所带来的链接没有释放,事务没有提交,出现异常事务没有回滚的Bug ...
- Spring事务传播机制和隔离级别
Spring有5种隔离级别,7种传播行为.这是面试常问的内容,也是代码中经常碰到的知识点.这些知识枯燥而且乏味,其中有些非常的绕.如果栽在这上面,就实在是太可惜了. @Transactional(is ...
- Spring事务传播机制大白话(使用springboot,注解演示)
1. 我对传播机制的理解 为什么需要传播机制? 因为事务之间可能存在相互调用,例如service业务层的方法存在相互调用,如果相互调用的方法都开启了事务(对应到springboot就是在方法上要添加@ ...
- Spring事务传播特性实例解析(以及如何使用注解形式事务)
原文地址:http://blog.csdn.net/yoara/article/details/16114853 原文地址的文章,写的demo会误导读者,所以在原文地址文章的基础上对原作者的demo进 ...
最新文章
- GARFIELD@02-10-2005
- 计算机控制技术在线,西交19春《计算机控制技术》在线作业【标准答案】
- 微软全都要!Win10引入真Linux内核
- 玩转oracle 11g(49):监听服务设置开机自启,不用一直配置监听
- js java 反射机制_java 类加载机制和反射机制
- slf4j+logback 日志
- MySQL5.6优化了Order by排序limit
- 软件工程概论第十三周学习进度
- 病毒详解及批处理病毒制作:自启动、修改密码、定时关机、蓝屏、进程关闭...
- GraphX中Pregel单源点最短路径(转)
- 更深入的理解动态代理——一个真正让我理解动态代理应用价值的示例
- fw313r手机登录_迅捷(FAST)fw313r路由器手机设置教程
- 软件开发人员培养计划
- docker安装nessus方法
- 蜻蜓和蝉c语言,趣味数学:蜻蜓、蜘蛛、蝉它们的翅膀和腿
- SKYPE的BUG 7/8
- 我那么爱你为什么?伤感爱情日志
- 五一影视圈的神仙打架,揭开了内容市场暗自角力的真相
- 惠普暗影精灵II代pro进入biso
- 亚马逊、虾皮、来赞达、速卖通、ebay等跨境电商平台如何搭建一个稳定的自养号补单系统?
热门文章
- 实习半年以来的感受和收获
- 进行卡方检验前为什么要加权个案
- qq在电脑上总是服务器连接中断,win7系统玩QQ炫舞老是掉线提示与服务器断开连接的解决方法...
- FMOD音频引擎简单使用
- 十六、Vert.x、Actix-web、Warp、Axum 性能对比
- dstwo linux n64,惊爆 NDS可以用N64模拟器 DSTWO用户有福了!
- 电脑用着用着突然黑屏怎么处理
- 正则表达式环视否定例子
- 机器学习--没有免费的午餐定理
- 大数据-NoSQL数据库-HBase操作框架:Phoenix【Java写的基于JDBC API的操作HBase数据库的SQL引擎框架;低延迟、事务性、可使用sql语句、提供JDBC接口】