SpringJDBC与声明式事务
文章目录
1.SpringJDBC
1.1 复习一下传统的java 原生 在 sql包下的 JDBC 写法
JDBC核心类
java.sql.DriverManager:管理JDBC程序的
java.sql.Connection:java程序与数据库之间建立的连接,相当于一个管道
java.sql.Statement:用于执行SQL语句,并返回结果
java.sql.ResultSet:存储数据库查询的结果
开发步骤
注册驱动.
获得连接.
获得语句执行平台
执行sql语句
处理结果
释放资源.
1.1.1 初始化 sql
drop TABLE if EXISTS tbl_user_info;
CREATE TABLE tbl_user_info (
user_id int NOT NULL COMMENT ‘id’ ,
user_school varchar(64) NULL COMMENT ‘用户所属学校’ ,
user_name varchar(32) NOT NULL COMMENT ‘用户姓名’ ,
PRIMARY KEY (user_id)
)
COMMENT=‘用户表’
;
INSERT INTO tbl_user_info (user_id, user_school, user_name) VALUES (1, NULL, ‘张三’);
INSERT INTO tbl_user_info (user_id, user_school, user_name) VALUES (2, ‘东方锐智’, ‘李四’);
INSERT INTO tbl_user_info (user_id, user_school, user_name) VALUES (3, NULL, ‘王朝’);
INSERT INTO tbl_user_info (user_id, user_school, user_name) VALUES (4, NULL, ‘马汉’);
1.1.2 传统jdbc连接的demo
导入mysql驱动包并编写测试类
<!--mysql的驱动包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
import java.sql.*;
public class JDBCDemo {
public static void main(String[] args) throws Exception {
// 1.注册驱动
// Class.forName("com.mysql.jdbc.Driver");
//mysql 8+ 已经更改了驱动名称
Class.forName("com.mysql.cj.jdbc.Driver");
// 2.获取连接对象 指定字符的编码、解码格式(有可能数据和项目中文编码格式不同)
// mysql8.x的jdbc升级了,增加了时区(serverTimezone)属性,并且不允许为空。
String url = "jdbc:mysql://localhost:3306/spring?serverTimezone=UTC";
Connection conn = DriverManager.getConnection(url, "liyw", "liyw123");
// 3.获取执行SQL语句
Statement stat = conn.createStatement();
// 拼写SQL语句
String sql = "select * from tbl_user_info";
// 4.调用执行者对象方法,执行SQL语句获取结果集
// 返回的是ResultSet接口的实现类对象,实现类在mysql驱动中
ResultSet rs = stat.executeQuery(sql);
// 5.处理结果集
// ResultSet接口的方法 boolean next() 有结果集true,没有结果集返回false
while (rs.next()) {
// 获取每列的数据,使用的是ResultSet接口的方法getXXX
int userId = rs.getInt("user_id");// 相当于rs.getInt(1);这个方法有弊端
String userName = rs.getString("user_name");
String userSchool = rs.getString("user_school");
System.out.println(userId+"\t"+userName+"\t"+userSchool+"\t");
}
// 6.关闭资源
rs.close();
stat.close();
conn.close();
}
}
1. 2 springJDBC引入
spring通过抽象JDBC访问并一致的API来简化JDBC编程的工作量。
即spring封装了JDBC,使用jdbcTemplate可以方便地对数据库进行操作。
我们只需要声明SQL、调用合适的SpringJDBC框架API、处理结果集即可。
Spring JDBC通过一个模板类org.springframework. jdbc.core.JdbcTemplate封装了样板式的代码,用户通过模板类就可以轻松地完成大部分数据访问的操作。并且数据源DataSource对象与模板jdbcTemplate对象均可通过Bean的形式定义在配置文件中,充分发挥了依赖注入的威力。
1.2.1 jdbcTemplate 正式使用
-
导入包
除了mysql的驱动包 还需要导入spring对jdbc支持、对事务支持的包
<!--mysql的驱动包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<!--spring事务支持-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.12.RELEASE</version>
</dependency>
<!--spring对jdbc的支持-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.12.RELEASE</version>
</dependency>
-
test
import com.liyw.springStart01.bean.User; import org.junit.Test; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DriverManagerDataSource; import java.util.List; public class JdbcTemplateDemo { @Test public void test() { // DriverManagerDataSource 位于 Spring 自带的 org.springframework.jdbc.datasource 类包。 //这是标准 JDBC 数据源的一个简单实现类,它用于开发简单的应用和程序测试,并且不支持连接池,每次连接数据库都是创建新的连接对象。 DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC"); dataSource.setUsername("liyw"); dataSource.setPassword("liyw123"); // 创建JDBC模板 JdbcTemplate jdbcTemplate = new JdbcTemplate(); // 这里也可以使用构造方法 jdbcTemplate.setDataSource(dataSource); // sql语句 String sql = "select count(1) from tbl_user_info"; Long num = (long) jdbcTemplate.queryForObject(sql, Long.class); System.out.println(num); } }
1.2.1 分析代码
通过我们上面的代码样例和传统的方式一比,诶,好像和传统的方式比起来jdbcTmeplete执行sql的代码还是挺长的,好像乍一看并没有很简单很好写啊?
但是我们来仔细分析一下上面这段代码:
在执行目标sql,关注业务逻辑的部分,jdbcTmeplet的核心部分代码还是很精简很一目了然地让人知道要干什么,做什么操作。
蓝框部分里面其实代码也只分成两类,一类是new语句,一类是set语句。
我们之前课程里学习到的spring的特性大家还记得吗?就是IOC、DI和AOP。
找一个同学来简单概括一下我们之前课上spring特性的 ioc和di 分别是什么?
IOC是控制反转,在spring的实践中就是把创建、装配和管理对象交给spring容器来完成。
DI是依赖注入,是IOC的一种实现方式,它分为set注入和构造器注入。
那当我们看到new 实例化一个对象的时候,看到set赋值属性的时候,我们就可以把它和di联想起来。
1.2.2 了解 数据库连接池与引入c3p0的包(一阶段–数据库连接方式升级成数据库连接池)
-
什么是数据库连接池
数据库连接池(Connection pooling)是程序启动时建立足够的数据库连接,并将这些连接组成一个连接池,由程序动态地对池中的连接进行申请,使用,释放。
-
数据库连接池工作原理(简单了解)
-
为什么要用连接池
数据库连接是一种关键的有限的昂贵的资源,频繁创建与数据库进行连接,会消耗数据库资源和性能(建立连接的过程很复杂)。
数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而再不是重新建立一个;释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏,数据库连接池的机制会提高数据库的性能。
-
常见的连接池
在Java中开源的数据库连接池有以下几种 :
1、C3P0:是一个开放源代码的JDBC连接池,它在lib目录中与Hibernate 一起发布,包括了实现jdbc3和jdbc2扩展规范说明的Connection 和Statement 池的DataSources 对象。
c3p0常见配置ref:https://blog.youkuaiyun.com/caychen/article/details/79625279
2、Proxool:是一个Java SQL Driver驱动程序,提供了对选择的其它类型的驱动程序的连接池封装。可以非常简单的移植到现存的代码中,完全可配置,快速、成熟、健壮。可以透明地为现存的JDBC驱动程序增加连接池功能。
3、Jakarta DBCP:DBCP是一个依赖Jakartacommons-pool对象池机制的数据库连接池。DBCP可以直接的在应用程序中使用。
4、DDConnectionBroker:是一个简单、轻量级的数据库连接池。
5、DBPool:是一个高效、易配置的数据库连接池。它除了支持连接池应有的功能之外,还包括了一个对象池,使用户能够开发一个满足自己需求的数据库连接池。
6、XAPool:是一个XA数据库连接池。它实现了javax.sql.XADataSource并提供了连接池工具。
7、Primrose:是一个Java开发的数据库连接池。当前支持的容器包括Tomcat4&5、Resin3与JBoss3。它同样也有一个独立的版本,可以在应用程序中使用而不必运行在容器中。Primrose通过一个WEB接口来控制SQL处理的追踪、配置,以及动态池管理。在重负荷的情况下可进行连接请求队列处理。
8、SmartPool:是一个连接池组件,它模仿应用服务器对象池的特性。SmartPool能够解决一些临界问题如连接泄漏(connection leaks)、连接阻塞、打开的JDBC对象(如Statements、PreparedStatements)等。
9、MiniConnectionPoolManager:是一个轻量级JDBC数据库连接池。它只需要Java1.5(或更高)并且没有依赖第三方包。
10、BoneCP:是一个快速、开源的数据库连接池。帮用户管理数据连接,让应用程序能更快速地访问数据库。比C3P0/DBCP连接池速度快25倍。
11、Druid:Druid不仅是一个数据库连接池,还包含一个ProxyDriver、一系列内置的JDBC组件库、一个SQL Parser。
支持所有JDBC兼容的数据库,包括Oracle、MySql、Derby、Postgresql、SQL Server、H2等
1.2.3 xml配置 以新增一个用户演示insert操作
-
导入包
<!--c3p0数据库连接池--> <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.2</version> </dependency>
-
整合xml (重点查看数据库连接池注册与装配、创建jdbc模板)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--对数据库连接池的注册和装配-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="jdbcUrl"
value="jdbc:mysql://localhost:3306/spring?serverTimezone=UTC&useUnicode=true&
characterEncoding=utf8&useSSL=false"/>
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
<property name="user" value="liyw"/>
<property name="password" value="liyw123"></property>
</bean>
<!--创建jdbc的模板-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--创建dao-->
<bean id="userSpringJDBCDao" class="com.liyw.springJDBC.dao.UserSpringJDBCDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
<!--创建service-->
<bean id="userService" class="com.liyw.springJDBC.service.UserServiceImpl">
<property name="userSpringJDBCDao" ref="userSpringJDBCDao"/>
</bean>
</beans>
-
dao
public interface IUserSpringJDBCDao { //新增一个用户 public void insertUser(User user); }
public class UserSpringJDBCDaoImpl implements IUserSpringJDBCDao {
private JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void insertUser(User user) {
String sql= "insert into tbl_user_info(user_id,user_name,user_school) values(?,?,?)";
jdbcTemplate.update(sql,user.getUserId(),user.getUserName(),user.getUserSchool());
}
}
-
service
public interface IUserService { public void insertUser(User user); }
public class UserServiceImpl implements IUserService {
private IUserSpringJDBCDao userSpringJDBCDao;
public void setUserSpringJDBCDao(IUserSpringJDBCDao userSpringJDBCDao) {
this.userSpringJDBCDao = userSpringJDBCDao;
}
@Override
public void insertUser(User user) {
userSpringJDBCDao.insertUser(user);
}
}
-
test
@Test public void test01(){ ApplicationContext context =new ClassPathXmlApplicationContext("springJDBC.xml"); IUserService userService = context.getBean("userService", IUserService.class); User user = new User(); user.setUserId(100); user.setUserName("王二小"); user.setUserSchool("DDD"); userService.insertUser(user); }
2.jdbcTemplate curd(掌握)
2.1 jdbcTemplate更新数据库常用方法
- update (更新数据)-- insert update delete 都用的這個方法
- queryForObject (查询单行)
- query (查询多行)
- queryForObject (单值查询)不同的
2.2 演示 jdbcTemplate的curd
-
update (更新数据)-- insert update delete 都用的這個方法 演示新增之后 課堂试着先練習一下更新用户信息、删除用户信息把吧?
新增修改删除
@Override
public void insertUser(User user) {
String sql= "insert into tbl_user_info(user_id,user_name,user_school) values(?,?,?)";
jdbcTemplate.update(sql,user.getUserId(),user.getUserName(),user.getUserSchool());
}
@Override
public void updateUser(User user) {
String sql= "update tbl_user_info set user_name=? where user_id=?";
jdbcTemplate.update(sql,user.getUserName()+"_new",user.getUserId());
}
@Override
public void deleteUserById(int userId) {
String sql= "delete from tbl_user_info where user_id=?";
jdbcTemplate.update(sql,userId);
}
- queryForObject (单值查询)不同的
有两个参数:
1.sql语句
2.返回类型class
```
String sql = "select count(1) from tbl_user_info";
Long num = jdbcTemplate.queryForObject(sql, Long.class);
```
- queryForObject (查询单个对象)
两个参数:
1.sql语句
**2.RowMapper -- 本身是spring提供的一个接口,返回不同类型的数据,使用这个接口的实现类完成数据类型的封装**。
rowmapper是个接口,所以我们可以选择自己去实现接口,也可以用已经封装好的BeanPropertyRowMapper。
```
String sql = "select * from tbl_user_info limit 1";
User user= (User) jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<User>(User.class));
```
- BeanPropertyRowMapper实例化源码:
```
protected void initialize(Class<T> mappedClass) {
this.mappedClass = mappedClass;
this.mappedFields = new HashMap();
this.mappedProperties = new HashSet();
PropertyDescriptor[] var2 = BeanUtils.getPropertyDescriptors(mappedClass);
int var3 = var2.length;
for(int var4 = 0; var4 < var3; ++var4) {
PropertyDescriptor pd = var2[var4];
if (pd.getWriteMethod() != null) {
this.mappedFields.put(this.lowerCaseName(pd.getName()), pd);
String underscoredName = this.underscoreName(pd.getName());
if (!this.lowerCaseName(pd.getName()).equals(underscoredName)) {
this.mappedFields.put(underscoredName, pd);
}
this.mappedProperties.add(pd.getName());
}
}
}
```
也就是说BeanPropertyRowMapper的ORM,需要列名称和Java实体类名字一致,如:属性名 “userName” 可以匹配数据库中的列字段 “USERNAME”(忽略大小写) 或 “user_name”。
**query (查询集合行)**
两个参数
1.sql语句
2.RowMapper – 本身是spring提供的一个接口,返回不同类型的数据,使用这个接口的实现类完成数据类型的封装。
@Override
public List getUserList() {
String sql = "select * from tbl_user_info where 1=1 ";
return jdbcTemplate.query(sql, new MyRowMapper());
}
import com.liyw.springJDBC.pojo.User;
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;
public class MyRowMapper implements RowMapper<User> {
@Override
public User mapRow(ResultSet resultSet, int i) throws SQLException {
int userId = resultSet.getInt("user_id");
String userName = resultSet.getString("user_name");
String userSchool = resultSet.getString("user_school");
User user = new User(userId,userName,userSchool);
return user;
}
}
2.3 JdbcTemplate总结
- Spring全套管理各种组件,注入、控制反转等,将组件之间的关系以配置文件的形式串联起来,最大程度的对应用解耦
- Spring的JdbcTemplate中的CRUD操作还好,比较常规,但是也只对JDBC的持久化操作的比较方便
3.声明式事务
3.1 什么是事务(重要概念)
事务(Transaction)
在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。事务由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操作组成。也就是说事务就是把一系列的动作当成一个独立的工作单元,这些动作要么全部完成,要么全部不起作用。
也就是说,事务的结束只能有两种形式:提交和回滚。操作完全成功则提交,产生永久性的修改;操作不完全成功则回滚,恢复到事务开始前的状态。它们将结束一个事务。
事务四个属性ACID(重要概念!)
- 原子性(atomicity)
事务是原子性操作,由一系列动作组成,事务的原子性确保动作要么全部完成,要么完全不起作用
- 一致性(consistency)
事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
- 隔离性(isolation)
多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果。
- 持久性(durability)
事务一旦完成,无论系统发生什么错误,结果都不会受到影响,不会再回滚。通常情况下,事务的结果被写到持久化存储器中
举个例子 :A向B银行转账
A扣款,B收款。要么都做,要么都不做–原子性
A扣款,B收款,不论成功与否,两个账户金额合起来要是对的–一致性
C也向B转账处理,这是两个事务不应该相互影响。但事实上很可能在这一环节出现问题,因为事务看不到中间状态–隔离性
AB转账过程结束就需要持久化,不然会乱套–持久性
- 事务在项目开发过程非常重要,涉及到数据的一致性的问题,不容马虎!
- 事务管理是企业级应用程序开发中必备技术,用来确保数据的完整性和一致性。
3.2 对比传统的事务方式(编程式)
JDBC的一切行为包括事务是基于一个Connection
的,在JDBC中是通过Connection
对象进行事务管理。在JDBC中,常用的和事务相关的方法是: setAutoCommit
、commit
、rollback
等。
JDBC事务处理核心思路:
(1)关闭自动提交事务。通过设置连接的自动提交事务属性为false,如下:
Connection conn = DriverManager.getConnection("连接URL", "用户名", "密码");
//关闭自动提交事务
conn.setAutoCommit(false);
(2)如果执行顺利,提交事务;一旦发生异常,回滚(rollback)事务,如下:
try{
conn.setAutoCommit(false); //关闭自动提交事务
stmt = conn.createStatement(); //创建会话
stmt.executeUpdate("sql语句");
...
conn.commit(); //提交事务
}catch(Exception e){
e.printStackTrace();
conn.rollback(); //回滚事务
}
- 编程式事务管理(上述的代码就是编程式的事务管理)
将事务管理代码嵌到业务方法中来控制事务的提交和回滚
优点:加了事务管理相比原来,能更高程度地保证数据的一致性
缺点:必须在每个事务操作业务逻辑中包含额外的事务管理代码,很繁琐 ,和业务代码耦合强
我们上一节课学习到的aop能使我们的业务代码只需要关心自己的业务逻辑,低耦合低入侵地解决了我们想集中解决公共部分的非业务核心代码的问题。事务管理没有可能也通过这样的机制来实现呢?
有,spring为我们提供了这一种解决方案,这就是声明式事务(底层就是AOP)。使用一个事务拦截器,在方法调用的前后/周围进行事务性增强(advice),来驱动事务完成。
3.3 配置 以银行转账为例 演示声明式事务的配置
- A向B转账100块。
3.3.1 准备工作 – 原来怎么写这个业务 结合注解注入
-
创建数据库表,以及添加数据。
CREATE TABLE
tbl_bank_account
(
account_id
int NOT NULL COMMENT ‘账户id’,
account_balance
int(10) unsigned zerofill DEFAULT NULL COMMENT ‘账户余额’,
account_name
varchar(32) NOT NULL COMMENT ‘账户名’,
PRIMARY KEY (account_id
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT=‘银行账户表’;INSERT INTO
tbl_bank_account
(account_id
,account_balance
,account_name
) VALUES (1, 0000000500, ‘A’);
INSERT INTOtbl_bank_account
(account_id
,account_balance
,account_name
) VALUES (2, 0000000100, ‘B’); -
增加事务支持包。
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.2.12.RELEASE</version> </dependency>
-
创建service,搭建dao,完成对象创建和注入关系
即在service中注入dao,在dao中注入jdbcTemplate,在jdbcTemplate中注入database
dao:
public interface AccountDao { //增加余额 public void addMoney(int num,int accountId); //减少余额 public void deMoney(int num,int accountId); }
package com.liyw.springTX01.dao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; @Repository public class AccountDaoImpl implements AccountDao{ @Autowired private JdbcTemplate jdbcTemplate; @Override public void addMoney(int num,int accountId) { String sql = "update tbl_bank_account set account_balance = account_balance+? where account_id = ?"; jdbcTemplate.update(sql,num,accountId); } @Override public void deMoney(int num,int accountId) { String sql = "update tbl_bank_account set account_balance = account_balance-? where account_id = ?"; jdbcTemplate.update(sql,num,accountId); } }
service:
public interface IUserService {
//转账方法
public void transferMoney(int num,int targetId,int id);
}
@Service
public class UserServiceImpl implements IUserService {
@Autowired
private AccountDao dao;
@Override
public void transferMoney(int num, int targetId, int id) {
dao.deMoney(num,id);
System.out.println("A==转出"+num);
dao.addMoney(num,targetId);
System.out.println("B==转入"+num);
}
}
配置文件 需要增加context约束
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
">
<context:component-scan base-package="com.liyw.springTX01.*" ></context:component-scan>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="password" value="liyw123"/>
<property name="user" value="liyw"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring?serverTimezone=UTC"></property>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
</beans>
测试
@Test
public void test(){
ApplicationContext context= new ClassPathXmlApplicationContext("springTX01.xml");
IUserService userService= context.getBean("userServiceImpl",IUserService.class);
userService.transferMoney(100,1,2);
}
原来的写法可以实现业务,但是如果一旦发生异常,两步走的转账那么就会出现问题。
我们可以手动模拟一下这个异常,即把service的两步骤中间增加一个错误。
@Service
public class UserServiceImpl implements IUserService {
@Autowired
private AccountDao dao;
@Override
public void transferMoney(int num, int targetId, int Id) {
dao.deMoney(num,Id);
System.out.println("A==转出"+num);
int i=1/0;
dao.addMoney(num,targetId);
System.out.println("B==转入"+num) ;
}
}
比如A的扣款已经完成了,但是发生异常,程序不会往下走,B收不到钱,数据就不对了。
那如果遇到异常,我们理想的逻辑应该是什么呢?
应该是A扣款,B收款,都成功后提交;A扣款成功,B扣款失败,那么就应该回滚数据。
3.3.2 事务的xml配置方法(掌握)
我们需要对service层的transferMoney方法进行事务管理
配置文件 :增加aop和tx的事务支持
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
">
<context:component-scan base-package="com.liyw.springTX01.*" ></context:component-scan>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="password" value="liyw123"/>
<property name="user" value="liyw"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring?serverTimezone=UTC"></property>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 声明事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置事务传播特性 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="transfer*" propagation="REQUIRED" isolation="DEFAULT"/>
</tx:attributes>
</tx:advice>
<!-- 配置参与事务的类 -->
<aop:config>
<aop:advisor advice-ref="txAdvice" pointcut="execution(* com.liyw.spring01.service..*(..))" />
</aop:config>
</beans>
3.3.3 事务的@注解方式(了解)
- xml:需要tx约束
<!-- 声明事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 开启事务注解 -->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
-
service 添加@Transactional 可以添加在类上,也可以添加到方法上。(其实也可以用在接口上,但是需要用到jdk动态代理,spring并不建议这么做)
如果添加到类上,那么就该类所有方法都添加了事务;如果添加到方法上,就只有该方法有事务。
@Service
@Transactional
public class UserServiceImpl implements IUserService {
@Autowired
private AccountDao dao;
@Override
public void transferMoney(int num, int targetId, int Id) {
dao.deMoney(num,Id);
System.out.println("A==转出"+num);
int i=1/0;
dao.addMoney(num,targetId);
System.out.println("B==转入"+num) ;
}
}
3.4 关于spring注解配置的更多说明
@Transactional还可以配置更多事务相关的参数。
3.4.1 spring支持的传播行为 – propagation(重要概念)
官网说明:
https://docs.spring.io/spring-framework/docs/5.2.12.RELEASE/spring-framework-reference/data-access.html#tx-propagation
-
什么是事务传播行为:
事务传播行为指的是多个事务方法之间进行直接调用,这个过程中事务是如何进行管理的。
事务方法:对数据库表数据进行变化的操作。也就是insert update和delete这种。
官网:In Spring-managed transactions, be aware of the difference between physical and logical transactions, and how the propagation setting applies to this difference.
翻译:在Spring管理的事务中,请注意物理事务和逻辑事务之间的差异,以及传播设置如何应用于这种差异。
物理事务:一次connection的开启和关闭,其间的所有数据库操作属于物理事务
逻辑事务:被@Transactional注解或xml方式配置修饰的操作(也就是用spring声明式事务进行管理),Spring就会为其创建一个事务范围,可以理解为是逻辑事务。逻辑事务中大范围的事务称为外围事务,小范围的事务称为内部事务,外围事务可以包含内部事务,但在逻辑上是互相独立的。
解释:也就是说,在spring的事务管理下,有可能出现两个(或多个)不同的逻辑事务,但是他们的物理事务是同一个的情况,这也就说明,我们如果只用connection的hash来判断事务的传播属性,事实上是在判断它的物理事务,不一定是正确的。
-
spring支持的传播行为 一共七种
required:如果add有事务,update没有,则update也会处于add的事务内运行(纳入当前事务);如果add没有事务,就会创建一个新的事务把update放进新的事务内进行管理。
required_new:不管add本身有没有事务,都会新建一个事务把update方法放进去管理。
-
@Transactional本身默认是required级别 如果要修改,配置格式为:@Transactional(propagation = Propagation.REQUIRES_NEW)
-
演示
*注意:spring的事务传播当A方法调用B方法时,如果AB方法写在同一个类中,spring事务默认只会处理一个。如果需要写在同一个类里,一般建议调用B方法时,将service自己注入自己,用注入对象来调用B方法。(原因:spring的事务依靠aop,需要代理对象才能完成)
以Propagation.REQUIRES_NEW级别为例,演示事务的传播行为。
被调用:
@Service
public class NewService {
@Autowired
IBankAccountDao dao;
//演示事务传播级别
// @Transactional(propagation = Propagation.REQUIRES_NEW)
public void add() {
System.out.println("add====开始执行");
dao.decrease(1,100);
System.out.println("add====执行结束");
}
}
@Service
public class BankServiceImpl implements IBankService {
@Autowired
IBankAccountDao dao;
@Autowired
NewService newService;
//调用入口
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW )
public void test1(){
// 调用别的service的add 会挂起当前事务 起一个新的事务
newService.add();
// 沿用之前挂起的事务
update();
}
@Override
public void update() {
System.out.println("====update开始执行");
dao.add(2,100);
int x=1;
if (x==1){
throw new RuntimeException("手动抛出错误!");
}
System.out.println("====update执行结束");
}
}
测试代码:
// 在不同的类里
@Test
public void test01() {
ApplicationContext context = new ClassPathXmlApplicationContext("springTX.xml");
IBankService service = context.getBean("bankServiceImpl",IBankService.class);
service.test1();
}
核心日志:
3.4.2 spring支持的隔离级别 – isolation(重要概念)
- 隔离级别是做什么的
事务有四个特性叫隔离性,多事务操作之间(也就是在并发的情况下)不会产生影响,不考虑隔离性会产生很多问题,比较典型的三个问题:脏读、不可重复读、和幻象读。
不过需要注意的是,事务本身是数据库层面的概念,隔离性是ACID中的一个特性,但是spring的隔离级别isolation是spring特有的一个事务控制参数,两者之间不能直接等同起来。
隔离级别就是为了解决多事务操作之间、并发时的问题。
3.4.2.1 并发时可能出现的脏读、不可重复读、幻象读(重要概念)
一个事务读到另外一个事务还没有提交的数据,我们称之为脏读。
步骤 | 事务A | 事务B |
---|---|---|
1 | 开始事务 | |
2 | 开始事务 | |
3 | 修改学生S的学号为001-》002 | |
4 | 读取S的学号 002 | |
5 | 撤销修改(回滚):S的学号002-》001 | |
6 | 提交事务 | |
结果 | B事务读到了A事务没有提交的数据,B的结果不正确 |
一个事务先后读取同一条记录,但两次读取的数据不同,我们称之为不可重复读。
(一个未提交的事务读到了另一个已经提交修改的事务的结果。)–本质是一种现象
步骤 | 事务A | 事务B |
---|---|---|
1 | 开始事务 | |
2 | 开始事务 | |
3 | 读取学生S的学号为001 | |
4 | 修改学生S的学号为001-》002 | |
5 | 提交事务 | |
6 | 读取学生S的学号为002 | |
结果 | 事务B两次读取结果不一致 |
一个事务先后读取一个范围的记录,但两次读取的纪录数不同,我们称之为幻象读(一个事务中两次执行同一条 select 语句会出现不同的结果,第二次读会增加一数据行)。
一个未提交的事务读到了另一个已经提交新增的事务的结果。
步骤 | 事务A | 事务B |
---|---|---|
1 | 开始事务 | |
2 | 查询学生数据为x | 开始事务 |
4 | 新增一条记录,入学时间为其他时间 | |
4 | 提交事务 | |
5 | 查询学生数据为x+1 | |
结果 | 相同的sql两次读取记录数不同 |
我们说事务的原子性是不可分割,事实上指的是一个事务内是原子性,多个事务之间没有原子性。
A事务和B事务中各自的sql可能是穿插或交替执行的,因此多个事务是非原子性的。
3.4.2.2 spring隔离级别 isolation
我们来看这里的概念,事务的隔离级别从上到下,从允许事务读取未被其他事务提交的变更read_uncommitted到最下面只允许串行的serializable,它其实是越来越严格的。
我们可以看到像串行执行serializable,这个最严格的事务隔离级别,它可以避免脏读、不可重复读和幻觉读,但它是靠事务串行执行,一个一个事务排队依次执行来实现的。一个事务做完了,另一个事务才能上,也就是一次只能做一个事务。这在并发高的情况下,效率肯定是会很低的。
我们在实际开发中,需要在效率和并发之间做一个取舍,并不只是一味地追求避免出现数据脏读之类的问题。比如说,我们很多网站有站长统计这种统计的功能,那么统计数据总数差个几条几十条,它影响大吗?影响不会很大吧,那我们就可以考虑相对低一些的隔离级别比如default;比如说有一些金融类相关的场景,包括我们举例的银行账户有关的操作,那它如果数据上产生了问题,后果就会很严重,那我们肯定要考虑较高的隔离级别,尽量避免这种情况发生。
总的来说,我们实际的项目场景里使用READ_COMMITTED的场景还是最多的。
//配置方式
@Transactional(propagation = Propagation.REQUIRES_NEW,isolation = Isolation.DEFAULT)
3.5 总结
- 编程式事务管理(之前学习的方式)
将事务管理代码嵌到业务方法中来控制事务的提交和回滚
缺点:必须在每个事务操作业务逻辑中包含额外的事务管理代码,很繁琐 ,和业务代码耦合强
- 声明式事务管理
一般情况下比编程式事务好用。
将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。
交事务 |
| 5 | 查询学生数据为x+1 | |
| 结果 | 相同的sql两次读取记录数不同 | |
我们说事务的原子性是不可分割,事实上指的是一个事务内是原子性,多个事务之间没有原子性。
A事务和B事务中各自的sql可能是穿插或交替执行的,因此多个事务是非原子性的。
3.4.2.2 spring隔离级别 isolation
[外链图片转存中…(img-cQlFADdy-1683103326643)]
[外链图片转存中…(img-1M2M5spk-1683103326644)]
我们来看这里的概念,事务的隔离级别从上到下,从允许事务读取未被其他事务提交的变更read_uncommitted到最下面只允许串行的serializable,它其实是越来越严格的。
我们可以看到像串行执行serializable,这个最严格的事务隔离级别,它可以避免脏读、不可重复读和幻觉读,但它是靠事务串行执行,一个一个事务排队依次执行来实现的。一个事务做完了,另一个事务才能上,也就是一次只能做一个事务。这在并发高的情况下,效率肯定是会很低的。
我们在实际开发中,需要在效率和并发之间做一个取舍,并不只是一味地追求避免出现数据脏读之类的问题。比如说,我们很多网站有站长统计这种统计的功能,那么统计数据总数差个几条几十条,它影响大吗?影响不会很大吧,那我们就可以考虑相对低一些的隔离级别比如default;比如说有一些金融类相关的场景,包括我们举例的银行账户有关的操作,那它如果数据上产生了问题,后果就会很严重,那我们肯定要考虑较高的隔离级别,尽量避免这种情况发生。
总的来说,我们实际的项目场景里使用READ_COMMITTED的场景还是最多的。
//配置方式
@Transactional(propagation = Propagation.REQUIRES_NEW,isolation = Isolation.DEFAULT)
3.5 总结
- 编程式事务管理(之前学习的方式)
将事务管理代码嵌到业务方法中来控制事务的提交和回滚
缺点:必须在每个事务操作业务逻辑中包含额外的事务管理代码,很繁琐 ,和业务代码耦合强
- 声明式事务管理
一般情况下比编程式事务好用。
将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。
将事务管理作为横切关注点,通过aop方法模块化。Spring中通过Spring AOP框架支持声明式事务管理。