【事务的总结】

1、事务的介绍和分类。

事务的介绍:

就是一组操作数据库的动作集合,事务是现代数据库理论中的核心概念之一。如果一组处理步骤或者全部发生或者一步也不执行,我们称该组处理步骤为一个事务。当所有的步骤像一个操作一样被完整地执行,我们称该事务被提交。由于其中的一部分或多步执行失败,导致没有步骤被提交,则事务必须回滚到最初的系统状态,从而保证时间的一致性。

事务的特征:

原子性 事务是一个完整的操作,事务的各步操作是不可分的要么都执行,要么都不执行。
一致性 当事务完成时,数据处于一致状态。
隔离性 并发事务之间彼此隔离,独立,不应以任何方式依赖于或影响其他事务。
持久性 事务完成后,它对数据库的修改被永久保持。
事务的隔离界别,默认五个级别:
默认级别 默认值,表示使用底层数据库的默认隔离级别,对大部分数据库而言,通常这个值就是读未提交。
读未提交 表示一个事务可以读取另一个事务已经提交的数据,该级别不可以防止脏读,幻读,和不可重复读。
读已提交 表示一个事务只能读取另一个事务已经提交的数据,该级别可以防止脏读,这也是大多数情况的推荐值。
可重复读 表示一个事务在整个过程中,可以多次重复执行某个查询,并且每次返回的记录都相同。
可序列化 所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

事务的传播行为:

REQUIRED 默认值。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
REQUIRES_NEW 创建一个新的事务,如果当前存在事务,则把当前事务挂起。
SUPPORTS 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

NOT_SUPPORTED 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
NEVER 以非事务方式运行,如果当前存在事务,则抛出异常。
MANDATORY 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
NESTED 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 REQUIRED。

2、事务的应用。

Jdbc连接开启事务

/**
 * 修改用户登录密码
 * 
 * @author pan_junbiao
 * @param id          用户编号
 * @param newPassword 新密码
 * @return
 */
public Boolean updateLoginPassword(int id, String newPassword)
{
	Boolean result = false;
	Connection conn = null;
	PreparedStatement preStmt = null;
	DBHelper dbHelper = new DBHelper();
 
	try
	{
		// 获取数据库连接对象
		conn = dbHelper.getConnection();

		// 关闭事务自动提交
		conn.setAutoCommit(false);
 
		// 执行操作
		preStmt = conn.prepareStatement("UPDATE user_info SET login_password = ? WHERE id = ?");
		preStmt.setString(1, newPassword);
		preStmt.setInt(2, id);
		int res = preStmt.executeUpdate();
		result = res > 0 ? true : false;
 
		// 提交事务
		conn.commit();
 
	} catch (SQLException sqle)
	{
		try
		{
			// 回滚事务
			conn.rollback();
		} catch (SQLException e)
		{
			e.printStackTrace();
		}
 
		sqle.printStackTrace();
 
	} finally
	{
		// 释放资源
		dbHelper.closeResource(preStmt, conn);
	}
 
	return result;
} 

sping事务的应用

事务生效的注意点:在Service层使用Transactional注解是可以生效的,但如果事务方法再被同一个类中的非事务方法调用,那么这个事务注解将不会生效。也就是说Transactional注解与Service注解并没有冲突,但是Transactional注解的方法在被其所在类中的非Transactional方法调用时,Transactional注解将不会生效。

@SpringBootApplication
//在springBoot启动时开启事务
@EnableTransactionManagement
public class AssetApplication {

    public static void main(String[] args) {
        SpringApplication.run(AssetApplication.class, args);
    }

}
//rollbackFor事务回滚机制,默认为Exception,也可以指定回滚的类
 @Transactional(rollbackFor = Exception.class)
    public Wrapper<Object> updateBusiness(SysUser user,BusinessSectionDto businessSectionDto) {
        //校验重复数据
        Pair<Boolean, String> booleanStringPair = checkHaveData(businessSectionDto);
        if(booleanStringPair.getLeft()){
            return WrapMapper.wrap(Wrapper.ERROR_CODE,booleanStringPair.getRight());
        }
        //获取原有数据
        BusinessSectionVo oneBusiness = getOneBusiness(businessSectionDto.getSeq());
        if(Objects.isNull(oneBusiness)){
            return WrapMapper.wrap(ErrorEnum.NOT_DATA.getCode(),ErrorEnum.NOT_DATA.getMsg());
        }
        //无效状态编辑则直接进行更新
        if(Constants.STATE_INVALID.equals(oneBusiness.getState())){
            BusinessSectionVo invalidBusiness = new BusinessSectionVo();
            conversion(businessSectionDto,invalidBusiness);
            invalidBusiness.setState(businessSectionDto.getState());
            return WrapMapper.ofOk(updateById(invalidBusiness));
        }
        //删除原有数据
        removeById(oneBusiness.getSeq());
        //生成最新数据
        conversion(businessSectionDto,oneBusiness);
        oneBusiness.setSeq(UUIDUtils.getDataUUID(null,10));
        oneBusiness.setModtime(new Date());
        oneBusiness.setState(Constants.STATE_EFFECTIVE);
        return WrapMapper.ofOk(save(oneBusiness));
    }

