mysql的事务管理,是指在有关系的表(比如有外键)之间实现ACID,比如表A的金额列的外键是表B的金额列,那么如果A的金额减了300,B的金额也要减少300。如果A减300成功了,B失败了,mysql会自动回滚导致两个减都失败。
可以看到上述两个表在数据库内部就是有联系的。那么如果表C和表D在数据库内是没有任何联系的,我也想让C减少300,D减少300必须同时成功和失败,怎么做呢?或者控制同一张表的不同行的金额一行增300另一行必须减300,怎么做呢?按三层架构开发的话,只能在业务层控制了。spring的事务管理实现了上述功能。
JDBC的事务管理能实现上述功能,开启手动控制事务,编写代码执行,手动commit。步骤如下:
1获取连接 Connection con = DriverManager.getConnection()
2开启事务con.setAutoCommit(true/false);
3执行CRUD
4提交事务/回滚事务 con.commit() / con.rollback();
5关闭连接 conn.close();
spring的事务管理简化了JDBC的事务书写的2和4步骤,再搭配连接池简化了1和5步骤,再搭配mybatis简化了3步骤。使用注解开发配置事务的话(属于声明式事务),2和4这两个步骤直接简化成了两个注解和一个bean。在配置类上使用@EnableTransactionManagement告诉IOC容器我们使用了spring管理事务,在需要使用事务的接口或者方法上使用注解@Transactional声明里面的操作要么同时成功、要么同时失败。此时失败的原因不一定是数据库的原因,几个方法操作的表也不一定有外键这样的联系。先有数据源,再声明一个bean。最好不要在实现类上使用,不利于解耦。
spring采用的声明式事务(声明,意思就是使用注解。另外有种编程式事务,官方都不推荐就不学了)的粒度最小是到方法,意思就是能控制同时成功同时失败的最小的范围是在同一个方法内,如果是一个方法内部的某一个代码块那就失效了。根据代理、spring AOP的知识,背后的原理也能大概猜一下,应该就是在被@Transactiona注解的方法执行前和执行后加上一些操作而已。
Spring事务管理——使用XML配置声明式事务_Evankaka的专栏-CSDN博客_spring事务事务简介:事务管理是企业级应用程序开发中必不可少的技术,用来确保数据的完整性和一致性事务就是一系列的动作,它们被当作一个单独的工作单元。这些动作要么全部完成,要么全部不起作用事务的四个关键属性(ACID)① 原子性(atomicity):事务室一个原子操作,有一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用② 一致性(consistency):一旦所有事务动作完成,事务就被提交。https://blog.csdn.net/Evankaka/article/details/45478007?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-1.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-1.no_search_link @Transactional默认有些异常不会回滚。ERROR系和runtime系的会回滚,其他的都不回滚,比如IOException,需要手动设置成@Transactional(rollbacker=(IOException.class)).
事务管理员:
事务协调员:
事务的传播行为:实现日志@Transactional(propagation = Propagation.REQUIRES_NEW)
spring的事务管理纯注解实现的具体步骤:
1、导入依赖包,因为spring是基于JDBC事务的,jdbc是jdk自带的,按理说只需要在spring-context基础上导入spring-jdbc就行了。但是没有数据源那事务就没有意义了,所以得有数据源,有了数据源再给配个Druid连接池,导入mysql-connector-java和druid依赖。为了简化CRUD操作,再导入mybatis,再导入spring提供的简化mybatis配置的mybatis-spring。为了简化测试,导入Junit依赖,为了简化Junit调用IOC容器中的bean来进行测试,导入spring-junit依赖。
2、建立数据库、表,根据表建立实体类
3、建立dao接口(使用注解配置SQL)目的是从数据库获取数据。编总配置类SpringConfig,打上@Configuration注解声明这是一个配置类,打上@ComponentScan("com.ldj")告诉spring哪些包中存在着要交给IOC容器管理的bean记得去扫描,打上@PropertySource("classpath:jdbc.properties")告诉spring数据库信息存在哪里,打上@Import({JdbcConfig.class,MybatisConfig.class})告诉spring还有两个子配置类记得去导入,打上@EnableTransactionManagement告诉spring我这个项目使用了事务记得扫描相关的注解。
4、编写子配置类JdbcConfig和MybatisConfig,@Value获取properties文件中的值,@bean声明以下四个为IOC容器管理的bean,DruidDataSource和PlatformTransactionManager和SqlSessionFactoryBean和MapperScannerConfigurer。通过MapperScannerConfigurer会自动扫描dao将其代理类生成bean交给IOC管理,所以后续在service中我能够用@Autowired自动装配注入。
5、编写service接口和实现类。在实现类上使用@Service声明为bean,不能在接口上声明。方便测试类后续调用,当然如果写了controller层,也方便controller层调用。在实现类中通过@Autowired自动装配注入dao
6、在AccountService接口的方法上使用@Transactiona声明为事务,该方法内调用到的其他接口的方法,也可以同样的声明为事务,并且可以设置他们两者之间的协调关系。这就是事务的传播行为。
7、编写测试类,注入service,调用方法测试。
==================================================================
项目结构:
package com.ldj.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@ComponentScan("com.ldj")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
@EnableTransactionManagement
public class SpringConfig {
}
package com.ldj.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager ptm = new DataSourceTransactionManager();
ptm.setDataSource(dataSource);
return ptm;
}
}
package com.ldj.config;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;
import javax.sql.DataSource;
public class MybatisConfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setTypeAliasesPackage("com.ldj.domain");
ssfb.setDataSource(dataSource);
return ssfb;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.ldj.dao");//dao不需要通过打注解声明为bean,在这用编程式已经声明了
return msc;
}
}
package com.ldj.domain;
import java.io.Serializable;
public class Account implements Serializable {
private Integer id;
private String name;
private Double money;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getMoney() {
return money;
}
public void setMoney(Double money) {
this.money = money;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", money=" + money +
'}';
}
}
package com.ldj.dao;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
public interface AccountDao {
@Update("update tb_account set money = money + #{money} where name = #{name}")
void inMoney(@Param("name") String name, @Param("money") Double money);
@Update("update tb_account set money = money - #{money} where name = #{name}")
void outMoney(@Param("name") String name, @Param("money") Double money);
}
package com.ldj.dao;
import org.apache.ibatis.annotations.Insert;
public interface LogDao {
@Insert("insert into tb_log (info,createDate) values(#{info},now())")
void log(String info);
}
package com.ldj.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.FileNotFoundException;
import java.io.IOException;
public interface AccountService {
//rollback:设置当前事务参与回滚的异常,默认非运行时异常不参与回滚
// @Transactional(rollbackFor = IOException.class)
@Transactional
public void transfer(String out,String in ,Double money) throws IOException;
}
package com.ldj.service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
public interface LogService {
//propagation设置事务属性:传播行为设置为当前操作需要新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
void log(String out, String in, Double money);
}
package com.ldj.service.impl;
import com.ldj.dao.AccountDao;
import com.ldj.service.AccountService;
import com.ldj.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Autowired
private LogService logService;
public void transfer(String out,String in ,Double money) {
try{
accountDao.outMoney(out,money);
// int i = 1/0;
accountDao.inMoney(in,money);
}finally {
logService.log(out,in,money);
}
}
}
package com.ldj.service.impl;
import com.ldj.dao.LogDao;
import com.ldj.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao logDao;
public void log(String out,String in,Double money ) {
logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
}
}
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=1234
/*
Source Database : spring_db
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for tb_account
-- ----------------------------
DROP TABLE IF EXISTS `tb_account`;
CREATE TABLE `tb_account` (
`id` int(255) NOT NULL AUTO_INCREMENT,
`name` varchar(35) DEFAULT NULL,
`money` double DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of tb_account
-- ----------------------------
INSERT INTO `tb_account` VALUES ('1', '22', '33');
INSERT INTO `tb_account` VALUES ('2', '33', '55');
INSERT INTO `tb_account` VALUES ('3', '77', '88');
INSERT INTO `tb_account` VALUES ('7', '类', '999');
INSERT INTO `tb_account` VALUES ('8', 'Tom', '9650');
INSERT INTO `tb_account` VALUES ('9', 'Jerry', '10350');
-- ----------------------------
-- Table structure for tb_log
-- ----------------------------
DROP TABLE IF EXISTS `tb_log`;
CREATE TABLE `tb_log` (
`info` varchar(50) DEFAULT NULL,
`createDate` timestamp NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of tb_log
-- ----------------------------
INSERT INTO `tb_log` VALUES ('转账操作由Tom到Jerry,金额:50.0', '2021-11-08 19:38:36');
INSERT INTO `tb_log` VALUES ('转账操作由Tom到Jerry,金额:50.0', '2021-11-08 19:39:07');
INSERT INTO `tb_log` VALUES ('转账操作由Tom到Jerry,金额:50.0', '2021-11-08 20:38:31');
package com.ldj.service;
import com.ldj.config.SpringConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.io.IOException;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {
@Autowired
private AccountService accountService;
@Test
public void testTransfer() throws IOException {
accountService.transfer("Tom","Jerry",50D);
}
}