一次存储过程调用是一个事务吗?

工作中经常接触一些 Oracle 的存储过程,里面各种复杂的业务逻辑,在调用存储过程之前、之后的 Java 代码里还有各种业务逻辑操作。
存储过程里还可能有显式的 commit/rollback 等事务控制语句,容易让人对系统的事务管理产生困惑。

对于事务管理,可以从 Oracle 数据库和 JDBC 应用端两个角度来说。

对于 Oracle 数据库,只有碰到 commit 语句时才会提交事务,然后开启新事务;碰到 rollback,回滚当前事务的操作,再开启新事务。

应用端角度通过 JDBC 与数据库进行交互,JDBC 有两种事务管理模式:自动提交(默认)、手动管理。

对于自动提交模式,JDBC 向数据库发出一个 SQL 命令后,如果 SQL 执行成功,则发出 commit 提交事务,否则发出 rollback 回滚事务。

回到标题,一次存储过程调用是一个事务吗?如果存储过程里没有显式的 commit/rollback 语句,且 JDBC 是自动提交的,那么一次存储过程调用就是一个事务。

Spring 事务原理与集成 MyBatis 事务管理

1. 事务管理器抽象

一个事务管理器只需要三个基本的能力:获取一个事务、提交事务、回滚事务。

public interface PlatformTransactionManager extends TransactionManager {
    // 获取一个事务
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
            throws TransactionException;

    // 提交事务
    void commit(TransactionStatus status) throws TransactionException;

    // 回滚事务
    void rollback(TransactionStatus status) throws TransactionException;
}

DataSourceTransactionManagerAutoConfiguration 配置类导入了数据库事务管理器 DataSourceTransactionManager

2. 事务同步回调钩子

事务同步回调钩子让我们有机会在事务的各个阶段加入一些协调的动作。

public interface TransactionSynchronization extends Flushable {
    /** Completion status in case of proper commit. */
    int STATUS_COMMITTED = 0;

    /** Completion status in case of proper rollback. */
    int STATUS_ROLLED_BACK = 1;

    /** Completion status in case of heuristic mixed completion or system errors. */
    int STATUS_UNKNOWN = 2;

    default void suspend() {}
    default void resume() {}
    default void flush() {}
    default void beforeCommit(boolean readOnly) {}
    default void beforeCompletion() {}
    default void afterCommit() {}
    default void afterCompletion(int status) {}
}

继续阅读

MySQL 事务隔离及实现

1. 事务的 ACID 特性

  • Actomic,原子性:一个事务中的所有操作,要么全部完成,要么全部失败。

  • Consistency:一致性:事务执行前和执行后数据库都处于一致的状态。

  • Isolation:隔离性:同时进行中的事务不会看到其他事务的中间状态。

  • Durability,持久性:事务提交成功后,对数据库所做的变更都是持久的。

2. SQL 标准的事务隔离级别

  • 读未提交, read uncommitted:一个事务还没提交,它做的变更就能被别的事务看到。问题:会产生”脏读”,读到的数据可能是不一致的。

  • 读已提交, read committed:一个事务提交后,它做的变更才会被别的事务看到。问题:不可重复读,在一个事务里,前后两次执行同一个 SQL 看到的数据是不一致的,产生的原因有:1. 数据被修改或删除(称为”不可重复读”),2. 有新插入的数据(称为”幻读”)。

  • 可重复读, repeatable read:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。解决了”不可重复读”的问题,但没有解决”幻读”问题。

  • 串行化, serializable:对于同一行记录,“写”会加写锁,“读”会加读锁。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。解决了”幻读”的问题,但效率低。

继续阅读

MyBatis 事务管理

1. 运行环境 Enviroment

当 MyBatis 与不同的应用结合时,需要不同的事务管理机制。与 Spring 结合时,由 Spring 来管理事务;单独使用时需要自行管理事务,在容器里运行时可能由容器进行管理。

MyBatis 用 Enviroment 来表示运行环境,其封装了三个属性:

public class Configuration {
    // 一个 MyBatis 的配置只对应一个环境
    protected Environment environment;
    // 其他属性 .....
}

public final class Environment {
    private final String id;
    private final TransactionFactory transactionFactory;
    private final DataSource dataSource;
}

2. 事务抽象

MyBatis 把事务管理抽象出 Transaction 接口,由 TransactionFactory 接口的实现类负责创建。