sping事务使用的注意点:

1、不要在接口上声明@Transactional,而要在具体类的方法上使用 @Transactional 注解,否则注解可能无效。

2、不要图省事,将@Transactional放置在类级的声明中,放在类声明,会使得所有方法都有事务。故@Transactional应该放在方法级别,不需要使用事务的方法,就不要放置事务,比如查询方法。否则对性能是有影响的。

3、@Transactional 的事务开启 ,或者是基于接口的 或者是基于类的代理被创建。所以在同一个类中一个方法调用另一个方法有事务的方法,事务是不会起作用的 。

4、用REQUIRES_NEW的时候,发现没有起作用
分析了一下,原因是A方法(有事务)调用B方法(要独立新事务),如果两个方法写在同一个类里,spring的事务会只处理能同一个。

解决方案1:需要将两个方法分别写在不同的类里。
解决方案2:方法写在同一个类里,但调用B方法的时候,将service自己注入自己,用这个注入对象来调用B方法。
5、使用了@Transactional的方法,只能是public,@Transactional注解的方法都是被外部其他类调用才有效,故只能是public。道理和上面的有关联。故在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错,但事务无效。

6、默认只有RuntimeExcetion会触发回滚,经验证明确实如此,所以rollbackFor可以自行设置如下:rollbackFor = {Exception.class},当然具体业务具体处理,可能有的业务抛出的某些异常并不需要触发回滚,所以此时应该细化处理异常。

@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor={/自定义Exception,可以是多个/ IOException.class})
上面的这个rollbackFor配置,可以理解为:当配置这个事务的方法内返回了IOException,则会触发回滚操作。

7、MySQL数据库表引擎应为InnoDB,否则不支持事务。

spring事失效的场景

1、 查看数据库中使用的表是否都是 InnoDB 的引擎,有些表为了追求写入的性能,比如记录日志的表,会将引擎改为 MyiSAM,如果数据库引擎不支持,那么添加的事务也就不会生效。

2、 事务要加在 public 方法上,否则事务将不会起作用,如果要用在非 public 方法上,事务不会生效。如果方法被 final 修饰, spring 事务底层使用了 aop, 也就是通过jdk动态代理或者cglib,生成了代理类,而被final 或者 staitc的方法,是不能重写或者代理,因此事务就不会生效。

3、 在事务内部调用本类的另一个事务方法,那么被调用的一方事务不会生效。因为spring 的事务是使用动态代理生效的,本类内部的调用相当于是this.method 的调用,没有经过spring代理所以事务不会生效。如果非要在类内部进行调用,需要按照如下图操作,updateUserInfo 调用方法 modifyUserInfo,需要使用 AopContext获取当前对象进行调用执行。同时还要设置 proxyTargetClass= true,改值默认为 false。
4、 注意事务设置的隔离级别,如果项目中配置了多个数据源,就会有多个事务管理器,在执行事务的时候需要指定其事务管理器。事务的传播属性和隔离级别是默认的,超时时间默认是 -1,不设置超时时间。异常回滚和异常不回滚需要根据业务的情况进行设置。
5、 方法内部不能将异常 try-catch ,如果方法内部都把异常给处理了,那么事务捕捉不到异常信息,事务自然不会回滚,也就不会生效。方法内部把异常信息给处理了,就不能被事务Aop 拦截进行处理,自然就会进行回滚操作。

6、 建议事务 @Transactional 注解需要在具体的实现类上进行标注,而不是在接口层标注。注解是不能继承的,所以只有在基于接口的代理(PROXY)时事务才生效。如果正在使用基于类(AspectJ)代理时,那么注解不会被基于类的代理识别,对象也不会被事务代理所包装,因此事务是不能生效的。

7、 多线程调用,在项目开发过程中多线程的应用场景是很多的,在多线程的场景下,主线程所在的 Context 和多线程执行下的方法不在同一个上下文,我们知道 spring 的事务是通过数据库连接来实现的,数据库连接是存放在一个 ThreadLocal 中的,只有在同一个线程上下文下,事务的操作才能生效。

8、在事务方法中加锁,可能会导致锁失效

无论是Java自带的锁,还是分布式锁,都有可能出现没锁住的情况

