备注:本文主要针对Spring对数据库的事务管理,及项目中注事项
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
事务前后数据的完整性必须保持一致。
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响
事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
如图
分布式事物基本理论:基本遵循CPA理论,采用柔性事物特征,软状态或者最终一致性特点保证分布式事物一致性问题
CAP理论作为分布式系统的基础理论,它描述的是一个分布式系统在以下三个特性中:
一致性(Consistency) 可用性(Availability) 分区容错性(Partition tolerance)
无法全部满足三个特性
AT
XA
SAGA
TCC
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted) | 是 | 是 | 是 |
不可重复读(read-committed) | 否 | 是 | 是 |
可重复读(repeatable-read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
|变量|内容|是否默认|备注| |:-|:-|:-| |Propagation.REQUIRED|支持当前事务,如果不存在 就新建一个|是|保证同一个事务中| |Propagation.SUPPORTS|支持当前事务,如果不存在,就不使用事务|否|保证同一个事务中| |Propagation.MANDATORY|支持当前事务,如果不存在,抛出异常|否|保证同一个事务中| |Propagation.REQUIRES_NEW|如果有事务存在,挂起当前事务,创建一个新的事务|否|保证没有在同一个事务中| |Propagation.NOT_SUPPORTED|以非事务方式运行,如果有事务存在,挂起当前事务|否|保证没有在同一个事务中| |Propagation.NEVER|以非事务方式运行,如果有事务存在,抛出异常|否|保证没有在同一个事务中| |Propagation.NESTED|如果当前事务存在,则嵌套事务执行|否|保证没有在同一个事务中|
变量 | 内容 | 描述 |
---|---|---|
Isolation.DEFAULT | 默认隔离级别 | 使用数据库默认的事务隔离级别 |
Isolation.READ_UNCOMMITTED | 未提交读(read uncommited) | 脏读,不可重复读,虚读都有可能发生 |
Isolation.READ_COMMITTED | 已提交读(read commited) | 避免脏读。但是不可重复读和虚读有可能发生(需事务提交) |
Isolation.REPEATABLE_READ | 可重复读(repeatable read) | 避免脏读和不可重复读.但是虚读有可能发生(行锁) |
Isolation.SERIALIZABLE | 串行化的(serializable) | 避免以上所有读问题(表锁) |
Mysql 默认:可重复读
Oracle 默认:读已提交
常见的有C3P0,dbcp,druid,hikari(Springboot默认) 目前FSP和EDS使用的是druid
只要被事务覆盖到的方法都会占用连接,如果随意开启事务,会造成连接池连接连接不够用
//通过@Transactional注解开启事务
//虽然没有操作数据库,但会占用一个连接池中的一个连接
//会直接占用一个连接20S
//此时会产生,数据库没有什么请求,但应用报无法从连接池获取连接
@Transactional
public void findTransactional() {
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
开启事务的连接数
没有开启事务的连接数
报错如下
//通过@Transactional注解开启事务
// 1 中为调用接口,不涉及事务,如果该操作耗时太长,会一直占用连接
@Transactional
public void insertTransactional() {
//1.调用接口获取数据http,doubbo
String code=HttpClient.getCode();
//2. 操作数据库
jdbcTemplate.queryForList("insert into subtable(sub) values('123'));
}
//修改如下,将事务作用在最小单元
public void insertInfo(){
String code=HttpClient.getCode();
insertTransactional
}
@Transactional
public void insertTransactional() {
//2. 操作数据库
jdbcTemplate.queryForList("insert into subtable(sub) values('123'));
}
//开启了事务后,执行update操作,如果查询条件没有索引,会锁表
//在开启事务的情况下,执行update操作 where条件尽量带索引字段
//如果有下面的配置,都要进行修改
//找出需要使用事务的代码(多张表必须同时更新成功的情况),使用@Transactional注解开启事务开启事务
//在配置中删除下面的事务管理切面
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="create*" propagation="REQUIRED" rollback-for="Exception"/>
<tx:method name="update*" propagation="REQUIRED" rollback-for="Exception"/>
<tx:method name="delete*" propagation="REQUIRED" rollback-for="Exception"/>
<tx:method name="do*" propagation="REQUIRED" rollback-for="Exception"/>
<tx:method name="save*" propagation="REQUIRED" rollback-for="Exception"/>
<!-- 上传文件,需要将文件信息添加到数据库中. -->
<tx:method name="upload" propagation="REQUIRED" rollback-for="Exception"/>
<tx:method name="find*" read-only="true"/>
</tx:attributes>
</tx:advice>
@Transactional
public void updateTransactionNestd(int propagation) {
//带事务
nestTransactionSupports();
updateNestd()
}
//@Transactional 相当于 @Transactional(propagation = Propagation.REQUIRED)
@Transactional
public void nestTransactionSupports() {
updateNestd();
}
//事务的传播特性
public void updateNestd() {
logger.info("【{}】开启事务,当前活跃连接数={}", "update", ((DruidDataSource) jdbcTemplate.getDataSource()).getActiveCount());
String id = "nest" + UUID.randomUUID().toString();
String parentTableSql = "insert into parenttable(parent) values('" + id + "')" ;
String subTableSql = "insert into subtable(sub) values('" + id + "')" ;
logger.info("【{}】执行sql={}", "updateNestd", parentTableSql);
jdbcTemplate.update(parentTableSql);
logger.info("【{}】执行sql={}", "updateNestd", subTableSql);
jdbcTemplate.update(subTableSql);
}
//错误写法
@Transactional
public void updateTransactional(String id) {
logger.info("【{}】开启事务,当前活跃连接数={}", "updateTransactional", ((DruidDataSource) jdbcTemplate.getDataSource()).getActiveCount());
String parentTableSql = "insert into parenttable(parent) values('" + id + "')" ;
String subTableSql = "insert into subtable(sub) values('" + id + "')" ;
//捕获了一次无法回滚
try {
jdbcTemplate.update(parentTableSql);
if (true) {
throw new Exception();
}
jdbcTemplate.update(subTableSql);
} catch (Exception e) {
}
}
//正确写法
//1.需要抛出异常
//2.需要添加rollbackFor参数
@Transactional(rollbackFor = Exception.class)
public void updateTransactionalRollback(String id) throws Exception {
logger.info("【{}】开启事务,当前活跃连接数={}", "updateTransactional", ((DruidDataSource) jdbcTemplate.getDataSource()).getActiveCount());
String parentTableSql = "insert into parenttable(parent) values('" + id + "')" ;
String subTableSql = "insert into subtable(sub) values('" + id + "')" ;
jdbcTemplate.update(parentTableSql);
if (true) {
throw new Exception();
}
jdbcTemplate.update(subTableSql);
}
//Integer.parseInt("aaa"); 模拟异常,不进行捕获
//下面也可以回滚
@Transactional
public void updateTransactionalError(String id) {
logger.info("【{}】开启事务,当前活跃连接数={}", "updateTransactional", ((DruidDataSource) jdbcTemplate.getDataSource()).getActiveCount());
String parentTableSql = "insert into parenttable(parent) values('" + id + "')" ;
String subTableSql = "insert into subtable(sub) values('" + id + "')" ;
jdbcTemplate.update(parentTableSql);
Integer.parseInt("aaa");
jdbcTemplate.update(subTableSql);
}
public int insertBatchs(Account account) {
int j=0;
SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH);
AccountMapper accountMapper= sqlSession.getMapper(AccountMapper.class);
for (int i = 0; i < 100; i++) {
account = new Account();
account.setId(40+i);
account.setUserId("test:"+i);
account.setMoney(5+i);
j= accountMapper.insertBatchs(account);
//该处提交过后,真个连接就关闭回收了,后面的数据库操作都会报错
//
sqlSession.commit();
}
sqlSession.commit();
return j;
}
报错如下
<bean id="dataSource"
class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy">
<property name="targetDataSource">
<bean id="dataSourceTarget" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="${db.driver}"/>
<property name="url" value="${db.url}"/>
<property name="username" value="${db.username}"/>
<property name="password" value="${db.password}"/>
<property name="initialSize" value="${db.initialSize:5}"/>
<property name="minIdle" value="${db.minIdle:10}"/>
<property name="maxActive" value="${db.maxActive:20}"/>
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="${db.maxWait:60000}"/>
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="${db.timeBetweenEvictionRunsMillis:60000}"/>
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="${db.minEvictableIdleTimeMillis:300000}"/>
<!--不开启事务,默认自动提交-->
<property name="defaultAutoCommit" value="${db.defaultAutoCommit:true}"/>
<!-- 打开PSCache,如果用Oracle,则把poolPreparedStatements配置为true,mysql可以配置为false。分库分表较多的数据库,建议配置为false -->
<property name="poolPreparedStatements" value="${db.poolPreparedStatements:false}"/>
<!--自动回收已断开链接-->
<property name="validationQuery" value="SELECT 1 FROM DUAL"></property>
<property name="testWhileIdle" value="${db.testWhileIdle:true}"></property>
<property name="testOnBorrow" value="${db.testOnBorrow:false}"></property>
<property name="testOnReturn" value="${db.testOnReturn:false}"></property>
<property name="logAbandoned" value="${db.logAbandoned:true}"></property>
<property name="removeAbandoned" value="${db.removeAbandoned:true}"></property>
<property name="removeAbandonedTimeout" value="${db.removeAbandonedTimeout:3600}"></property>
</bean>
</property>
</bean>
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。