public interface Transaction {
    Connection getConnection() throws SQLException;
    void commit() throws SQLException;
    void rollback() throws SQLException;
    void close() throws SQLException;
    Integer getTimeout() throws SQLException;
}

public interface TransactionFactory {
    void setProperties(Properties props);
    Transaction newTransaction(Connection conn);
    Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);
}

Executor 的实现持有一个 SqlSession 实现,事务控制是委托给 SqlSession 的方法来实现的。

public abstract class BaseExecutor implements Executor {
    protected Transaction transaction;

    public void commit(boolean required) throws SQLException {
        if (closed) {
            throw new ExecutorException("Cannot commit, transaction is already closed");
        }
        clearLocalCache();
        flushStatements();
        if (required) {
            transaction.commit();
        }
    }

    public void rollback(boolean required) throws SQLException {
        if (!closed) {
            try {
                clearLocalCache();
                flushStatements(true);
            } finally {
                if (required) {
                    transaction.rollback();
                }
            }
        }
    }

    // 省略其他方法、属性
}

继续阅读

Spring 事务钩子

1. 应用场景说明

有多个事务发起的点 E0、E1,都调用到需要在事务里执行的方法 M0、M1,M0、M1 里都可能产生一些逻辑:一些作为事务的一部分执行、一些在事务提交失败时执行、一些在事务成功提交后执行。

2. 自行实现的一个简陋实现

应用命令模式把要根据事务是否提交成功来决定执行的逻辑封装成一个命令,在事务之外来执行。

定义了下面这样一个类,分别用于收集在事务提交成功和失败后执行的逻辑:

public class Actions {
    private List<Runnable> transactionCommitSuccessActions = new ArrayList<Runnable>();
    private List<Runnable> transactionCommitFailedActions = new ArrayList<Runnable>();
}

在开启事务前初始化一个 Actions 对象,然后事务方法调用到的方法都得传递这个对象,在事务方法结束后就可以根据事务是否成功来执行不同的队列里的逻辑。

继续阅读

应用事务管理混乱导致的一个坑

Spring 的事务传播属性

org.springframework.transaction.annotation.Propagation 定义了 Spring 的事务传播属性:

  • REQUIRED: 支持当前事务,如果不存在则新建一个。

  • REQUIRES_NEW: 创建新的事务,如果当前存在一个则 suppend 当前的。

  • SUPPORTS: 支持当前事务,如果不存在则以非事务方式执行。

  • MANDATORY: 支持当前事务,如果不存在则抛出异常。

  • NOT_SUPPORTED: 以非事务方式执行,如果当前存在一个事务则 suppend 当前的。

  • NEVER: 以非事务方式执行,如果存在事务则抛出异常。

  • NESTED: 如果当前存在一个事务则以嵌套事务的方式执行。

一个生产问题

一开始是 DBA 反馈数据库出现两种现象:

  1. 出现一些操作做完但会话一直还在等待客户端的提交动作。
  2. 偶尔出现大量的行锁,导致 JVM 线程互相等待而假死。

有个获取流水号的方法 systemService.getSerialNo 的事务传播属性是 NOT_SUPPORTED 的,这个方法通过类似这样的 update t_serialno set serial_no = :newSerialNo where serial_key = :key and serial_no = :oldSerialNo SQL 语句进行更新,更新返回的受影响行数等于 1 认为新的流水号是不重复的,更新不成功则重试。

行锁就出现在这些流水号的更新上。

继续阅读

Spring 事务管理的一个 trick

问题

最近有同事碰到这个异常信息: Transaction rolled back because it has been marked as rollback-only ,异常栈被吃了,没打印出来。

调用代码大概如下:

@Component
public class InnerService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional(rollbackFor = Throwable.class)
    public void innerTx(boolean ex) {
        jdbcTemplate.execute("insert into t_user(uname, age) values('liuwhb', 31)");
        if (ex) {
            throw new NullPointerException();
        }
    }

}

@Component
public class OutterService {
    @Autowired
    private InnerService innerService;

