• 事务之spring事务管理


    原生JDBC事务的弊端

    1. 如果需要操作多个Dao,需要每次操作数据库都需要在Service层的方法里写开启连接、提交事务、回滚事务的模版代码,Service转账伪代码如下

      public void transfer1(int sourceid, int targetid, double money)
      {
      	Connection conn = null;
      	try
      	{
      		conn = JdbcUtils.getConnection();
      		//设置事务不自动提交
      		conn.setAutoCommit(false);
      		//将连接传入dao层
      		AccountDao dao = new AccountDao(conn);
      		Account a = dao.find(sourceid); 
      		Account b = dao.find(targetid);
      		a.setMoney(a.getMoney() - money);
      		b.setMoney(b.getMoney() + money);
      		dao.update(a); 
      		dao.update(b);
      		conn.commit();
      	}catch(Exception e){
      		if(conn!=null)
            conn.rollback();
      	} finally
      	{
      		if (conn != null)
      			conn.close();
      	}
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
    2. 为了解决上述问题,可以通过AOP方式将上面的非业务代码抽取成框架层面的公共代码。并且将连接对象放在ThreadLocal中,无需通过构造方法或者其他方式传递连接对象,而且Service和dao在同一个线程下,Service可以控制事务的提交和回滚。虽然Spring对事务做了很好的处理,简化了我们很多模版代码,但是同样带来很多坑(主要还是很多开发不熟悉或者没有深入理解),导致出现各种诡异问题,甚至重大bug

      使用AOP简化代码
      public void transfer1(int sourceid, int targetid, double money)
      {ThreadLocal中获取数据库连接对象conn
      		AccountDao dao = new AccountDao();
      		Account a = dao.find(sourceid); 
      		Account b = dao.find(targetid);
      		a.setMoney(a.getMoney() - money);
      		b.setMoney(b.getMoney() + money);
      		dao.update(a); // update
      		dao.update(b);// update
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
    3. 架构图
      在这里插入图片描述

    Spring事务管理核心API

    1. Spring事务管理高层抽象主要包括3个接口

      • PlatformTransactionManager:事务管理器真正用来进行事务管理对象
      • TransactionDefinition:事务定义信息(隔离、传播、超时、只读)
      • TransactionStatus:事务具体运行状态
    2. PlatformTransactionManager根据 TransactionDefinition 信息来进行事务管理, 在管理事务过程中,每个时间点都可以获取事务状态 (TransactionStatus )

      //返回当前活动的事务或创建一个新的事务
      TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
      		throws TransactionException;
      //根据给定事务的状态提交给定事务
      void commit(TransactionStatus status) throws TransactionException;
      //执行给定事务的回滚
      void rollback(TransactionStatus status) throws TransactionException;
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
    3. spring没有直接管理事务,而是将管理事务的责任委托给JTA或相应的持久性机制所提供的某个特定平台的事务实现。Spring为不同的持久化框架提供了不同PlatformTransactionManager接口实现

      • org.springframework.jdbc.datasource.DataSourceTransactionManager:使用Spring JDBC或iBatis 进行持久化数据时使用
      • org.springframework.orm.hibernate3.HibernateTransactionManager:使用Hibernate3.0版本进行持久化数据时使用
      • org.springframework.orm.jpa.JpaTransactionManager:使用JPA进行持久化时使用
      • org.springframework.jdo.JdoTransactionManager:当持久化机制是Jdo时使用
      • org.springframework.transaction.jta.JtaTransactionManager:使用一个JTA实现来管理事务,在一个事务跨越多个资源时必须使用
    4. TransactionDefinition接口

      public interface TransactionDefinition {
        //事务的7个传播行为
      	int PROPAGATION_REQUIRED = 0;
      	int PROPAGATION_SUPPORTS = 1;
      	int PROPAGATION_MANDATORY = 2;
      	int PROPAGATION_REQUIRES_NEW = 3;
      	int PROPAGATION_NOT_SUPPORTED = 4;
      	int PROPAGATION_NEVER = 5;
      	int PROPAGATION_NESTED = 6;
      
        //事务的5个隔离级别
      	int ISOLATION_DEFAULT = -1;
      	int ISOLATION_READ_UNCOMMITTED = 1;  // same as java.sql.Connection.TRANSACTION_READ_UNCOMMITTED;
      	int ISOLATION_READ_COMMITTED = 2;  // same as java.sql.Connection.TRANSACTION_READ_COMMITTED;
      	int ISOLATION_REPEATABLE_READ = 4;  // same as java.sql.Connection.TRANSACTION_REPEATABLE_READ;
      	int ISOLATION_SERIALIZABLE = 8;  // same as java.sql.Connection.TRANSACTION_SERIALIZABLE;
      	
        //事务超时时间
      	int TIMEOUT_DEFAULT = -1;
      	//返回传播行为
      	default int getPropagationBehavior() {
      		return PROPAGATION_REQUIRED;
      	}
         //返回隔离级别
      	default int getIsolationLevel() {
      		return ISOLATION_DEFAULT;
      	}
      	//返回超时时间
      	default int getTimeout() {
      		return TIMEOUT_DEFAULT;
      	}
      	//是否为只读事务
      	default boolean isReadOnly() {
      		return false;
      	}
      	//返回事务的名称
      	@Nullable
      	default String getName() {
      		return null;
      	}
      
      	static TransactionDefinition withDefaults() {
      		return StaticTransactionDefinition.INSTANCE;
      	}
      
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
    5. TransactionStatus:代表当前事务的状态,也可以对当前事务进行控制

      public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
      
       //当前事务是否有保存点
      	boolean hasSavepoint();
      
      	//用于刷新底层会话中的修改到数据库,一般用于刷新如Hibernate/JPA的会话
      	@Override
      	void flush();
      
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10

    传播行为

    1. 事务传播行为,并不是数据库内部支持特性,而是Spring 为了解决企业开发中实际事务管理问题而设计的

      隔离级别含义
      PROPAGATION_REQUIRED(默认值)支持当前事务,如果方法运行时,已经处在一个事务中,那么加入到该事务,否则为自己创建一个新的事务
      PROPAGATION_SUPPORTS业务方法在某个事务范围内被调用,则方法成为该事务的一部分。如果业务方法在事务范围外被调用,则方法在没有事务的环境下执行
      PROPAGATION_MANDATORY业务方法只能在一个已经存在的事务中执行,业务方法不能发起自己的事务。如果业务方法在没有事务的环境下调用,容器就会抛出异常。
      PROPAGATION_REQUIRES_NEW不管是否存在事务,业务方法总会为自己发起一个新的事务。如果方法已经运行在一个事务中,则原有事务会被挂起,新的事务会被创建,直到方法执行结束,新事务才算结束,原先的事务才会恢复执行。原有事务和新事务的事务提交、回滚互不影响,两个事务是互补想干的独立事务
      PROPAGATION_NOT_SUPPORTED声明方法不需要事务。如果方法没有关联到一个事务,容器不会为它开启事务。如果方法在一个事务中被调用,该事务会被挂起,在方法调用结束后,原先的事务便会恢复执行
      PROPAGATION_NEVER业务方法绝对不能在事务范围内执行。如果业务方法在某个事务中执行,容器会抛出异常,只有业务方法没有关联到任何事务,才能正常执行
      PROPAGATION_NESTED如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按REQUIRED属性执行.它使用了一个单独的事务, 这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效。底层数据源必须基于JDBC3.0,需要支持保存点事务机制
    2. 归类

      • 支持当前事务,被调用方法和原来方法可以处于同一个事务中(REQUIRED 、SUPPORTS 、MANDATORY)
      • 不支持当前事务,被调用方法和原来方法 一定不会处于同一个事务(REQUIRES_NEW、NOT_SUPPORT 、NEVER)
      • NESTED 是最特殊的一种 ,嵌套事务执行 ,原理就是SavePoint 回滚点技术。嵌套事务,仍然使用是同一个事务 ,可以在事务执行过程中设置回滚点,如果被调用方法出错后,可以选择将程序回滚到回滚点 (只对DataSourceTransactionManager有效 )
    3. 常用的总结

      • REQUIRED: 两个操作,处于同一个事务中,要么都成功,要么都失败
      • REQUIRES_NEW : 两个操作分别处于两个不同事务,彼此之间不会互相影响。比如ATM机取完钱然后要打印凭条,如果打印凭条失败了,取钱的方法是否需要回滚(其实不需要回滚) 。
      • NESTED : 两个操作,处于同一个事务,但是内部采用 SavePoint机制,在一个操作出现问题时,回滚到保存点,继续操作

    传播行为案例

    1. REQUIRED调用REQUIRED:不出异常,事务不一定提交。调用transferRequired方法,虽然此方法本身没有抛出异常,但是事务回滚了。调用方出现了UnexpectedRollbackException异常,因为transferRequiredThrowRuntimeException方法标记了事务需要回滚,而且是REQUIRED,所以导致主方法也提交不了

      @Service("propagationAccountService")
      @Slf4j
      public class PropagationAccountServiceImpl implements PropagationAccountService {
      
      	@Autowired
      	private PropagationAccountDao accountDao;
      
      	@Autowired
      	private PropagationAccountService propagationAccountService;
      
      
      	/**
      	 * 抛出异常
      	 * UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
      	 */
      	@Override
      	@Transactional(propagation = Propagation.REQUIRED)
      	public void transferRequired(String account, BigDecimal money) {
      		accountDao.in(account, money);
      		try {
      			propagationAccountService.transferRequiredThrowRuntimeException(account, money);
      		} catch (Exception e) {
      			//虽然捕获了异常,但是因为没有开启新事务,而当前事务因为异常已经被标记为rollback了
      			log.error(e.getMessage(), e);
      		}
      	}
      
      	@Override
      	@Transactional(propagation = Propagation.REQUIRED)
      	public void transferRequiredThrowRuntimeException(String account, BigDecimal money) {
      		accountDao.in(account, money);
      		int d = 1 / 0;
      	}
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
    2. 解决REQUIRED调用REQUIRED方法一:使用手动方式设置回滚保存点

      @Service("propagationAccountService")
      @Slf4j
      public class PropagationAccountServiceImpl implements PropagationAccountService {
      
      	@Autowired
      	private PropagationAccountDao accountDao;
      
      	@Autowired
      	private PropagationAccountService propagationAccountService;
      
      	@Override
      	@Transactional(propagation = Propagation.REQUIRED)
      	public void transferRequiredSavePoint(String account, BigDecimal money) {
      		accountDao.in(account, money);
      		//只回滚以下异常
      		Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
      		try {
      			propagationAccountService.transferRequiredThrowRuntimeException("tom", money);
      		} catch (Exception e) {
      			TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
      			log.error(e.getMessage(), e);
      		}
      	}
      
      	@Override
      	@Transactional(propagation = Propagation.REQUIRED)
      	public void transferRequiredThrowRuntimeException(String account, BigDecimal money) {
      		accountDao.in(account, money);
      		int d = 1 / 0;
      	}
      }  
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
    3. 解决REQUIRED调用REQUIRES_NEW方法二:调用此方法时开启新的事务,并挂起当前事务

      @Service("propagationAccountService")
      @Slf4j
      public class PropagationAccountServiceImpl implements PropagationAccountService {
      
      	@Autowired
      	private PropagationAccountDao accountDao;
      
      	@Autowired
      	private PropagationAccountService propagationAccountService;
      
      	@Override
      	@Transactional(propagation = Propagation.REQUIRED)
      	public void transferRequired2(String account, BigDecimal money) {
      		accountDao.in(account, money);
      		try {
      			propagationAccountService.transferRequiredThrowRuntimeExceptionNew(account, money);
      		} catch (Exception e) {
      			//虽然捕获了异常,但是因为没有开启新事务,而当前事务因为异常已经被标记为rollback了
      			log.error(e.getMessage(), e);
      		}
      	}
      
      	@Override
      	@Transactional(propagation = Propagation.REQUIRES_NEW)
      	public void transferRequiredThrowRuntimeExceptionNew(String account, BigDecimal money) {
      		accountDao.in(account, money);
      		int d = 1 / 0;
      	}
      }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30

    Spring事务管理方式

    1. 数据库SQL

      CREATE DATABASE IF NOT EXISTS test_tx DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_bin;
      DROP TABLE IF EXISTS `account`;
      CREATE TABLE `account` (
                                 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
                                 `name` varchar(50) COLLATE utf8_bin NOT NULL COMMENT '账户名',
                                 `money` decimal(20,2) NOT NULL COMMENT '金额',
                                 PRIMARY KEY (`id`)
      ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
      
      BEGIN;
      INSERT INTO `account` VALUES (1, 'jannal', 2000.00);
      INSERT INTO `account` VALUES (2, 'tom', 500.00);
      COMMIT;
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13

    编程式事务管理

    1. 编写程序式的事务管理可以清楚的定义事务的边界,可以实现细粒度的事务控制,比如你可以通过程序代码来控制你的事务何时开始,何时结束等,与后面介绍的声明式事务管理相比,它可以实现细粒度的事务控制。在实际应用中很少使用,通过TransactionTemplate手动管理事务

    案例准备

    1. Jdbc配置

      jdbc.username=root
      jdbc.password=root
      jdbc.url=jdbc:mysql://127.0.0.1:3306/test_tx?useUnicode=true&autoReconnect=true&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&characterEncoding=utf8&&charaterSetResults=utf8&useSSL=false&serverTimezone=GMT%2B8
      jdbc.driver=com.mysql.jdbc.Driver
      
      • 1
      • 2
      • 3
      • 4
    2. 事务配置

      @Configuration
      @PropertySource(value = {"classpath:jdbc.properties"})
      @ComponentScan({"cn.jannal.tx.programmatic.account"})
      public class DataSourceConfiguration {
      	@Value("${jdbc.username}")
      	private String jdbcUsername;
      	@Value("${jdbc.password}")
      	private String jdbcPassword;
      	@Value("${jdbc.driver}")
      	private String jdbcDriverClass;
      	@Value("${jdbc.url}")
      	private String jdbcUrl;
      
      	@Bean
      	public DataSource datasource() {
      		HikariDataSource hikariDataSource = new HikariDataSource();
      		hikariDataSource.setJdbcUrl(jdbcUrl);
      		hikariDataSource.setUsername(jdbcUsername);
      		hikariDataSource.setDriverClassName(jdbcDriverClass);
      		hikariDataSource.setPassword(jdbcPassword);
      		return hikariDataSource;
      	}
      
      	@Bean
      	public DataSourceTransactionManager transactionManager(DataSource dataSource) {
      		DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource);
      		return dataSourceTransactionManager;
      	}
      
      	/**
      	 * 配置事务管理器模板
      	 */
      	@Bean
      	public TransactionTemplate transactionTemplate(DataSourceTransactionManager transactionManager) {
      		TransactionTemplate transactionTemplate = new TransactionTemplate();
      		transactionTemplate.setTransactionManager(transactionManager);
      		return transactionTemplate;
      	}
      
      	@Bean
      	public JdbcTemplate jdbcTemplate(DataSource datasource) {
      		JdbcTemplate jdbcTemplate = new JdbcTemplate();
      		jdbcTemplate.setDataSource(datasource);
      		return jdbcTemplate;
      	}
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
    3. 业务操作代码

      public interface AccountDao {
      	public int out(String outAccount, BigDecimal money);
      
      	public int in(String inAccount, BigDecimal money);
      }
      @Repository
      public class AccountDaoImpl implements AccountDao {
      
      	@Autowired
      	private JdbcTemplate jdbcTemplate;
      
      	@Override
      	public int out(String outAccount, BigDecimal money) {
      		String sql = "update account set money= money - ? where name= ?";
      		return this.jdbcTemplate.update(sql, money, outAccount);
      	}
      
      	@Override
      	public int in(String inAccount, BigDecimal money) {
      		String sql = "update account set money=money + ? where name = ?";
      		return this.jdbcTemplate.update(sql, money, inAccount);
      	}
      }
      
      public interface AccountService {
      	public void transfer(final String outAccount, final String inAccount, final BigDecimal money, final boolean mockException);
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27

    基于底层基础API的事务管理

    1. 业务实现类

      @Service("accountService0")
      public class AccountService0Impl implements AccountService {
      	@Autowired
      	private AccountDao accountDao;
      	@Autowired
      	private PlatformTransactionManager txManager;
      
      	@Override
      	public void transfer(String outAccount, String inAccount, BigDecimal money, boolean mockException) {
      		//定义事务
      		DefaultTransactionDefinition defaultTransactionDefinition = new DefaultTransactionDefinition();
      		defaultTransactionDefinition.setPropagationBehaviorName("PROPAGATION_REQUIRED");
      		//启动事务
          TransactionStatus txStatus = txManager.getTransaction(defaultTransactionDefinition);
      		try {
      			accountDao.out(outAccount, money);
      			if (mockException) {
      				int d = 1 / 0;
      			}
      			accountDao.in(inAccount, money);
      			txManager.commit(txStatus);
      		} catch (Throwable e) {
      			txManager.rollback(txStatus);
      			throw e;
      		}
      	}
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
    2. 测试用例

      @RunWith(SpringJUnit4ClassRunner.class)
      @ContextConfiguration(classes = DataSourceConfiguration.class)
      public class TestMain {
      
      	@Resource(name = "accountService")
      	private AccountService accountService;
      	@Test
      	public void testTransferNoException() {
      		accountService.transfer("jannal", "tom", BigDecimal.valueOf(1000), false);
      	}
      
      	@Test(expected = ArithmeticException.class)
      	public void testTransferHasException() {
      		accountService.transfer("jannal", "tom", BigDecimal.valueOf(1000), true);
      	}
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16

    基于TransactionTemplate的编程事务管理

    1. 业务实现类,这里我们看一下TransactionTemplate的源码

      @Service("accountService")
      public class AccountServiceImpl implements AccountService {
      
      	@Autowired
      	private AccountDao accountDao;
      	@Autowired
      	private TransactionTemplate transactionTemplate;
      
      	@Override
      	public void transfer(final String outAccount, final String inAccount, final BigDecimal money, final boolean mockException) {
      		//TransactionCallbackWithoutResult是没有返回值的
      		//TransactionCallback是有返回值的
      		transactionTemplate.executeWithoutResult(transactionStatus -> {
      			accountDao.out(outAccount, money);
      			if (mockException) {
      				int d = 1 / 0;
      			}
      			accountDao.in(inAccount, money);
      		});
      	}
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
    2. 测试用例

      @RunWith(SpringJUnit4ClassRunner.class)
      @ContextConfiguration(classes = DataSourceConfiguration.class)
      public class TestMain {
      	@Resource(name = "accountService0")
      	private AccountService accountService;
      
      	@Test
      	public void testTransferNoException() {
      		accountService.transfer("jannal", "tom", BigDecimal.valueOf(1000), false);
      	}
      
      	@Test(expected = ArithmeticException.class)
      	public void testTransferHasException() {
      		accountService.transfer("jannal", "tom", BigDecimal.valueOf(1000), true);
      	}
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16

    声明式事务管理

    1. 如果你并不需要细粒度的事务控制,你可以使用声明式事务,在Spring中,你只需要在Spring配置文件中做一些配置,即可将操作纳入到事务管理中,解除了和代码的耦合, 这是对应用代码影响最小的选择。当你不需要事务管理的时候,可以直接从Spring配置文件中移除该设置

    基于注解的

    1. @Transactional 可以作用于接口、接口方法、类以及类方法上。当作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该注解来覆盖类级别的定义。

    2. @Transactional注解可以被继承,即:在父类上声明了这个注解,则子类中的所有public方法也都是会开事务的。Spring 小组建议不要在接口或者接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效

    3. @Transactional注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的(可以通过AspectJ解决)。如果你在 protected、private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。

      AbstractFallbackTransactionAttributeSource类
      这个方法会检查目标方法的修饰符是不是 public,若不是 public,就不会获取@Transactional 的属性配置信息,最终会造成不会用 TransactionInterceptor 来拦截该目标方法进行事务管理  
        
      protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
      		// Don't allow no-public methods as required.
      		if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
      			return null;
      		}
      
      		// The method may be on an interface, but we need attributes from the target class.
      		// If the target class is null, the method will be unchanged.
      		Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
      
      		// First try is the method in the target class.
      		TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
      		if (txAttr != null) {
      			return txAttr;
      		}
      
      		// Second try is the transaction attribute on the target class.
      		txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
      		if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
      			return txAttr;
      		}
      
      		if (specificMethod != method) {
      			// Fallback is to look at the original method.
      			txAttr = findTransactionAttribute(method);
      			if (txAttr != null) {
      				return txAttr;
      			}
      			// Last fallback is the class of the original method.
      			txAttr = findTransactionAttribute(method.getDeclaringClass());
      			if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
      				return txAttr;
      			}
      		}
      
      		return null;
      	}
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
    4. 默认情况下,如果在事务中抛出了未检查异常(继承自 RuntimeException 的异常)或者 Error,则 Spring 将回滚事务;除此之外,Spring 不会回滚事务。若在目标方法中抛出的异常是 rollbackFor 指定的异常的子类,事务同样会回滚。

      RollbackRuleAttribute 源码
        
      private int getDepth(Class<?> exceptionClass, int depth) {
      	if (exceptionClass.getName().contains(this.exceptionName)) {
      		// Found it!
      		return depth;
      	}
      	// If we've gone as far as we can go and haven't found it...
      	if (exceptionClass == Throwable.class) {
      		return -1;
      	}
      	return getDepth(exceptionClass.getSuperclass(), depth + 1);
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
    5. 属性信息

      name当在配置文件中有多个 TransactionManager , 可以用该属性指定选择哪个事务管理器。
      propagation事务的传播行为,默认值为 REQUIRED。
      isolation事务的隔离度,默认值采用 DEFAULT。
      timeout事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
      read-only指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。
      rollback-for用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指定,各类型之间可以通过逗号分隔。
      no-rollback- for抛出 no-rollback-for 指定的异常类型,不回滚事务。
    6. 在 Spring 的 AOP 代理下,只有目标方法由外部调用,目标方法才由 Spring 生成的代理对象来管理,这会造成自调用问题(可以通过AspectJ解决)。若同一类中的其他没有@Transactional 注解的方法内部调用有@Transactional 注解的方法,有@Transactional 注解的方法的事务被忽略,不会发生回滚

      public class AccountServiceImpl implements AccountService {
      	@Autowired
      	private AccountDao accountDao;
        //外部调用此方法事务不会回滚
      	@Override
      	public void transferNoTransactionalInvokeTransactionalException(String outAccount, String inAccount, BigDecimal money) {
      		transferThrowRuntimeExcetion(outAccount, inAccount, money);
      	}
      
      	@Override
      	@Transactional
      	public void transferThrowRuntimeExcetion(String outAccount, String inAccount, BigDecimal money) {
      		accountDao.out(outAccount, money);
      		accountDao.in(inAccount, money);
      		int d = 1 / 0;
      	}
      
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18

    基于tx命名空间的

    1. Spring 2.x引入了命名空间,结合使用命名空间。不需要针对每一个业务service建立一个代理对象了

      <beans xmlns="http://www.springframework.org/schema/beans"
      	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      	   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/aop
          http://www.springframework.org/schema/aop/spring-aop.xsd
          http://www.springframework.org/schema/tx
          http://www.springframework.org/schema/tx/spring-tx.xsd">
      
      	<!-- 配置事务增强,通过事务通知的方式实现事务 -->
      	<tx:advice id="txAdvice" transaction-manager="transactionManager">
      		<!-- 事务管理属性配置,配置哪些方法要使用什么样的事务配置,没有匹配到的方法不会为其管理事务 -->
      		<tx:attributes>
      			<!-- 可选属性配置
      			  name:方法名称,将匹配的方法注入事务管理,可用通配符
      			  propagation:事务传播行为
      			  isolation:事务隔离级别,默认为DEFAULT
      			  read-only:是否只读,默认为 false,表示不是只读
      			  timeout:事务超时时间,单位为秒,默认 -1,表示事务超时将依赖于底层事务系统
      			  rollback-for:需要触发回滚的异常定义,多个以逗号","分割,默认任何 RuntimeException 都将导致事务回滚,而任何 Checked Exception 将不导致事务回滚
      			  no-rollback-for:不被触发进行回滚的 Exception(s)。多个以逗号","分割
      			 -->
      			<!-- 设置进行事务操作的方法匹配规则 -->
      			<tx:method name="insert*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
      			<tx:method name="transferThrow*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
      			<tx:method name="transferNoException" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
      			<tx:method name="delete*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
      			<tx:method name="update*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
      			<tx:method name="get*" read-only="true"/>
      			<tx:method name="find*" read-only="true"/>
      		</tx:attributes>
      	</tx:advice>
      	<!-- 切面配置 -->
      	<aop:config>
      		<!-- 切入点配置: cn.jannal.tx.declarative.tx.account.service 包-->
      		<aop:pointcut expression="execution(* cn.jannal.tx.declarative.tx.*.service..*(..))" id="txPonitcut"/>
      		<!-- 通知与切入点关联 -->
      		<aop:advisor advice-ref="txAdvice" pointcut-ref="txPonitcut"/>
      	</aop:config>
      </beans>
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42

    基于AspectJ解决默认AOP的问题

    1. Spring Aop代理不支持内部调用。比如A方法里调用带@Transactional注解的B方法。可以通过在当前类注入自己的代理对象来解决自调用问题。也可以通过AopContext.currentProxy()来获取当前类的代理对象,但是这样会导致硬编码。

    2. 引入依赖(这里直接在spring源码中调试,所以依赖的是内部模块)

      compile(project(":spring-aop"))
      compile(project(":spring-tx"))
      compile(project(":spring-aspects"))
      compile("org.aspectj:aspectjweaver")
      
      • 1
      • 2
      • 3
      • 4
    3. 开启AspectJ支持

      @EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
      
      • 1
    4. 增加agent

      在IDEA的启动类上增加JVM参数
      -javaagent:/Users/jannal/aspectj1.9/lib/aspectjweaver.jar
      如果通过jar执行
      java -jar app.jar -javaagent:/Users/jannal/aspectj1.9/lib/aspectjweaver.jar
      
      • 1
      • 2
      • 3
      • 4

    常见问题

    1. 使用AspectJ的时候需要指定META-INF/aop.xml。因为spring-aspects模块里已经有这个文件了

      <aspectj>
      
      	
      
      	<aspects>
      		<aspect name="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"/>
      		<aspect name="org.springframework.scheduling.aspectj.AnnotationAsyncExecutionAspect"/>
      		<aspect name="org.springframework.transaction.aspectj.AnnotationTransactionAspect"/>
      		<aspect name="org.springframework.transaction.aspectj.JtaAnnotationTransactionAspect"/>
      		<aspect name="org.springframework.cache.aspectj.AnnotationCacheAspect"/>
      		<aspect name="org.springframework.cache.aspectj.JCacheCacheAspect"/>
      	aspects>
      
      aspectj>
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
    2. 终端输出很多**[Xlint:cantFindType],怎么去掉? 在项目resoures目录新建一个META-INF/aop.xml。将目标类限定在自己的项目包下。-showWeaveInfo**表示显示织入目标类的信息,便于查看

      <aspectj>
      	<aspects>
      		<aspect name="org.springframework.transaction.aspectj.AnnotationTransactionAspect"/>
      		<aspect name="org.springframework.transaction.aspectj.JtaAnnotationTransactionAspect"/>
      	aspects>
      	<weaver options="-showWeaveInfo -XmessageHandlerClass:org.springframework.aop.aspectj.AspectJWeaverMessageHandler">
      		<include within="cn.jannal.tx.declarative.annotation.account.service.*"/>
      	weaver>
      aspectj>
      或者直接将Xlint去掉(不建议)
      <aspectj>
      	<weaver options="-showWeaveInfo -Xlint:ignore" >weaver>
      aspectj>
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13

    事务不生效

    1. 事务不生效的原因

      • 事务方法没有被Spring容器管理或者未配置事务管理器

      • 方法没有被public修饰

      • 同一个类的A没有添加注解,B方法添加了事务注解,A调用B,方法B的事务会失效

      • 默认情况下,抛出了检查异常

    多个事务管理器

    1. 如果要在程序中使用多个事务管理器(主要是针对多数据源的情况),可以通过以下的方式实现,每个事务都会绑定各自的独立的数据源,进行各自的事务管理

      手动指定不同的事务管理器
      public class UserService {
      	@Transactional("transactionManager0")
      	public void delete(Long id){}
        @Transactional("transactionManager2")
      	public void delete(Long id){}
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
    2. 以上方式不够优雅,可以自定义一个绑定到特定事务管理器的注解,然后直接使用这个自定义的注解进行特定数据源的事务管理(这相当于运用了组合注解)

      /**
       * 自定义一个绑定到特定事务管理器的注解
       *
       * @Transactional默认的事务管理器名称为transactionManager
       */
      @Target({ElementType.TYPE, ElementType.METHOD})
      @Retention(RetentionPolicy.RUNTIME)
      @Inherited
      @Documented
      @Transactional("transactionManager0")
      public @interface Transactional_0 {
      }
      
      @Target({ElementType.TYPE, ElementType.METHOD})
      @Retention(RetentionPolicy.RUNTIME)
      @Inherited
      @Documented
      @Transactional("transactionManager1")
      public @interface Transactional_1 {
      }
      
      
      public class UserService {
      	@Transactional_0
      	public void delete(Long id){}
        @Transactional_1
      	public void delete(Long id){}
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28

    完整的案例代码

    1. 项目完整代码https://github.com/jannal/transaction/tree/master/spring

    2. 配置代码

      @Configuration
      @PropertySource(value = {"classpath:jdbc.properties"})
      @ComponentScan({"cn.jannal.tx.txmanager.account"})
      @EnableTransactionManagement
      public class MulitManagerDataSourceConfiguration {
      
      	@Bean(name = "datasource0")
      	public HikariDataSource datasource0(
      			@Value("${jdbc0.username}") String jdbcUsername,
      			@Value("${jdbc0.password}") String jdbcPassword,
      			@Value("${jdbc0.driver}") String jdbcDriverClass,
      			@Value("${jdbc0.url}") String jdbcUrl) {
      		HikariDataSource hikariDataSource = new HikariDataSource();
      		hikariDataSource.setPoolName("datasource0");
      		hikariDataSource.setJdbcUrl(jdbcUrl);
      		hikariDataSource.setUsername(jdbcUsername);
      		hikariDataSource.setDriverClassName(jdbcDriverClass);
      		hikariDataSource.setPassword(jdbcPassword);
      		return hikariDataSource;
      	}
      
      	@Bean(name = "datasource1")
      	public HikariDataSource datasource1(
      			@Value("${jdbc1.username}") String jdbcUsername,
      			@Value("${jdbc1.password}") String jdbcPassword,
      			@Value("${jdbc1.driver}") String jdbcDriverClass,
      			@Value("${jdbc1.url}") String jdbcUrl) {
      		HikariDataSource hikariDataSource = new HikariDataSource();
      		hikariDataSource.setPoolName("datasource1");
      		hikariDataSource.setJdbcUrl(jdbcUrl);
      		hikariDataSource.setUsername(jdbcUsername);
      		hikariDataSource.setDriverClassName(jdbcDriverClass);
      		hikariDataSource.setPassword(jdbcPassword);
      		return hikariDataSource;
      	}
      
      
      	@Bean(name = "transactionManager0")
      	public DataSourceTransactionManager transactionManager0(@Qualifier(value = "datasource0") DataSource dataSource0) {
      		DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource0);
      		return dataSourceTransactionManager;
      	}
      
      
      	@Bean(name = "jdbcTemplate0")
      	public JdbcTemplate jdbcTemplate0(@Qualifier(value = "datasource0") DataSource datasource0) {
      		JdbcTemplate jdbcTemplate = new JdbcTemplate();
      		jdbcTemplate.setDataSource(datasource0);
      		return jdbcTemplate;
      	}
      
      	@Bean(name = "jdbcTemplate1")
      	public JdbcTemplate jdbcTemplate1(@Qualifier(value = "datasource1") DataSource datasource1) {
      		JdbcTemplate jdbcTemplate = new JdbcTemplate();
      		jdbcTemplate.setDataSource(datasource1);
      		return jdbcTemplate;
      	}
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
    3. 自定义注解

      /**
       * 自定义一个绑定到特定事务管理器的注解
       * @Transactional默认的事务管理器名称为transactionManager
       */
      @Target({ElementType.TYPE, ElementType.METHOD})
      @Retention(RetentionPolicy.RUNTIME)
      @Inherited
      @Documented
      @Transactional("transactionManager0")
      public @interface Transactional_0 {
      }
      
      @Target({ElementType.TYPE, ElementType.METHOD})
      @Retention(RetentionPolicy.RUNTIME)
      @Inherited
      @Documented
      @Transactional("transactionManager1")
      public @interface Transactional_1 {
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
    4. sql语句

      1.创建test_tx和test_tx1两个数据库
      2. 两个数据库都执行如下SQL  
      DROP TABLE IF EXISTS `account`;
      CREATE TABLE `account` (
        `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
        `name` varchar(50) COLLATE utf8_bin NOT NULL COMMENT '账户名',
        `money` decimal(20,2) NOT NULL COMMENT '金额',
        PRIMARY KEY (`id`)
      ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
      
      BEGIN;
      INSERT INTO `account` VALUES (1, 'jannal', 2000.00);
      INSERT INTO `account` VALUES (2, 'tom', 500.00);
      COMMIT;
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
    5. Jdbc.properties配置

      jdbc0.username=root
      jdbc0.password=root
      jdbc0.url=jdbc:mysql://127.0.0.1:3306/test_tx?useUnicode=true&autoReconnect=true&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&characterEncoding=utf8&&charaterSetResults=utf8&useSSL=false&serverTimezone=GMT%2B8
      jdbc0.driver=com.mysql.jdbc.Driver
      jdbc1.username=root
      jdbc1.password=root
      jdbc1.url=jdbc:mysql://127.0.0.1:3306/test_tx1?useUnicode=true&autoReconnect=true&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&characterEncoding=utf8&&charaterSetResults=utf8&useSSL=false&serverTimezone=GMT%2B8
      jdbc1.driver=com.mysql.jdbc.Driver
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
    6. 业务代码

      public interface MulitTxManagerAccountDao {
      	public int out(String outAccount, BigDecimal money, JdbcTemplate jdbcTemplate);
      
      	public int in(String inAccount, BigDecimal money, JdbcTemplate jdbcTemplate);
      }
      
      @Repository
      public class MulitTxManagerAccountDaoImpl implements MulitTxManagerAccountDao {
      
      	@Override
      	public int out(String outAccount, BigDecimal money, JdbcTemplate jdbcTemplate) {
      		String sql = "update account set money= money - ? where name= ?";
      		return jdbcTemplate.update(sql, money, outAccount);
      	}
      
      	@Override
      	public int in(String inAccount, BigDecimal money, JdbcTemplate jdbcTemplate) {
      		String sql = "update account set money=money + ? where name = ?";
      		return jdbcTemplate.update(sql, money, inAccount);
      	}
      }
      
      public interface MulitTxManagerAccountService {
      
      	public void transferNoException0(final String outAccount, final String inAccount, final BigDecimal money);
      
      	public void transferNoException1(final String outAccount, final String inAccount, final BigDecimal money);
      }
      
      @Service("mulitTxManagerAccountService")
      public class MulitTxManagerAccountServiceImpl implements MulitTxManagerAccountService {
      
      	@Autowired
      	private MulitTxManagerAccountDao accountDao;
      	@Resource(name = "jdbcTemplate0")
      	private JdbcTemplate jdbcTemplate0;
      	@Resource(name = "jdbcTemplate1")
      	private JdbcTemplate jdbcTemplate1;
      
      
      	private void transfer0(final String outAccount, final String inAccount, final BigDecimal money, JdbcTemplate jdbcTemplate) {
      		accountDao.out(outAccount, money, jdbcTemplate);
      		accountDao.in(inAccount, money, jdbcTemplate);
      	}
      
      	@Override
      	@Transactional_0
      	public void transferNoException0(String outAccount, String inAccount, BigDecimal money) {
      		transfer0(outAccount, inAccount, money, jdbcTemplate0);
      	}
      
      	@Override
      	@Transactional_1
      	public void transferNoException1(String outAccount, String inAccount, BigDecimal money) {
      		transfer0(outAccount, inAccount, money, jdbcTemplate1);
      	}
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
    7. 测试代码

      public class MulitMain {
      	public static void main(String[] args) {
      		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MulitManagerDataSourceConfiguration.class);
      		MulitTxManagerAccountService accountService = (MulitTxManagerAccountService) context.getBean("mulitTxManagerAccountService");
      		transferNoException0(accountService);
      		transferNoException1(accountService);
      	}
      
      	public static void transferNoException0(MulitTxManagerAccountService accountService) {
      		accountService.transferNoException0("jannal", "tom", BigDecimal.valueOf(1000));
      	}
      
      	public static void transferNoException1(MulitTxManagerAccountService accountService) {
      		accountService.transferNoException1("jannal", "tom", BigDecimal.valueOf(1000));
      	}
      
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
  • 相关阅读:
    QEMU模拟mini2440开发环境
    mvc core基于Asp Net的印刷网站
    【外汇天眼】价格波动的节奏感:优化止盈方法!
    Docker容器-------Consul部署
    mmo中匹配机制的思考与实现
    无信号交叉口车辆通行控制研究
    Spark SQL----ANSI Compliance
    ORACLE 在内存管理机制上的演变和进化
    格式化以后数据还在吗 格式化后数据怎么恢复
    基于JAVA汽车销售系统计算机毕业设计源码+系统+mysql数据库+lw文档+部署
  • 原文地址:https://blog.csdn.net/usagoole/article/details/126162223