前言
目前seata版本是1.4.2
运用在实际场景中发现一些性能问题 特此记录下来(1.5版本会优化)
业务场景
下单完成之后扣减库存,开启本地事务的前提条件下发现使用AT模式的时候经常出现获取全局锁失败
问题分析
阅读源码之后发现,该问题其实由于上一事务一阶段已经提交但是没有进行全局提交,所以全局锁并没有释放,导致下一个分支事务注册的时候获取不到全局锁直接返回了异常(前提条件是在开启本地事务的情况下)。
开启本地事务的情况下并不会进行重试而是直接失败
重点来了,一直以为分支事务注册失败的时候回进行重试,然而通过阅读源码之后发现其实并不是,是直接就返回失败。这里大家可以去看官网的写隔离的例子,其实官网写的有问题在开启本地事务的情况下分支事务注册的时候获取全局锁并不会进行重试等待
源码分析
由于seata 进行代理了数据源 所以当本地事务进行commit的时候 就会触发这个方法
重点看execute方法
@Override
public void commit() throws SQLException {
try {
LOCK_RETRY_POLICY.execute(() -> {
doCommit();
return null;
});
} catch (SQLException e) {
if (targetConnection != null && !getAutoCommit() && !getContext().isAutoCommitChanged()) {
rollback();
}
throw e;
} catch (Exception e) {
throw new SQLException(e);
}
}
这里可以看到有个参数retryPolicyBranchRollbackOnConflict
默认是true所以并没有进行重试,所以导致争抢资源的情况下失败率非常高,尤其是本身业务链比较长
public static class LockRetryPolicy {
protected static final boolean LOCK_RETRY_POLICY_BRANCH_ROLLBACK_ON_CONFLICT = ConfigurationFactory
.getInstance().getBoolean(ConfigurationKeys.CLIENT_LOCK_RETRY_POLICY_BRANCH_ROLLBACK_ON_CONFLICT, DEFAULT_CLIENT_LOCK_RETRY_POLICY_BRANCH_ROLLBACK_ON_CONFLICT);
// LOCK_RETRY_POLICY_BRANCH_ROLLBACK_ON_CONFLICT 默认是true
public <T> T execute(Callable<T> callable) throws Exception {
if (LOCK_RETRY_POLICY_BRANCH_ROLLBACK_ON_CONFLICT) {
return callable.call();
} else {
//重试
return doRetryOnLockConflict(callable);
}
}
//重试
protected <T> T doRetryOnLockConflict(Callable<T> callable) throws Exception {
LockRetryController lockRetryController = new LockRetryController();
while (true) {
try {
return callable.call();
} catch (LockConflictException lockConflict) {
onException(lockConflict);
lockRetryController.sleep(lockConflict);
} catch (Exception e) {
onException(e);
throw e;
}
}
}
/**
* Callback on exception in doLockRetryOnConflict.
*
* @param e invocation exception
* @throws Exception error
*/
protected void onException(Exception e) throws Exception {
}
}
没有开启本地事务的情况下
可以看到这里是进行重试机制的,所以失败率比较低
protected T executeAutoCommitTrue(Object[] args) throws Throwable {
ConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
try {
connectionProxy.changeAutoCommit();
return new LockRetryPolicy(connectionProxy).execute(() -> {
T result = executeAutoCommitFalse(args);
connectionProxy.commit();
return result;
});
} catch (Exception e) {
// when exception occur in finally,this exception will lost, so just print it here
LOGGER.error("execute executeAutoCommitTrue error:{}", e.getMessage(), e);
if (!LockRetryPolicy.isLockRetryPolicyBranchRollbackOnConflict()) {
connectionProxy.getTargetConnection().rollback();
}
throw e;
} finally {
connectionProxy.getContext().reset();
connectionProxy.setAutoCommit(true);
}
}
public <T> T execute(Callable<T> callable) throws Exception {
if (LOCK_RETRY_POLICY_BRANCH_ROLLBACK_ON_CONFLICT) {
return doRetryOnLockConflict(callable);
} else {
return callable.call();
}
}
解决方案
针对需要进行争抢的资源进行select for update,之后再进行update 数据
seata AT的全局锁以及隔离
全局锁 其实是各个分支事务需要进行修改数据的值的一个数据集合,分支事务进行注册的时候会把这个数据集合发送给TC,由TC来完成对这些资源进行上锁,当二阶段提交成功之后会对这些进行释放,所以通过了本地锁以及全局锁 就完成了AT模式的隔离。
全局锁的校验有两个地方 第一个是分支事务注册的时候,另一个就是select for update。