互联网的金融和电商行业,最关注数据库事务。
业务核心 | 说明 |
---|---|
金融行业-金融产品金额 | 不允许发生错误 |
电商行业-商品交易金额,商品库存 | 不允许发生错误 |
高并发下保证: 数据一致性,高性能;
采用AOP技术提供事务支持,申明式事务,去除了代码中重复的try-catch-finally代码;
两个场景的解决方案:
场景 | 解决办法 |
---|---|
库存扣减,交易记录,账户金额的数据一致性 | 数据库事务保证一致性 |
批量处理部分任务失败不影响批量任务的回滚 | 数据库事务传播行为 |
package com.springbootpractice.demo.demo_jdbc_tx.biz; import lombok.SneakyThrows; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Objects; import java.util.Optional; /** * 说明:代码方式事务编程 VS 申明式事物编程 * @author carter * 创建时间: 2020年01月08日 11:02 上午 **/ @Service public class TxJdbcBiz { private final JdbcTemplate jdbcTemplate; public TxJdbcBiz(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @SneakyThrows public int insertUserLogin(String username, String note) { Connection connection = null; int result = 0; try { connection = Objects.requireNonNull(jdbcTemplate.getDataSource()).getConnection(); connection.setAutoCommit(false); connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); final PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO user_login(user_name,password,sex,note) VALUES(?,?,?,?)"); preparedStatement.setString(1, username); preparedStatement.setString(2, "abc123"); preparedStatement.setInt(3, 1); preparedStatement.setString(4, note); result = preparedStatement.executeUpdate(); connection.commit(); } catch (Exception e) { Optional.ofNullable(connection) .ifPresent(item -> { try { item.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } }); e.printStackTrace(); } finally { Optional.ofNullable(connection) .filter(this::closeConnection) .ifPresent(item -> { try { item.close(); } catch (SQLException e) { e.printStackTrace(); } }); } return result; } private boolean closeConnection(Connection item) { try { return !item.isClosed(); } catch (SQLException e) { e.printStackTrace(); return false; } } @Transactional public int insertUserLoginTransaction(String username, String note) { String sql = "INSERT INTO user_login(user_name,password,sex,note) VALUES(?,?,?,?)"; Object[] params = {username, "abc123", 1, note}; return jdbcTemplate.update(sql, params); } }
package com.springbootpractice.demo.demo_jdbc_tx; import com.springbootpractice.demo.demo_jdbc_tx.biz.TxJdbcBiz; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.TransactionManager; import org.springframework.util.Assert; @SpringBootTest class DemoJdbcTxApplicationTests { @Autowired private TxJdbcBiz txJdbcBiz; @Autowired private TransactionManager transactionManager; @Test void testInsertUserTest() { final int result = txJdbcBiz.insertUserLogin("monika.smith", "xxxx"); Assert.isTrue(result > 0, "插入失败"); } @Test void insertUserLoginTransactionTest() { final int result = txJdbcBiz.insertUserLoginTransaction("stefan.li", "hello transaction"); Assert.isTrue(result > 0, "插入失败"); } @Test void transactionManagerTest() { System.out.println(transactionManager.getClass().getName()); } }
代码中有一个很讨厌的地方,就是 try-catch-finally;
graph TD A[开始] --> B(开启事务) B --> C{执行SQL} C -->|发生异常| D[事务回滚] C -->|正常| E[事物提交] D --> F[释放事务资源] E --> F[释放事务资源] F --> G[结束]
整体流程跟AOP的流程非常的相似,使用AOP,可以把执行sql的步骤抽取出来单独实现,其它的固定流程放到通知里去做。
jdbc使用事物编程代码点我!
通过注解@Transaction来标注申明式事务,可以标准在类或者方法上;
@Tranaction使用位置 | 说明 |
---|---|
类上或者接口上 | 类中所有的 公共非静态方法 都将启用事务, spring推荐放在实现类上,否则aop必须基于接口的代理生效的时候才能生效 |
方法上 | 本方法 |
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.transaction.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.core.annotation.AliasFor; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Transactional { @AliasFor("transactionManager") String value() default ""; @AliasFor("value") String transactionManager() default ""; Propagation propagation() default Propagation.REQUIRED; Isolation isolation() default Isolation.DEFAULT; int timeout() default -1; boolean readOnly() default false; Class<? extends Throwable>[] rollbackFor() default {}; String[] rollbackForClassName() default {}; Class<? extends Throwable>[] noRollbackFor() default {}; String[] noRollbackForClassName() default {}; }
属性 | 说明 |
---|---|
isolation | 事务的隔离级别 |
propagation | 传播行为 |
rollbackFor,rollbakcForClassName | 哪种异常会触发事务回滚 |
value | 事务管理器 |
timeout | 事务超时时间 |
readOnly | 是否是只读事务 |
noRollbackFor,noRollbackForClassName | 哪些异常不会触发事务回滚 |
我们要做的只是标注@Transactional和配置属性即可;
graph TD A[开始] --> B(开启和设置事务) B --> C{执行方法逻辑} C -->|发生异常| D[事务回滚] C -->|正常| E[事物提交] D --> F[释放事务资源] E --> F[释放事务资源] F --> G[结束]
使用方式大大简化;
@Transactional public int insertUserLoginTransaction(String username, String note) { String sql = "INSERT INTO user_login(user_name,password,sex,note) VALUES(?,?,?,?)"; Object[] params = {username, "abc123", 1, note}; return jdbcTemplate.update(sql, params); }
事务的打开,提交,回滚都是放在事务管理器上的。TransactionManager;
package org.springframework.transaction; public interface TransactionManager { }
这是一个空接口,实际起作用的是PlatfromTransactionManager;
package org.springframework.transaction; import org.springframework.lang.Nullable; public interface PlatformTransactionManager extends TransactionManager { TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException; void commit(TransactionStatus var1) throws TransactionException; void rollback(TransactionStatus var1) throws TransactionException; }
3个架子的事务管理器对比:
架子 | 事务管理 | 说明 |
---|---|---|
spring-jdbc | DatasourceTransactionManager | |
jpa | JpaTransactionManager | |
mybatis | DatasourceTransactionManager |
mybatis的代码实例点我!
场景:电商行业的库存扣减,时刻都是多线程的环境中扣减库存,对于数据库而言,就会出现多个事务同事访问同一记录,这样引起的数据不一致的情况,就是数据库丢失更新。
即ACID
事务的特性 | 英文全称 | 说明 |
---|---|---|
原子性 | Atomic | 一个事务中包含多个步骤操作A,B,C,原子性是标识这些操作要目全部成功,要么全部失败,不会出现第三种情况 |
一致性 | Consistency | 在事务完成后,所有的数据都保持一致状态 |
隔离性 | Isolation | 多个线程同时访问同一数据,每个线程处在不同的事务中,为了压制丢失更新的产生,定了隔离级别,通过隔离性的设置,可以压制丢失更新的发生,这里存在一个选择的过程 |
持久性 | Durability | 事务结束后,数据都会持久化,断电重启后也是可以提供给程序继续使用 |
隔离级别 | 说明 | 问题 | 并发性能 |
---|---|---|---|
读未提交【read uncommitted】 | 允许事务读取另外一个事务没有提交的数据,事务要求比较高的情况下不适用,适用于对事务要求不高的场景 | 脏读(单条) | 并发性能最高 |
读已提交【read committed】 | 一个事务只能读取另外一个事务已经提交的数据 | 不可重复读(单条) | 并发性能一般 |
可重复读【read repeated】 | 事务提交的时候也会判断最新的值是否变化 | 幻想读(多条数据而言) | 并发性能比较差 |
串行化【serializable】 | 所有的sql都按照顺序执行 | 数据完全一致 | 并发性能最差 |
隔离级别 | 脏读 | 不可重复读 | 幻象读 |
---|---|---|---|
读未提交 | 是 | 是 | 是 |
读已提交 | 否 | 是 | 是 |
可重复读 | 否 | 否 | 是 |
串行化 | 否 | 否 | 否 |
按照实际场景的允许情况来设置事务的隔离级别;
隔离级别会带来锁的代价;优化方法:
数据库 | 事务隔离级别 | 默认事务隔离级别 |
---|---|---|
mysql | 4种 | 可重复读 |
oracle | 读已提交,串行化 | 读已提交 |
springboot配置应用默认的事务隔离级别:spring.datasource.xxx.default-transaction-isolation=2
数字 | 对应隔离级别 |
---|---|
-1 | 无 |
1 | 读未提交 |
2 | 读已提交 |
4 | 可重复读 |
8 | 串行化 |
传播行为是方法之间调用事务采取的策略问题。
package org.springframework.transaction.annotation; import org.springframework.transaction.TransactionDefinition; public enum Propagation { REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED), SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS), MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY), REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW), NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED), NEVER(TransactionDefinition.PROPAGATION_NEVER), NESTED(TransactionDefinition.PROPAGATION_NESTED); private final int value; Propagation(int value) { this.value = value; } public int value() { return this.value; } }
列举了7种传播配置属性,下面分别说明:
传播行为 | 父方法中存在事务子方法行为 | 父方法中不存在事务子方法行为 |
---|---|---|
REQUIRED | 默认传播行为,沿用, | 创建新的事务 |
SUPPORTS | 沿用; | 无事务,子方法中也无事务 |
MANDATORY | 沿用 | 抛出异常 |
REQUIRES_NEW | 创建新事务 | 创建新事务 |
NOT_SUPPORTED | 挂起事务,运行子方法 | 无事务,运行子方法 |
NEVER | 抛异常 | 无事务执行子方法 |
NESTED | 子方法发生异常,只回滚子方法的sql,而不回滚父方法中的事务 | 发生异常,只回滚子方法的sql,跟父方法无关 |
代码测试这三种传播行为:
代码点我!
spring使用了save point的技术来让子事务回滚,而父事务不会滚;如果不支持save point,则新建一个事务来运行子事务;
区别点 | RequestNew | Nested |
---|---|---|
传递 | 拥有自己的锁和隔离级别 | 沿用父事务的隔离级别和锁 |
事务的实现原理是基于AOP,同一个类中方法的互相调用,是自己调用自己,而没有代理对象的产生,就不会用到aop,所以,事务会失效;
解决办法:通过spring的ioc容器得到当前类的代理对象,调用本类的方法解决;
原创不易,转载请注明出处,欢迎沟通交流。