讲讲事务处理

107 阅读12分钟

一篇文章说清楚事务

事务是什么

事务就是一个或多个操作的共进退,要么都执行,要么都不执行

事务要做什么

我们总喜欢讲事务的四个特性,原子性(Atomicity),一致性(Consistency),隔离型(Isolation)以及持久性(Durability)等,在我理解,这四大特性只是在各自的维度表述了事务要做的事情。

原子性:原子性是指一个事务是一个不可分割的操作单位,其中的操作要么都做,要么都不做;如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态

持久性:持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

隔离性:隔离性注重的是事务之间的影响,指的是事务内部的操作和其他事务是隔离的。

一致性:一致性是指事务结束后不会影响数据的约束状态,指的是从一个正确的状态到达另一个正确的状态

事务的数据库实现原理

关系型数据库:mysql

mysql的日志:二进制日志(binlog),错误日志,查询日志,慢查询日志,事务则是使用redo log(重做日志) 和 undo log(回滚日志)

image.png

原子性实现

当事务对数据库进行修改时,innoDB会生成对应的undolog,如果事务执行失败或调用了rollback,导致事务需要回滚,可以使用undo log中的信息将数据回滚到修改前

undo log存放的是sql相关的执行信息,当发生回滚时,innoDB会根据undo log的内容做之前相反的工作: 每个insert语句回滚时执行delete; 每个delete语句回滚时执行insert; 每个update语句回滚时执行update;

持久性实现

数据库存放数据存在一个缓存buffer pool, 并且会在redo log中记录操作,mysql宕机后重启会执行存储的操作信息进行数据恢复

隔离性实现

innoDB通过锁实现隔离性,事务在修改数据之前,需要先获取相应的锁,获取锁之后事务就可以修改数据,该事务操作期间数据是锁定的,其他事务需要修改数据需要等待事务提交或者回滚释放锁

查看锁语句:

select * from information_schema.innodb_locks; 
#锁的概况
show engine innodb status; 
#InnoDB整体状态,其中包括锁的情况

事务隔离级别

image.png

一致性实现

数据库本身的保障,保证数据规范不被更改即可

文档型数据库:mongoDb

内存型数据库:redis

spring事务的集成

jdbc控制事务

    Connection conn = null;
    Statement stmt = null;
    ResultSet rs = null;
    try{
        // 1.注册 JDBC 驱动
        Class.forName("com.mysql.jdbc.Driver");
        // 2.创建链接
        System.out.println("连接数据库...");
        conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/my_db","root","root");
        //设置事务隔离等级 
        conn.setTransactionIsolation(2)
        //关闭事务自动提交
        conn.setAutoCommit(false);
        // 3.发起请求
        stmt = conn.createStatement();
        String sql = "SELECT id, name, url FROM websites";
        rs = stmt.executeQuery(sql);
        // 4.输出结果
        System.out.print("查询结果:" + rs);
        //手动提交事务
        con.commit();
        // 关闭资源(演示代码,不要纠结没有写在finally中)
        rs.close();
        stmt.close();
        conn.close();
    } catch (SQLException se)
        se.printStackTrace();
    }catch(Exception e){
        e.printStackTrace();
    }

jdbc提供的api可以实现在java代码中控制事务

spring管理事务

spring封装了jdbc,实现了编程式事务和声明式事务两种形式

编程式事务:

try{
    transactionManager.commit(status);
}catch(Exception e){
    transactionManager.rollback(status);
    throw new InvoiceApplyException("异常")
}

由于需要手动创建事务管理器,代码耦合度太高,不推荐使用

声明式事务: 基于aop切面实现,使用@Transational注解,spring的隔离级别比数据库的隔离级别多一个默认级别,默认级别就是使用数据库的默认级别

spring事务传播

支持当前事务,如果不存在则创建新事务(默认配置) 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),

如果当前事务存在,则在嵌套事务中执行,否则行为类似于 REQUIRED NESTED(TransactionDefinition.PROPAGATION_NESTED);

