04 Seata 开发指南

04 Seata 开发指南

1 API支持

1.1 概述

Seata API 分为两大类:High-Level API 和 Low-Level API :

  • High-Level API :用于事务边界定义、控制及事务状态查询。
  • Low-Level API :用于控制事务上下文的传播。

1.2 High-Level API

1.2.1 GlobalTransaction

全局事务:包括开启事务、提交、回滚、获取当前状态等方法。

public interface GlobalTransaction {

    /**
     * 开启一个全局事务(使用默认的事务名和超时时间)
     */
    void begin() throws TransactionException;

    /**
     * 开启一个全局事务,并指定超时时间(使用默认的事务名)
     */
    void begin(int timeout) throws TransactionException;

    /**
     * 开启一个全局事务,并指定事务名和超时时间
     */
    void begin(int timeout, String name) throws TransactionException;

    /**
     * 全局提交
     */
    void commit() throws TransactionException;

    /**
     * 全局回滚
     */
    void rollback() throws TransactionException;

    /**
     * 获取事务的当前状态
     */
    GlobalStatus getStatus() throws TransactionException;

    /**
     * 获取事务的 XID
     */
    String getXid();

}

1.2.2 GlobalTransactionContext

GlobalTransaction 实例的获取需要通过 GlobalTransactionContext:

 /**
     * 获取当前的全局事务实例,如果没有则创建一个新的实例。
     */
    public static GlobalTransaction getCurrentOrCreate() {
        GlobalTransaction tx = getCurrent();
        if (tx == null) {
            return createNew();
        }
        return tx;
    }

    /**
     * 重新载入给定 XID 的全局事务实例,这个实例不允许执行开启事务的操作。
     * 这个 API 通常用于失败的事务的后续集中处理。
     * 比如:全局提交超时,后续集中处理通过重新载入该实例,通过实例方法获取事务当前状态,并根据状态判断是否需要重试全局提交操作。
     */
    public static GlobalTransaction reload(String xid) throws TransactionException {
        GlobalTransaction tx = new DefaultGlobalTransaction(xid, GlobalStatus.UnKnown, GlobalTransactionRole.Launcher) {
            @Override
            public void begin(int timeout, String name) throws TransactionException {
                throw new IllegalStateException("Never BEGIN on a RELOADED GlobalTransaction. ");
            }
        };
        return tx;
    }

1.2.3 TransactionalTemplate

事务化模板:通过上述 GlobalTransaction 和 GlobalTransactionContext API 把一个业务服务的调用包装成带有分布式事务支持的服务。

public class TransactionalTemplate {

    public Object execute(TransactionalExecutor business) throws TransactionalExecutor.ExecutionException {

        // 1. 获取当前全局事务实例或创建新的实例
        GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();

        // 2. 开启全局事务
        try {
            tx.begin(business.timeout(), business.name());

        } catch (TransactionException txe) {
            // 2.1 开启失败
            throw new TransactionalExecutor.ExecutionException(tx, txe,
                TransactionalExecutor.Code.BeginFailure);

        }

        Object rs = null;
        try {
            // 3. 调用业务服务
            rs = business.execute();

        } catch (Throwable ex) {

            // 业务调用本身的异常
            try {
                // 全局回滚
                tx.rollback();

                // 3.1 全局回滚成功:抛出原始业务异常
                throw new TransactionalExecutor.ExecutionException(tx, TransactionalExecutor.Code.RollbackDone, ex);

            } catch (TransactionException txe) {
                // 3.2 全局回滚失败:
                throw new TransactionalExecutor.ExecutionException(tx, txe,
                    TransactionalExecutor.Code.RollbackFailure, ex);

            }

        }

        // 4. 全局提交
        try {
            tx.commit();

        } catch (TransactionException txe) {
            // 4.1 全局提交失败:
            throw new TransactionalExecutor.ExecutionException(tx, txe,
                TransactionalExecutor.Code.CommitFailure);

        }
        return rs;
    }

}