    @Transactional(rollbackFor = Throwable.class)
    public void outTx(boolean ex) {
        try {
            innerService.innerTx(ex);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

outterService.outTx(true);

他期望的是 innerService.innerTx(ex); 调用即使失败了也不会影响 OutterService.outTx 方法上的事务,只回滚了 innerTx 的操作。

结果没有得到他想要的,调用 OutterService.outTx 的外围方法捕获到了异常,异常信息是 Transaction rolled back because it has been marked as rollback-onlyoutTx 的其他操作也没有提交事务。

分析

上述方法的事务传播机制的默认的,也就是 Propagation.REQUIRED,如果当前已有事务就加入当前事务,没有就新建一个事务。

事务性的方法 outTx 调用了另一个事务性的方法 innerTx 。调用方对被调用的事务方法进行异常捕获,目的是希望被调用方的异常不会影响调用方的事务。

但还是会影响调用方的行为的。Spring 捕获到被调用事务方法的异常后,会把事务标记为 read-only,然后调用方提交事务的时候发现事务是只读的,就会抛出上面的异常。

继续阅读

Spring AOP 与 事务实现

1. AOP 与方法调用

对于 接口 interface,可以通过 java.lang.reflect.Proxy 来生成动态代理对象;
对于 类 class,可以通过字节码技术,如 CBLIB 来生成一个子类作为代理对象,此时类不能声明为 final 。

当 Spring 把 Bean 注入到另一个 Bean 时,其实注入的是它生成的代理对象,进行方法调用时,首先调用的是代理对象上的方法,代理对象的方法最终再调目标对象的方法,也就是开发人员编写的方法。Spring 通过在调用目标方法前后做处理来实现一些特性,例如事务管理、缓存等。

Spring 的 AOP 是基于对代理对象的方法调用的拦截的,只能拦截外部对象 对 某个对象的方法的调用,对象的方法调用同一个类的方法是不会被拦截的。

@Service
public class SpringTxService {
    private static final Logger LOGGER = LoggerFactory
            .getLogger(SpringTxService.class);

    @Transactional(propagation = Propagation.REQUIRED)
    public void add() {
        LOGGER.info("do add");
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void requireNew() {
        LOGGER.info("do requireNew");
    }

    public void composite() {
        add();
        requireNew();
    }
}

上面的代码是基于注解进行事务控制的,composite 方法没有声明事务属性,它会调用 add、 requireNew
方法。当把 SpringTxService 注入到另一个 BeanB:
1. 在 BeanB 的方法里调用代理对象 composite 时,最终执行 add、 requireNew 是没有在事务里执行的,只是普通的方法调用。
2. 在 BeanB 的方法里调用代理对象的 add、 requireNew 方法时,这两个方法都会分别在相应的事务里执行。

当发现基于 AOP 实现的特性没有预期的效果时,一定要看看是不是在代理对象上调用还是目标类内部的方法调用。

继续阅读

Java 任务处理

最近梳理其他同事以前写的 job 后有点想法,记录下。

一、业务场景

在大多数的系统都有类似这样的逻辑,比如下单了给用户赠送积分,用户在论坛上发表了帖子,给用户增加积分等等。

下单赠送积分,那么一个订单肯定不能重复赠送积分,所以需要一些状态来比较来哪些是已赠送的,哪些是没有赠送的。或许可以在订单表里加个字段来标记是否赠送了积分。

有时候,业务人员出于营销的需要,可能要搞个某某时间段内下单返券的活动。难道又在订单表里加个字段?肯定不能,谁知道还要搞多少活动呢。

二、实现

为了使核心的业务流程尽可能简单高效,赠送积分、返券(后面简称为task)之类的逻辑应该通过异步的job来处理。

因为 task 的处理状态不能放在核心的业务表里,所以,可以另外建一个表示异步任务的 async_task 表,结构如下:

-- 业务job处理 任务
create table async_task (
  id number(11) primary key,
  key_work  varchar2(32),  --  不同业务逻辑的task用不同的keyword
  biz_id char(32),         --  业务数据 ID,比如订单号
  biz_data varchar2(256),  --  核心的业务数据,用于避免关联业务表;具体结构取决于keyword
  status number,           --  任务的处理状态; -2:未处理, -1:处理中, 0:已处理, 大于 0 的数字表示失败次数
  create_tm date,          --  任务的创建时间
  modify_tm date           --  任务的修改时间
);

处于性能考虑,可以在 key_work 字段上建立分区,在 biz_id 上建立索引。

当业务表有需要处理的数据时,就往 async_task 插入一条相应的记录(可以异步插入),异步 job 再从 async_task 表里取数据来处理。

注意:处理 task 时,要保证数据的一致性。所在的项目组曾出现过,下单返券的活动里,送券与更新状态的操作没有放在同一个事务里,出现券送了,状态没更新,重复送券的问题。一定要注意事务的正确处理。

继续阅读