事务传播有七个传播级别,默认都是使用required,加入当前事务

spring事务失效

1.数据库不支持事务 spring的事务其实还是基于数据的事务支持,数据库不支持事务,则失效 2.方法没有被public修饰 在AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。 3.方法用final修饰 spring事务是基于aop动态代理实现的,final修饰的方法不能被代理,所以失效 4.方法内部调用 同一个类中的两个方法分别为ab,a调用b,b的事务注解失效

解决办法:

一:不处理,在屎山里一直加代码,一个注解保证所有的事务

二:新加一个service方法

三:在service里注入自己

 @Autowired
    private Service service;

四:通过aopconntent类获取代理service 在启动类加上@EnableAspectJAutoProxy(exposeProxy = true)

/**
     * 通过AopContext获取代理类
     * @return StudentService代理类
     */
    private StudentService getService(){
        return Objects.nonNull(AopContext.currentProxy()) ? (StudentService)AopContext.currentProxy() : this;
    }

五:多线程调用 同一个事务其实是同一个数据库连接,多线程是多个数据库连接就是不同的事务

多数据源事务

两阶段提交:

第一阶段:首先多个数据源的事务都开启,然后各事务分别去执行对应的sql

第二阶段:如果都成功就提交事务,只要有一个失败就把事务全部回滚

使用aop实现:

/**
 * 多数据源事务注解
 *
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MultiDataSourceTransactional {
 
    /**
     * 事务管理器数组
     */
    String[] transactionManagers();
}
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
 
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
 
/**
 * 多数据源事务切面
 * ※采用Around似乎不行※
 *
 */
@Component
@Aspect
public class MultiTransactionAop {
	/**
     * 线程本地变量:为什么使用栈?※为了达到后进先出的效果※
     */
	private static final ThreadLocal<Stack<Map<DataSourceTransactionManager, TransactionStatus>>> THREAD_LOCAL = new ThreadLocal<>();
	
    /**
     * 用于获取事务管理器
     */
    @Autowired
    private ApplicationContext applicationContext;
 
    /**
     * 事务声明
     */
    private DefaultTransactionDefinition def = new DefaultTransactionDefinition();
    {
        // 非只读模式
        def.setReadOnly(false);
        // 事务隔离级别:采用数据库的
        def.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT);
        // 事务传播行为
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    }
 
    /**
     * 切点
     */
    @Pointcut("@annotation(MultiTransactional注解路径)")
    public void pointcut() {
    }
 
    /**
     * 声明事务
     *
     * @param transactional 注解
     */
    @Before("pointcut() && @annotation(transactional)")
    public void before(MultiDataSourceTransactional transactional) {
        // 根据设置的事务名称按顺序声明,并放到ThreadLocal里
        String[] transactionManagerNames = transactional.transactionManagers();
        Stack<Map<DataSourceTransactionManager, TransactionStatus>> pairStack = new Stack<>();
        for (String transactionManagerName : transactionManagerNames) {
            DataSourceTransactionManager transactionManager = applicationContext.getBean(transactionManagerName, DataSourceTransactionManager.class);
            TransactionStatus transactionStatus = transactionManager.getTransaction(def);
            Map<DataSourceTransactionManager, TransactionStatus> transactionMap = new HashMap<>();
            transactionMap.put(transactionManager, transactionStatus);
            pairStack.push(transactionMap);
        }
        THREAD_LOCAL.set(pairStack);
    }
 
    /**
     * 提交事务
     */
    @AfterReturning("pointcut()")
    public void afterReturning() {
        // ※栈顶弹出(后进先出)
        Stack<Map<DataSourceTransactionManager, TransactionStatus>> pairStack = THREAD_LOCAL.get();
        while (!pairStack.empty()) {
            Map<DataSourceTransactionManager, TransactionStatus> pair = pairStack.pop();
            pair.forEach((key,value)->key.commit(value));
        }
        THREAD_LOCAL.remove();
    }
 
    /**
     * 回滚事务
     */
    @AfterThrowing(value = "pointcut()")
    public void afterThrowing() {
        // ※栈顶弹出(后进先出)
        Stack<Map<DataSourceTransactionManager, TransactionStatus>> pairStack = THREAD_LOCAL.get();
        while (!pairStack.empty()) {
        	Map<DataSourceTransactionManager, TransactionStatus> pair = pairStack.pop();
            pair.forEach((key,value)->key.rollback(value));
        }
        THREAD_LOCAL.remove();
    }
}