模板方法执行的异常:ExecutionException

 class ExecutionException extends Exception {

        // 发生异常的事务实例
        private GlobalTransaction transaction;

        // 异常编码:
        // BeginFailure(开启事务失败)
        // CommitFailure(全局提交失败)
        // RollbackFailure(全局回滚失败)
        // RollbackDone(全局回滚成功)
        private Code code;

        // 触发回滚的业务原始异常
        private Throwable originalException;

外层调用逻辑 try-catch 这个异常,根据异常编码进行处理:

  • BeginFailure (开启事务失败):getCause() 得到开启事务失败的框架异常,getOriginalException() 为空。
  • CommitFailure (全局提交失败):getCause() 得到全局提交失败的框架异常,getOriginalException() 为空。
  • RollbackFailure (全局回滚失败):getCause() 得到全局回滚失败的框架异常,getOriginalException() 业务应用的原始异常。
  • RollbackDone (全局回滚成功):getCause() 为空,getOriginalException() 业务应用的原始异常。

1.3 Low-Level API

1.3.1 RootContext

事务的根上下文:负责在应用的运行时,维护 XID 。

High-Level API 的实现都是基于 RootContext 中维护的 XID 来做的。

应用的当前运行的操作是否在一个全局事务的上下文中,就是看 RootContext 中是否有 XID。

RootContext 的默认实现是基于 ThreadLocal 的,即 XID 保存在当前线程上下文中。

 /**
     * 得到当前应用运行时的全局事务 XID
     */
    public static String getXID() {
        return CONTEXT_HOLDER.get(KEY_XID);
    }

    /**
     * 将全局事务 XID 绑定到当前应用的运行时中
     */
    public static void bind(String xid) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("bind " + xid);
        }
        CONTEXT_HOLDER.put(KEY_XID, xid);
    }

    /**
     * 将全局事务 XID 从当前应用的运行时中解除绑定,同时将 XID 返回
     */
    public static String unbind() {
        String xid = CONTEXT_HOLDER.remove(KEY_XID);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("unbind " + xid);
        }
        return xid;
    }

    /**
     * 判断当前应用的运行时是否处于全局事务的上下文中
     */
    public static boolean inGlobalTransaction() {
        return CONTEXT_HOLDER.get(KEY_XID) != null;
    }

1.3.2 远程调用事务上下文的传播

远程调用前获取当前 XID:

String xid = RootContext.getXID();

远程调用过程把 XID 也传递到服务提供方,在执行服务提供方的业务逻辑前,把 XID 绑定到当前应用的运行时:

RootContext.bind(rpcXid);

1.3.3 事务的暂停和恢复

在一个全局事务中,如果需要某些业务逻辑不在全局事务的管辖范围内,则在调用前,把 XID 解绑:

String unbindXid = RootContext.unbind();

待相关业务逻辑执行完成,再把 XID 绑定回去,即可实现全局事务的恢复:

RootContext.bind(unbindXid);

2 微服务框架支持

2.1 事务上下文

Seata 的事务上下文由 RootContext 来管理。应用开启一个全局事务后,RootContext 会自动绑定该事务的 XID,事务结束(提交或回滚完成),RootContext 会自动解绑 XID。

应用可以通过 RootContext 的 API 接口来获取当前运行时的全局事务 XID。

应用是否运行在一个全局事务的上下文中,就是通过 RootContext 是否绑定 XID 来判定的。

2.2 事务传播

Seata 全局事务的传播机制就是指事务上下文的传播,根本上,就是 XID 的应用运行时的传播方式。

2.2.1 服务内部的事务传播

默认的,RootContext 的实现是基于 ThreadLocal 的,即 XID 绑定在当前线程上下文中。

所以服务内部的 XID 传播通常是天然的通过同一个线程的调用链路串连起来的。默认不做任何处理,事务的上下文就是传播下去的。

如果希望挂起事务上下文,则需要通过 RootContext 提供的 API 来实现。

2.2.2 跨服务调用的事务传播

通过上述基本原理,我们可以很容易理解:

跨服务调用场景下的事务传播,本质上就是要把 XID 通过服务调用传递到服务提供方,并绑定到 RootContext 中去。

只要能做到这点,理论上 Seata 可以支持任意的微服务框架。

3 ORM框架支持

Seata 虽然是保证数据一致性的组件,但对于 ORM 框架并没有特殊的要求,像主流的Mybatis,Mybatis-Plus,Spring Data JPA, Hibernate等都支持。

这是因为ORM框架位于JDBC结构的上层,而 Seata 的 AT,XA 事务模式是对 JDBC 标准接口操作的拦截和增强。