原因是解锁先于事务提交,一旦锁释放后其它线程就可以获取到锁了,由于事务还没提交,所以新线程读到的还是旧数据(跟前一个线程读取到的数据是一样的),这就相当于多个线程做了一模一样的事情了。

事务即便生效也不一定能回滚

通过 AOP 实现事务处理可以理解为,使用 try…catch…来包裹标记了 @Transactional 注解的方法,当方法出现了异常并且满足一定条件的时候,在 catch 里面我们可以设置事务回滚,没有异常则直接提交事务。

这个的一定条件满足以下两条:

只有异常传播出了标记了 @Transactional 注解的方法,事务才能回滚

默认情况下,出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务。

3.分布式事务

分布式事务的解决

ChatGPT
分布式系统中实现事务的方式有多种,具体选择取决于系统的需求、复杂性和可靠性要求。以下是一些常见的分布式事务实现方式:

两阶段提交(Two-Phase Commit, 2PC):
两阶段提交是一种经典的分布式事务处理协议,它确保所有参与者要么都提交,要么都回滚。在第一阶段,协调者向所有参与者发送准备请求,并等待它们的响应。只有当所有参与者都准备就绪时,才会进入第二阶段,在此阶段协调者向所有参与者发送提交请求或回滚请求。
补偿事务(Compensating Transaction):
补偿事务是另一种用于处理分布式事务的方法。在这种方法中,首先执行主事务,然后如果发生失败,系统会执行一系列补偿操作来回滚或修正之前的操作。这种方法比两阶段提交更具有容错性,因为它不依赖于全局锁定,并且可以在系统失败时进行恢复。
消息队列:
使用消息队列来实现分布式事务是一种流行的方法。在这种模式下,事务操作被封装为消息,并通过消息队列传递。每个消息被视为一个独立的事务单元,由消息队列负责传递和处理。通过消息队列的事务特性,可以确保消息的可靠传递和处理,从而实现了分布式事务。
Saga模式:
Saga是一种用于处理长时间和复杂事务的模式。Saga将长时间事务分解为一系列较小的、可撤销的事务单元。每个事务单元都负责执行一部分操作,并且具有补偿操作来回滚或修正之前的操作。通过这种方式,Saga模式可以实现分布式系统中的事务管理,同时保持系统的可靠性和可扩展性。
分布式事务协调器:
分布式事务协调器是一种专门用于管理分布式事务的中间件。它负责协调分布式事务的各个阶段,并确保事务的一致性和可靠性。一些常见的分布式事务协调器包括Atomikos、Narayana、Hazelcast等。

rocketMq实现分布式事务

事务消息生产者注册事务监听器:
生产者在发送事务消息之前,需要注册一个事务监听器(TransactionListener)。事务监听器负责实现本地事务的执行和事务状态的反馈给 RocketMQ。该监听器有两个方法:
executeLocalTransaction:执行本地事务的方法。在此方法中执行本地事务,并返回事务执行的结果(提交、回滚或未知)。
checkLocalTransaction:检查本地事务状态的方法。RocketMQ 在执行事务消息时会调用该方法,以获取本地事务的状态。
发送事务消息:
生产者通过发送事务消息(Transactional Message)来触发本地事务的执行。事务消息包含事务监听器注册时指定的业务参数。
执行本地事务:
在 executeLocalTransaction 方法中,生产者执行本地事务。这可以是与数据库交互、调用其他服务或执行任何其他需要原子性保证的操作。事务的成功或失败状态将在此方法中确定。
提交或回滚事务:
在 executeLocalTransaction 方法中,根据本地事务的执行结果,生产者需根据情况返回事务的提交状态(TransactionStatus.CommitTransaction)或回滚状态(TransactionStatus.RollbackTransaction)。如果无法确定事务的状态,则返回未知状态(TransactionStatus.Unknown)。
事务消息确认:
RocketMQ 根据生产者返回的事务状态来确认事务消息。如果事务监听器返回了提交状态,则 RocketMQ 确认事务消息已提交;如果返回了回滚状态,则 RocketMQ 回滚事务消息。
检查本地事务状态:
在 checkLocalTransaction 方法中,RocketMQ 会调用生产者注册的事务监听器来检查本地事务的状态。如果事务消息的确认超时或者生产者宕机等原因导致 RocketMQ 无法获取事务状态,则 RocketMQ 将回调该方法以检查事务状态。生产者应在此方法中返回本地事务的最终状态。

seata应用

在这里插入图片描述
步骤 1:建立数据库
创建 UNDO_LOG 表
SEATA AT 模式需要 UNDO_LOG 表。你可以通过 github 获取到指定版本的undo log SQL 脚本.

CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
    ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`); 
  • 9
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值