思路就是创建一个事务列表,在具体的方法上使用注解选择对应的数据库连接,最后统一提交事务

分布式事务

真正处理多数据源,微服务或者多线程之间的事务,需要使用分布式事务来处理

方案:

一:两阶段提交

思路就是将事务提交拆分为准备阶段和提交阶段,然后统一交给事务协调者管理(TC)

缺点:

1:单点故障,协调者发送准备命令前,或者发送提交回滚命令后宕机可以接受,但发送准备后到提交前宕机,数据库会一直持有锁不释放

2:需要等所有参与者处理完毕

3:数据一致性无法保证

二:三阶段提交

基于二阶段提交,优化一致持有锁不释放的问题,增加了预提交阶段,分为:准备、预提交、提交 避免了因为一个数据源无法提交,导致所有数据库上锁的问题,但是性能更差,一致性问题也未解决

三:TCC

不建议使用 image.png 四:可靠消息队列 本地事务执行后等待定时反查事务状态确认订阅方是否执行事务,实际上只是实现最终一致性 image.png

五:SAGA事务

正向补偿或者反向补偿概念,将大事务拆分为每个小事务,每个事务都是原子行为,如果有失败的就进行补偿,正向补偿,一致尝试失败的事务,直至成功;反向补偿,撤销其他事务

六:AT事务 也是基于两阶段提交协议实现的,针对两阶段提交的必须等待最慢的事务完成后才能统一释放锁的问题,at事务在业务数据提交时模仿数据库的redo和undo日志,自动拦截了所有的sql,保存了数据库的重做和回滚日志,如果成功了,清理每个数据源的日志数据即可,如果失败了需要回滚,就根据日志数据自动执行逆向的sql用于补偿,所以每个数据源可以立即执行操作

消息队列实现最终一致性

案例:用户购买商品后需要添加积分,但订单和积分属于两个服务,需要考虑最终一致性

方案一:不使用mq的事务消息

基本思路: 订单服务新建一个任务表,将积分的操作任务存储到数据库中,设置定时任务扫描任务表,发送任务数据到mq,发送未成功尝试重新发送,

积分服务接收消息,使用redis查询唯一任务id的方式确保不被重复消费,修改用户积分,删除redis中的任务,执行完毕,若业务执行失败则通知订单服务进行失败补偿

image.png

mq设计:添加积分消息队列和完成添加消息队列,使用一个交换机即可,设计不同的路由key;

定时任务设计:读取任务表,将需要执行的任务定时推送至消息队列

redis:使用任务id确保分布式锁

避免消息重复:监听消息队列,如果队列中消息则删除原任务防止消息重复消费

总结:将分布式事务拆分为本地事务,并且做好补偿机制

方案二:使用事务消息

基本思路:使用mq的事务消息

概念:半消息(预处理消息,该状态的消息暂时不能被消费者消费,当一条事务消息被传递到briker上,但是broker并没有收到二次确认时,消息处于不可消费状态),消息状态回查(由于网络原因或者其他原因导致半消息的二次提交长时间未发送,此时broker会主动回查,查询事务状态)

官网示例图:

image.png

使用事务消息确保本地事务和消息的发送,消费者端的事务生效与否还需要做对应的处理和回滚

生产者

@RestController
@Slf4j
public class RocketMqController {
 
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
 