4 数据源支持

4.1 AT模式

AT模式支持的数据库有:MySQL、Oracle、PostgreSQL、 TiDB、MariaDB。

4.2 TCC模式

TCC模式不依赖数据源(1.4.2版本及之前),1.4.2版本之后增加了TCC防悬挂措施,需要数据源支持。

4.3 Saga模式

Saga模式不依赖数据源。

4.4 XA模式

XA模式只支持实现了XA协议的数据库。Seata支持MySQL、Oracle、PostgreSQL和MariaDB。

5 SQL参考

5.1 SQL限制

Seata 事务目前支持** INSERT、UPDATE、DELETE **三类 DML 语法的部分功能,这些类型都是已经经过Seata开源社区的验证。SQL 的支持范围还在不断扩大,建议在本文限制的范围内使用。如果您有意帮助社区支持更多类型的SQL,请提交PR申请。

5.1.1 使用限制

  • 不支持 SQL 嵌套
  • 不支持多表复杂 SQL
  • 不支持存储过程、触发器
  • 部分数据库不支持批量更新,在使用 MySQL、Mariadb、PostgreSQL9.6+作为数据库时支持批量。
    批量更新方式如下以 Java 为例:
  // use JdbcTemplate
    public void batchUpdate() {
        jdbcTemplate.batchUpdate(
            "update storage_tbl set count = count -1 where id = 1",
            "update storage_tbl set count = count -1 where id = 2"
		);
    }

    // use Statement
    public void batchUpdateTwo() {
        statement.addBatch("update storage_tbl set count = count -1 where id = 1");
        statement.addBatch("update storage_tbl set count = count -1 where id = 2");
        statement.executeBatch();
    }

5.2 DML语句

