Spring JDBC 和 Tx
一、准备
(1)新增依赖
<!--新增--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.39</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.3.39</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.15</version> </dependency>
(2)jdbc配置
driverName = com.mysql.cj.jdbc.Driver url = jdbc:mysql://localhost:3306/studytest?characterEncoding=utf8&useSSL=false&serverTimezone=UTC username = root password = 11111
(3)Spring配置
<?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:property-placeholder location="classpath:jdbc.properties"/> <!-- 配置druid数据源 --> <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="url" value="${url}" /> <property name="username" value="${username}" /> <property name="password" value="${password}" /> <property name="driverClassName" value="${driverName}" /> </bean> </beans>
(4)数据库
create database studytest; use studytest; create table fruitshop ( name varchar(10) null, price double null, count int null, total double null )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
二、JdbcTemplate
(1)在applicationContext.xml增加JdbcTemplate对象到Spring容器
<!-- 配置JdbcTemplate --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="druidDataSource" /> </bean>
(2)测试
public class StudyTest { private final static Logger logger = LoggerFactory.getLogger(StudyTest.class); @Test public void test1() throws SQLException { // 老的获取Spring容器方法 ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); // 验证DruidDataSource获取Connection对象 DruidDataSource dataSource = context.getBean("druidDataSource", DruidDataSource.class); DruidPooledConnection connection = dataSource.getConnection(); logger.info("connection: {}", connection); // 验证JdbcTemplate,执行简单的sql JdbcTemplate jdbcTemplate = context.getBean(JdbcTemplate.class); String sql = "insert into fruitshop values(?,?,?,?)"; // int count = jdbcTemplate.update(sql, "波罗", "10", "12", "120"); sql = "update fruitshop set name=? where name=?"; int count = jdbcTemplate.update(sql, "pineapple","菠萝"); logger.info("count: {}", count); } }
(3)使用整合Junit更方便
@SpringJUnitConfig(locations = "classpath:applicationContext.xml") public class StudyIntegrateJUnit5Test { private static final Logger logger = LoggerFactory.getLogger(StudyIntegrateJUnit5Test.class); @Autowired private JdbcTemplate jdbcTemplate; @Test public void test01() { String sql = "select count(*) from fruitshop"; Integer fruitCnt = jdbcTemplate.queryForObject(sql, Integer.class); logger.info("fruitCnt:{}", fruitCnt); } }
(4)其他CRUD方法
- 对于增、删、改,均可以使用
jdbcTemplate.update(sql, Object...params)
,该方法返回一个int类型,表示影响的记录数 - 对于查询:
- jdbcTemplate.queryForObject(sql, Class):返回单个对象
- jdbcTemplate.query(sql, BeanPropertyRowMapper<>, params):返回list
事务
(1)什么是事务?
数据库事务(Transaction)是访问数据库并操作各种数据项的一个数据库操作序列,要么全部执行,要么全部不执行,是一个不可分割的单位。
事务由事务开始与事务结束之间执行的全部数据库操作组成。
(2)事务的特性
ACID
- A:Atomicity,原子性
- 一个事务中的所有操作,要么全部完成,要么全部不完成,不会卡在中间环节。
- 事务执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就行事务从未执行过。
- C:Consistency,一致性
- 指事务执行前后,数据库都必须处于一致状态。
- 如果事务成功完成,系统中所有变化将正确应用,系统处于有效状态。
- 如果事务出现错误,系统中所有变化将自动回滚,系统返回原始状态。
- I:Isolation,隔离性
- 指在并发环境中,当不同事务同时操纵相同数据时,每个事务都有各自的完整数据空间。
- 由并发实物所做的修改,必须与任何其他并发事务所做的修改隔离。
- 事务查看数据更新时,数据所处的状态要么是另一个事务修改之前状态,要么是另一个事务修改之后状态,不会存在中间状态的数据。
- D:Durability,持久性
- 只要事务成功结束,对数据库的更新就必须保存下来。
- 即使系统发生崩溃,重启数据库后还是能恢复到事务成功结束时的状态。
(3)编程式事务
即事务功能全部由自己编写代码来实现
// 一个演示手写事务实现的方法 public void demo() { Connection conn = dataSource.getConnection(); try { // 开启事务:关闭事务自动提交 conn.setAutoCommit(false); // 核心代码 // 提交事务 conn.commit(); } catch(Exception e) { // 发生异常,回滚事务 conn.rollback(); } finally{ // 最终释放连接 conn.close(); } }
编程式事务的缺陷:
- 细节没有被屏蔽:具体操作过程中,所有细节都要程序员自己完成,比较繁琐;
- 代码复用性不高:如果没有有效抽取出来,每次实现功能都要自己编写代码,代码没有复用。
(4)声明式事务
通过上述编程式事务
的demo,我们可想到——事务控制的代码是有规律的——代码结构基本固定,所以框架就可以将固定的代码抽取出来,进行封装。
封装完成后,我们只需要简单配置就可完成开启事务的全部操作。
好处:
- 提高开发效率
- 消除冗余代码
- 框架通过广泛应用,可以综合考虑相关领域中在实际开发环境下可能遇到的问题,能针对性的对健壮性、性能等各方面进行优化
因此:
- 编程式事务:自己写代码实现事务
- 声明式事务:通过框架配置实现事务
Spring中基于注解实现声明式事务
创建数据库、表:
CREATE DATABASE `spring`; use `spring`; CREATE TABLE `t_emp` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(20) DEFAULT NULL COMMENT '姓名', `age` int(11) DEFAULT NULL COMMENT '年龄', `sex` varchar(2) DEFAULT NULL COMMENT '性别', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `t_book` ( `book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `book_name` varchar(20) DEFAULT NULL COMMENT '图书名称', `price` int(11) DEFAULT NULL COMMENT '价格', `stock` int(10) unsigned DEFAULT NULL COMMENT '库存(无符号)', PRIMARY KEY (`book_id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; insert into `t_book`(`book_id`,`book_name`,`price`,`stock`) values (1,'斗破苍穹',80,100),(2,'斗罗大陆',50,100); CREATE TABLE `t_user` ( `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(20) DEFAULT NULL COMMENT '用户名', `balance` int(10) unsigned DEFAULT NULL COMMENT '余额(无符号)', PRIMARY KEY (`user_id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; insert into `t_user`(`user_id`,`username`,`balance`) values (1,'admin',50);
(1)没有添加事务的情况
@SpringJUnitConfig(locations = "classpath:applicationContext.xml") public class TestBookSellNoTranstion { @Autowired private BookController bookController; @Test public void testBuyBook(){ /* * 用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额 * 假设用户id为1的用户,购买id为1的图书,操作顺序如下: * 1. 从书籍库存中减1,原来100本,减后为99本; * 2. 再从用户账户扣除购书款80,用户余额为50,而图书价格为80,购买图书之后,用户的余额为-30 * 数据库中余额字段设置了无符号,因此无法将-30插入到余额字段;此时执行sql语句会抛出SQLException * */ bookController.buyBook(1,1); /* 观察结果; 因为没有添加事务,图书的库存更新了,但是用户余额因为异常而未更新 这个结果不是我们想要的,购买图书是一个完整的过程,书本拿走,账户扣款;而此时因为账户钱不够,而导致用户为花钱就买走的图书。 */ } }
(2)添加事务的情况
- Spring配置文件中引入tx命名空间
<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: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"> <!-- 扫描组件 --> <context:component-scan base-package="com.sheeprunner.springjdbc" /> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="druidDataSource"></property> </bean> <!-- 开启事务的注解驱动 通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务 --> <!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性 --> <tx:annotation-driven transaction-manager="transactionManager" /> </beans>
由于开启了事务注解驱动(在Spring配置文件中),并且在service(一般在业务层)使用了事务注解,
此时账户扣款发生异常,两个表都没有更新
@Transactional注解标识的位置:
- @Transactional标识在方法上,则只会影响该方法
- @Transactional标识的类上,则会影响类中所有的方法
(3)事务属性
1、只读
针对一个查询操作,如果我们把事务设置为只读,就明确告诉数据库,不涉及写操作,这样数据库可以针对查询进行优化。
import org.springframework.transaction.annotation.Transactional; @Transactional(readOnly = true)
注意,该属性只能对查询的方法配置,如果对增删改配置,会抛出下面异常:
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
2、超时
事务在执行过程中,可能遇到某些问题,导致程序卡住,从而长时间占用数据库资源,大概率是程序运行出现问题(如应用或数据库网络连接异常等)。
此时在阻塞资源的程序应该被回滚,撤销已做操作,结束事务,把资源让出来,让其他程序先执行。
// 超时单位为秒 import org.springframework.transaction.annotation.Transactional; @Transactional(timeout = 3) public void buyBook(Integer bookId, Integer userId) { // 睡眠4秒 try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } // 查询图书价格 Integer price = bookDao.getPriceByBookId(bookId); // 更新图书库存 bookDao.updateStock(bookId); // 更新用户余额 bookDao.updateBalance(userId, price); }
执行过程中如果超时,则抛出异常:
org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Sun Dec 15 21:57:34 CST 2024
3、回滚策略
声明式事务默认只针对运行时异常回滚,编译时异常不回滚。
@Transactional四个属性控制回滚方式:
- rollbackFor:设置一个Class类型对象,表示出现什么异常类型需要回滚
- rollbackForClassName:设置一个字符串类型的全类名
- noRollbackFor:设置一个Class类型对象,表示出现什么异常类型不需要回滚
- noRollbackForClassName:设置一个字符串类型的全类名
@Transactional(noRollbackFor = ArithmeticException.class )
上述设置表示,即使出现数学运算异常(ArithmeticException),也不发生回滚。
4、隔离级别
数据库系统必须具有隔离并发运行各个事务的能力,使各事务间不互相影响 ,避免并发问题。
一个事务与其他事务的隔离程度称为隔离级别。
以下是sql标准规定的四种隔离级别,隔离级别递增,总之,隔离性越高,数据一致性越好,并发性越弱。
- 读未提交:READ UNCOMMITTED,允许事务1读取事务2的未提交修改。
- 读已提交:READ COMMITTED,事务1只能读取事务2已提交的修改。
- 可重复度:REPEATABLE READ,确保事务1可以多次从一个字段中读取到相同值,即事务1执行期间禁止其他事务对该字段进行更新。
- 串行化:SERIALIZABLE,确保事务1可以多次从一个表中读取到相同的行,在事务1执行期间,禁止其他事务对表进行增删改。避免任何并发问题,但性能十分低下。
各个隔离级别解决并发问题的能力见下表:
隔离级别 |
脏读 |
不可重复读 |
幻读 |
READ UNCOMMITTED |
有 |
有 |
有 |
READ COMMITTED |
无 |
有 |
有 |
REPEATABLE READ |
无 |
无 |
有 |
SERIALIZABLE |
无 |
无 |
无 |
各种数据库产品对事务隔离级别的支持程度:
隔离级别 |
Oracle |
MySQL |
READ UNCOMMITTED |
× |
√ |
READ COMMITTED |
√(默认) |
√ |
REPEATABLE READ |
× |
√(默认) |
SERIALIZABLE |
√ |
√ |
使用方式
@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别 @Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交 @Transactional(isolation = Isolation.READ_COMMITTED)//读已提交 @Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读 @Transactional(isolation = Isolation.SERIALIZABLE)//串行化
5、传播行为
有两个方法A和B,都存在事务,当A执行过程中调用B,事务是如果传递的,就是传播行为。
有7种传播行为:
- REQUIRED:支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】
- SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行**【有就加入,没有就不管了】**
- MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常**【有就加入,没有就抛异常】**
- REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起**【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】**
- NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务**【不支持事务,存在就挂起】**
- NEVER:以非事务方式运行,如果有事务存在,抛出异常**【不支持事务,存在就抛异常】**
- NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。】
属性名称propagation
public enum Propagation { REQUIRED(0), SUPPORTS(1), MANDATORY(2), REQUIRES_NEW(3), NOT_SUPPORTED(4), NEVER(5), NESTED(6); private final int value; private Propagation(int value) { this.value = value; } public int value() { return this.value; } }
以最常用的REQUIRES和REQUIRES_NEW为例:
- 当两个都是默认当前事务(REQUIRES)时,循环中第一次执行成功,第二次执行失败,则两次都回滚;
- 当两个都是REQUIRES_NEW(开启新事务),则第一次成功的会更新到数据库,第二次失败的回滚。
(4) 全注解开发
1、配置类
package com.sheeprunner.springjdbc.config; import com.alibaba.druid.pool.DruidDataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.sql.DataSource; import java.io.IOException; import java.io.InputStream; import java.util.Properties; /** * @description: * @author: RunningSheep * @date: 2024-12-15 23:09 * @version: V1.0 */ @Configuration @ComponentScan("com.sheeprunner.springjdbc") // 启用事务 @EnableTransactionManagement public class SpringTxConfig { @Bean public DataSource getDataSource() throws IOException { InputStream resourceAsStream = SpringTxConfig.class.getClassLoader().getResourceAsStream("jdbc.properties"); Properties properties = new Properties(); properties.load(resourceAsStream); DruidDataSource dataSource = new DruidDataSource(); dataSource.setDriverClassName(properties.getProperty("driverName")); dataSource.setUrl(properties.getProperty("url")); dataSource.setUsername(properties.getProperty("username")); dataSource.setPassword(properties.getProperty("password")); return dataSource; } @Bean(name = "jdbcTemplate") public JdbcTemplate getJdbcTemplate(DataSource dataSource) { JdbcTemplate jdbcTemplate = new JdbcTemplate(); jdbcTemplate.setDataSource(dataSource); return jdbcTemplate; } @Bean public DataSourceTransactionManager getDatasourceTransactionManager(DataSource dataSource) { DataSourceTransactionManager dstm = new DataSourceTransactionManager(); dstm.setDataSource(dataSource); return dstm; } }
2、测试
@Test public void test() { ApplicationContext context = new AnnotationConfigApplicationContext(SpringTxConfig.class); BookController bookController = context.getBean("bookController", BookController.class); bookController.chekout(new Integer[]{1,2}, 1); }
(5)全XML配置
<!-- 开启事务的注解驱动 通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务 --> <!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性 --> <!--<tx:annotation-driven transaction-manager="transactionManager" />--> <!--下面为使用XML方式配置事务--> <aop:config> <!-- 配置事务通知和切入点表达式 --> <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.sheeprunner.springjdbc.service.*.*(..))" /> </aop:config> <!-- tx:advice标签:配置事务通知 --> <!-- id属性:给事务通知标签设置唯一标识,便于引用 --> <!-- transaction-manager属性:关联事务管理器 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!-- tx:method标签:配置具体的事务方法 --> <!-- name属性:指定方法名,可以使用星号代表多个字符 --> <tx:method name="get*" read-only="true"/> <tx:method name="query*" read-only="true"/> <tx:method name="find*" read-only="true"/> <!-- read-only属性:设置只读属性 --> <!-- rollback-for属性:设置回滚的异常 --> <!-- no-rollback-for属性:设置不回滚的异常 --> <!-- isolation属性:设置事务的隔离级别 --> <!-- timeout属性:设置事务的超时属性 --> <!-- propagation属性:设置事务的传播行为 --> <tx:method name="save*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/> <tx:method name="update*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/> <tx:method name="delete*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/> </tx:attributes> </tx:advice>