    @GetMapping("sendMqTransaction")
    public Object sendMqTransaction() {
        int i = new Random().nextInt(1000);
        String name = "name" + i;
        MqMessage message = MqMessage.builder().name("事务消息" + i).msg("这是事务消息" + i).build();
        Message<MqMessage> mqMessage = MessageBuilder.withPayload(message).setHeader("key", name).build();
        User user = new User();
        user.setName(name);
        user.setSex("" + i);
        log.info("sex:{}", i);
        TransactionSendResult transactionSendResult = rocketMQTemplate.sendMessageInTransaction(MqUtil.tx_group, MqUtil.tx_topic, mqMessage , user);
        return transactionSendResult;
    }
 
}

常量值

public class MqUtil {
 
    public static final String  tx_topic = "tx_topic";
 
    public static final String tx_group = "tx_producer_group";
}

生产者监听

@Slf4j
@Service
//@RocketMQTransactionListener表明这个一个生产端的消息监听器,需要配置监听的事务消息生产者组。
// 实现RocketMQLocalTransactionListener接口,重写执行本地事务的方法和检查本地事务方法
@RocketMQTransactionListener(txProducerGroup = MqUtil.tx_group)
public class TxProducerListener implements RocketMQLocalTransactionListener {
 
    @Autowired
    private UserMapper userMapper;
 
 
    /**
     * 每次推送消息会执行executeLocalTransaction方法,首先会发送半消息,到这里的时候是执行具体本地业务,
     * 执行成功后手动返回RocketMQLocalTransactionState.COMMIT状态,
     * 这里是保证本地事务执行成功,如果本地事务执行失败则可以返回ROLLBACK进行消息回滚。 此时消息只是被保存到broker,并没有发送到topic中,broker会根据本地返回的状态来决定消息的处理方式。
     * @param msg
     * @param arg
     * @return
     */
    @Override
    @Transactional
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        log.info("开始执行本地事务");
        User u = (User) arg;
        userMapper.insert(u);
        if (Integer.parseInt(u.getSex()) % 2 == 0) {
            int i = 1 / 0;
            //这个地方抛出异常,消息状态会是UNKNOWN状态
        }
        log.info("本地事务提交");
        return RocketMQLocalTransactionState.COMMIT;
    }
 
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        log.info("开始执行回查");
        String key = msg.getHeaders().get("key").toString();
        User u = new User();
        u.setName(key);
        List<User> select = userMapper.select(u);
        if (CollectionUtils.isEmpty(select)) {
            log.info("回滚半消息");
            return RocketMQLocalTransactionState.ROLLBACK;
        }
        log.info("提交半消息");
        return RocketMQLocalTransactionState.COMMIT;
    }
}

生产者常量值

// COMMIT:即生产者通知Rocket该消息可以消费
RocketMQLocalTransactionState.COMMIT;
// ROLLBACK:即生产者通知Rocket将该消息删除
RocketMQLocalTransactionState.ROLLBACK;
// UNKNOWN:即生产者通知Rocket继续查询该消息的状态
RocketMQLocalTransactionState.UNKNOWN;

消费者

@Slf4j
@Component
@RocketMQMessageListener(
        topic = MqUtil.tx_topic,
        consumerGroup = "tx_consumer_group")
public class TxConsumerListener implements RocketMQListener<MqMessage>{
    @Override
    public void onMessage(MqMessage message) {
        log.info("{}收到消息:{}", this.getClass().getSimpleName(), message);
    }
 
}

总结:分布式事务使用mq实现,第一步就是需要实现消息和生产者本地的事务,无论是定时发送等待返回还是事务消息都是达到这个目的,针对不同的方案会产生对应的问题,重复消息,重复消费,超时等问题,可以使用分布式锁,业务中唯一主键等方案去处理。但无论哪种方案,消费方的事务都是无法保证稳定执行的,一定需要考虑如何补偿

seata实现分布式事务

原理

实现