类型SQL 实例是否支持
INSERT`INSERT INTO tb1_name (col_name,…) VALUES ({exprFAULT},…),(…),… 或 INSERT INTO tb1_name SET col_name={expr
UPDATEUPDATE tb1_nameSET col_name1=expr1 [, col_name2=expr2 …][WHERE where_definition]
DELETEDELETE FROM tb1_name [WHERE where_definition]
SELECTSELECT [ALLDISTINCT
REPLACE`REPLACE [LOW_PRIORITYDELAYED][INTO] tb1_name [(col_name,…)]VALUES ({expr
TRUNCATETRUNCATE [TABLE] tb1_name

5.3 SQL修饰

Seata 的隔离级别默认为读未提交,该模式下本文表中的 select 语句的 SQL 修饰用法都是支持的;

使用 for update 代理可以让 Seata 达到****读已提交,该模式下 select 语句的用法只能部分支持。

5.3.1 DML语句支持

类型SQL 实例是否支持
AND & ORUPDATE … WHERE col_name1=expr1 AND col_name2= expr2
LIKEUPDATE … WHERE col_name1 LIKE ‘NE’
通配符UPDATE … WHERE col_name1 LIKE ‘NE%’
BETWEENUPDATE … WHERE col_name1 BETWEEN expr1 AND expr2
ON DUPLICATEINSERT INTO tb1_name [(col_name,…)]VALUES ({exprDEFAULT},…),(…),…[ ON DUPLICATE KEY UPDATE col_name=expr, … ]

5.3.2 Select语句支持

类型SQL 实例读未提交读已提交
AND & ORSELECT * FROM tb1_name WHERE col_name1=expr1 AND col_name2= expr2
ORDER BYSELECT col_name1, col_name2 FROM tb1_name ORDER BY col_name1
GROUP BYSELECT col_name1, col_name2 FROM tb1_name GROUP BY col_name1
LIKESELECT col_name1, col_name2 FROM tb1_name WHERE col_name1 LIKE ‘NE’
通配符SELECT col_name1, col_name2 FROM tb1_name WHERE col_name1 LIKE ‘NE%’
EXISTSSELECT col_name1, col_name2 FROM tb1_name WHERE EXISTS (expr1)
INSELECT col_name1, col_name2 FROM tb1_name WHERE col_name1 IN (expr1, expr2,…)
BETWEENSELECT col_name1, col_name2 FROM tb1_name WHERE col_name1 BETWEEN expr1 AND expr2
ON DUPLICATEINSERT INTO tb1_name [(col_name,…)]VALUES ({exprDEFAULT},…),(…),…[ ON DUPLICATE KEY UPDATE col_name=expr, … ]
ALIASESSELECT t1. col_name1, t2.col_name2 FROM tb1_name AS t1, tb2_name AS t2 WHERE t1. col_name=expr AND t2. col_name=expr
TOPSELECT TOP 2 * FROM tb1_name
LIMITSELECT col_name1, col_name2 FROM tb1_name LIMIT 5
JOININNER JOINLEFT JOINRIGHT JOINFULL JOINSELECT col_name1, col_name2 FROM tb1_name JOIN tb2_name>ON tb1_name. col_name1= tb2_name. col_name1
UNIONUNION ALLSELECT INTOSELECT col_name1, col_name2 FROM tb1_name UNION SELECT col_name1, col_name2 FROM tb2_name

5.4 函数

在本文下面的表列出了 DQL 语句和 DML 语句对函数的支持情况。需要注意的是,在 DML 语句中使用函数,不能将其作为主键。

5.4.1 DML语句支持

类型是否支持
CONCAT(string2[,…])
INSTR(string,substring)
LCASE(string2)
LEFT(string2,length)
LENGTH(string)
LOAD_FILE(file_name)
LOCATE(substring,string[,start_position])
LPAD(string2,length,pad)
LTRIM(string2)
REPEAT(string2,count)
REPLACE(str,search_str,replace_str)
RPAD(string2,length,pad)
RTRIM(string2)
STRCMP(string1,string2)
SUBSTRING(str,position[,length])
TRIM([[BOTHLEADING
UCASE(string2)
RIGHT(string2,length)
SPACE(count)
ABS(number2)
BIN(decimal_number)
CEILING(number2)
CONV(number2,from_base,to_base)
FLOOR(number2)
FORMAT(number,decimal_places)
HEX(DecimalNumber)
LEAST(number,number2[,…])
MOD(numerator,denominator)
POWER(number,power)
RAND([seed])
ROUND(number[,decimals])
SIGN(number2)
SQRT(number2)
ADDTIME(date2,time_interval)
CONVERT_TZ(datetime2,fromTZ,toTZ)
CURRENT_DATE()
CURRENT_TIME()
CURRENT_TIMESTAMP()
DATE(datetime)
DATE_ADD(date2,INTERVALd_valued_type)
DATE_FORMAT(datetime,FormatCodes)
DATE_SUB(date2,INTERVALd_valued_type)
DATEDIFF(date1,date2)
DAY(date)
DAYNAME(date)
DAYOFWEEK(date)
DAYOFYEAR(date)
EXTRACT(interval_nameFROMdate)
MAKEDATE(year,day)
MAKETIME(hour,minute,second)
MONTHNAME(date)
NOW()
SEC_TO_TIME(seconds)
STR_TO_DATE(string,format)
TIMEDIFF(datetime1,datetime2)
TIME_TO_SEC(time)
WEEK(date_time[,start_of_week])
YEAR(datetime)
DAYOFMONTH(datetime)
HOUR(datetime)
LAST_DAY(date)
MICROSECOND(datetime)
MONTH(datetime)
MINUTE(datetime)
FIRST()
LAST()
MIN()
MAX()
AVG()
SUM()
COUNT()

5.4.2 DQL语句支持

类型读未提交读已提交
CONCAT(string2[,…])
INSTR(string,substring)
LCASE(string2)
LEFT(string2,length)
LENGTH(string)
LOAD_FILE(file_name)
LOCATE(substring,string[,start_position])
LPAD(string2,length,pad)
LTRIM(string2)
REPEAT(string2,count)
REPLACE(str,search_str,replace_str)
RPAD(string2,length,pad)
RTRIM(string2)
STRCMP(string1,string2)
SUBSTRING(str,position[,length])
TRIM([[BOTHLEADINGTRAILING][padding]FROM]string2)
UCASE(string2)
RIGHT(string2,length)
SPACE(count)
ABS(number2)
BIN(decimal_number)
CEILING(number2)
CONV(number2,from_base,to_base)
FLOOR(number2)
FORMAT(number,decimal_places)
HEX(DecimalNumber)
LEAST(number,number2[,…])
MOD(numerator,denominator)
POWER(number,power)
RAND([seed])
ROUND(number[,decimals])
SIGN(number2)
SQRT(number2)
ADDTIME(date2,time_interval)
CONVERT_TZ(datetime2,fromTZ,toTZ)
CURRENT_DATE()
CURRENT_TIME()
CURRENT_TIMESTAMP()
DATE(datetime)
DATE_ADD(date2,INTERVALd_valued_type)
DATE_FORMAT(datetime,FormatCodes)
DATE_SUB(date2,INTERVALd_valued_type)
DATEDIFF(date1,date2)
DAY(date)
DAYNAME(date)
DAYOFWEEK(date)
DAYOFYEAR(date)
EXTRACT(interval_nameFROMdate)
MAKEDATE(year,day)
MAKETIME(hour,minute,second)
MONTHNAME(date)
NOW()
SEC_TO_TIME(seconds)
STR_TO_DATE(string,format)
TIMEDIFF(datetime1,datetime2)
TIME_TO_SEC(time)
WEEK(date_time[,start_of_week])
YEAR(datetime)
DAYOFMONTH(datetime)
HOUR(datetime)
LAST_DAY(date)
MICROSECOND(datetime)
MONTH(datetime)
MINUTE(datetime)
FIRST()
LAST()
MIN()
MAX()
AVG()
SUM()
COUNT()

6 事务状态

6.1 全局事务状态表

以db模式举例,global_table是seata的全局事务表。你可以通过观察global_table表中status字段知悉全局事务处于哪个状态。

状态代码备注
全局事务开始(Begin)1此状态可以接受新的分支事务注册
全局事务提交中(Committing)2这个状态会随时改变
全局事务提交重试(CommitRetry)3在提交异常被解决后尝试重试提交
全局事务回滚中(Rollbacking)4正在重新回滚全局事务
全局事务回滚重试中(RollbackRetrying)5在全局回滚异常被解决后尝试事务重试回滚中
全局事务超时回滚中(TimeoutRollbacking)6全局事务超时回滚中
全局事务超时回滚重试中(TimeoutRollbackRetrying)7全局事务超时回滚重试中
异步提交中(AsyncCommitting)8异步提交中
二阶段已提交(Committed)9二阶段已提交,此状态后全局事务状态不会再改变
二阶段提交失败(CommitFailed)10二阶段提交失败
二阶段决议全局回滚(Rollbacked)11二阶段决议全局回滚
二阶段全局回滚失败(RollbackFailed)12二阶段全局回滚失败
二阶段超时回滚(TimeoutRollbacked)13二阶段超时回滚
二阶段超时回滚失败(TimeoutRollbackFailed)14二阶段超时回滚失败
全局事务结束(Finished)15全局事务结束
二阶段提交超时(CommitRetryTimeout)16二阶段提交因超过重试时间限制导致失败
二阶段回滚超时(RollbackRetryTimeout)17二阶段回滚因超过重试时间限制导致失败
未知状态(UnKnown)0未知状态

6.2 分支事务状态表

状态代码备注
分支事务注册(Registered)1向TC注册分支事务
分支事务一阶段完成(PhaseOne_Done)2分支事务一阶段业务逻辑完成
分支事务一阶段失败(PhaseOne_Failed)3分支事务一阶段业务逻辑失败
分支事务一阶段超时(PhaseOne_Timeout)4分支事务一阶段处理超时
分支事务二阶段已提交(PhaseTwo_Committed)5分支事务二阶段提交
分支事务二阶段提交失败重试(PhaseTwo_CommitFailed_Retryable)6分支事务二阶段提交失败重试
分支事务二阶段提交失败不重试(PhaseTwo_CommitFailed_Unretryable)7分支事务二阶段提交失败不重试
分支事务二阶段已回滚(PhaseTwo_Rollbacked)8分支事务二阶段已回滚
分支事务二阶段回滚失败重试(PhaseTwo_RollbackFailed_Retryable)9分支事务二阶段回滚失败重试
分支事务二阶段回滚失败不重试(PhaseTwo_RollbackFailed_Unretryable)10二阶段提交失败
未知状态(UnKnown)0未知状态

6.3 全局事务超时回滚中(TimeoutRollbacking)

怎么发生的?

  1. 当某个seata全局事务执行过程中,无法完成业务。
  2. TC中的一个定时任务(专门用来寻找已超时的全局事务),发现该全局事务未回滚完成,就会将此全局事务改为全局事务超时回滚中(TimeoutRollbacking),开始回滚,直到回滚完毕后删除global_table数据。

建议:当你发现全局事务处于该状态,请排查为何业务无法在限定时间内完成事务。若确实无法完成,应调大全局事务超时时间。(如排查一切正常,请检查tc集群时区与数据库是否一致,若不一致请改为一致)。

7 事务隔离

参考地址https://seata.io/zh-cn/docs/user/appendix/isolation.html

7.1 Seata事务隔离

本文目标:帮助用户明白使用Seata AT模式时,该如何正确实现事务隔离,防止脏读脏写。

希望读者在阅读本文前,已阅读过seata官网中对AT模式的介绍,并且对数据库本地锁有所了解

(例如,两个事务同时在对同一条记录做update时,只有拿到record lock的事务才能更新成功,另一个事务在record lock未释放前只能等待,直到事务超时)

首先请看这样的一段代码,尽管看着“初级”,但持久层框架实际上帮我们做的主要事情也就这样:

@Service
public class StorageService {

    @Autowired
    private DataSource dataSource;

    @GlobalTransactional
    public void batchUpdate() throws SQLException {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        try {
            connection = dataSource.getConnection();
            connection.setAutoCommit(false);
            String sql = "update storage_tbl set count = ?" +
                "    where id = ? and commodity_code = ?";
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setInt(1, 100);
            preparedStatement.setLong(2, 1);
            preparedStatement.setString(3, "2001");
            preparedStatement.executeUpdate();
            connection.commit();
        } catch (Exception e) {
            throw e;
        } finally {
            IOutils.close(preparedStatement);
            IOutils.close(connection);
        }
    }

}

7.2 从代理数据源说起

使用AT模式,最重要的事情便是代理数据源,那么用DataSourceProxy代理数据源有什么作用呢?

DataSourceProxy能帮助我们获得几个重要的代理对象

  • 通过DataSourceProxy.getConnection()获得ConnectionProxy
  • 通过ConnectionProxy.prepareStatement(...)获得StatementProxy

Seata的如何实现事务隔离,就藏在这2个Proxy中,我先概述下实现逻辑。

7.2.1 StatementProxy.executeXXX()的处理逻辑

  • 当调用io.seata.rm.datasource.StatementProxy.executeXXX()会将sql交给io.seata.rm.datasource.exec.ExecuteTemplate.execute(...)处理。
    • ExecuteTemplate.execute(...)方法中,Seata根据不同dbType和sql语句类型使用不同的Executer,调用io.seata.rm.datasource.exec.Executer类的execute(Object... args)
    • 如果选了DML类型Executer,主要做了以下事情:
      • 查询前镜像(select for update,因此此时获得本地锁)
      • 执行业务sql
      • 查询后镜像
      • 准备undoLog*
    • 如果你的sql是select for update则会使用SelectForUpdateExecutor(Seata代理了select for update),代理后处理的逻辑是这样的:
      • 先执行 select for update(获取数据库本地锁)
      • 如果处于@GlobalTransactional or @GlobalLock检查是否有全局锁
      • 如果有全局锁,则未开启本地事务下会rollback本地事务,再重新争抢本地锁和全局锁,以此类推,除非拿到全局锁

7.2.2 ConnectionProxy.commit()的处理逻辑

  • 处于全局事务中(即,数据持久化方法带有@GlobalTransactional
    • 注册分支事务,获取全局锁
    • undoLog数据入库
    • 让数据库commit本次事务
  • 处于@GlobalLock中(即,数据持久化方法带有@GlobalLock
    • 向tc查询是否有全局锁存在,如存在,则抛出异常
    • 让数据库commit本次事务
  • 除了以上情况(else分支)
    • 让数据库commit本次事务

7.2.3 @GlobalTransactional的作用

标识一个全局事务

7.2.4 @GlobalLock + select for update的作用

如果像updateA()方法带有@GlobalLock + select for update,Seata在处理时,会先获取数据库本地锁,然后查询该记录是否有全局锁存在,若有,则抛出LockConflictException。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值