2023年3月

微软发布 C# async/await 异步语法功能已经好久了,但是目前来看使用并不广泛。本人经过实践在开发过程中使用 async/await 一路到底确实很爽,而且也没有啥问题。但是在面对旧项目变更要使用些功能的时候可能会遇到同步方法调用异步方法的情况,本人在这种情况就发生调用没有响应的问题,并作些总结。

发生些问题的具体条件

  1. asp.net 应用程序(控制台无法重现,winform、wpf 没有尝试过)
  2. 同步方法里面调用异步方法直接通过Result属性获取方法返回值
  3. 在被调用的异步方法里面肯定会发生新的线程执行异步任务

上案例(asp.net webapi)

public classTodoController : ApiController
{
protected async Task<string>GetString()
{
return await new HttpClient().GetStringAsync("https://www.cnblogs.com");
}
public stringGetTodo()
{
returnGetString().Result;
}
}

上述代码中当GetTodo被调用时就会出现长时间的等待。

解决方案 A

public classTodoController : ApiController
{
protected async Task<string>GetString()
{
return await new HttpClient().GetStringAsync("https://www.cnblogs.com");
}
public stringGetTodo()
{
return Task.Factory.StartNew(() =>GetString()).Unwrap().GetAwaiter().GetResult();
}
}

解决方案 B

添加包Nito.AsyncEx

using Nito.AsyncEx;

public
classTodoController : ApiController
{
protected async Task<string>GetString()
{
return await new HttpClient().GetStringAsync("https://www.cnblogs.com");
}
public stringGetTodo()
{
return AsyncContext.Run(() =>GetString());
}
}

作者:京东科技 刘清洁

1、痛点(*)

自动化测试有2种形式,接口自动化和UI自动化。而UI自动化经常会被登录节点堵塞,例如验证码、图形、滑块等,尽管有些方式可以识别图形和定位滑块位置,但成功率都不高,无法真正意义上实现自动化执行;而http接口的自动化测试前置如果依赖cookie,也无法实现自动化执行。

a、怎么样才能绕过登录,实现从前端到后端的自动化执行

b、面对复杂的登录验证无法直接自动获取到cookie,需要人工操作登录,而cookie又有时效,不能长久使用

本方案将有效解决以上问题,在面对复杂的登录验证及有cookie时效的模式下,可以将短暂时效的cookie改为长久有效,真正意义上实现UI自动化和依赖cookie鉴权的接口自动化。

2、什么是cookie

cookie称之为会话跟踪技术,是一个很小的文本文件,是浏览器储存在用户的机器上的。Cookie是纯文本,没有可执行代码。储存一些服务器需要的信息,每次请求站点,会发送相应的cookie,这些cookie可以用来辨别用户身份信息等作用

3、过期时间查看方式

打开浏览器,并转到您希望查看 cookie 的网站。

按 F12 键打开浏览器的开发者工具。

在开发者工具的“调试工具”选项卡中,单击“存储”按钮。

在左侧的“网站数据”列表中,单击“Cookies”。

在右侧的“值”列表中,查看每个 cookie 的“Expires”或“Max-Age”字段。这些字段显示 cookie 的过期时间。

4、cookie机制

客户端发送一个请求到服务器 --》 服务器发送一个HttpResponse响应到客户端,其中包含Set-Cookie的头部 --》 客户端保存cookie,之后向服务器发送请求时,HttpRequest请求中会包含一个Cookie的头部 --》服务器返回响应数据

时效限制:每个cookie都有时效,默认的有效期是,会话级别:就是当浏览器关闭,那么cookie立即销毁,但是我们也可以在存储的时候手动设置cookie的过期时间

5、cookie时效无限延长方案(*)

5-1、前提

a. 登录节点有验证机制,例如短信验证码、图形识别、滑块等校验;

b. cookie有时效,超过时效则需要重新登录;

c. 同一个账号不会在多个平台退出或登录

5-2、实现原理

此方案是通过一个微服务提供接口,供自动化调用,通过传递账号,返回永久cookie,将此步嵌入到自动化流程中,替代登录并获取cookie的节点,并将cookie的时效永久延长,并不会时效,以保证后续自动化流程永久循环正常执行。

5-3、核心流程步骤

步骤1:先手工登录,从header中获取cookie,将此cookie和时效值保存在微服务平台(一个账号只需一次手工登录,后续永久不需要操作登录)。

步骤2:微服务平台将此账号、cookie、时效值、关联的业务接口进行持久化存储,并跟进时效值计算出轮询时长,并触发轮询任务执行,任务中将携带此cookie去调用业务接口,保持长会话,并hold进程等待,在轮询时长到达时,继续执行任务执行,再次hold进程等待,持续循环,以保证次cookie的会话永久保持住。

步骤3:自动化任务执行前会调用微服务接口,通过账号获取到永久cookie,携带此cookie执行后续自动化任务。

6、落地案例

目前通过下方方案,已实现了cookie一次配置,长久使用的目的。

实践效果对比

之前:ui自动化和http接口自动化执行时经常出现cookie过期,需要手工重新登录,并在自动化平台上更新cookie,比较繁琐,且影响凌晨自动执行成功率

现在:使用上面方案后,只需手工在cookie微服务平台上配置一次cookie,以后不再需要更新cookie

7、专利描述

https://zhuanli.tianyancha.com/811840799431036187d34680d5b10ae3

本文链接
https://www.cnblogs.com/zichliang/p/17265960.html

Javascript 和 我之前发的
python加密
以及
go加密
解密不一样 不需要导那么多的库
只需要安装几个库 其中需要了解最多的
crypto-js

具体就不多介绍了直接上官网
https://www.npmjs.com/package/crypto-js

安装

npm install crypto-js --save-dev
npm install md5 --save-dev

一些常见的built-in 函数加密

unescape

unescape() 函数可对通过 escape() 编码的字符串进行解码。

let e = escape("始識")
console.log(e) // %u59CB%u8B58
let u = unescape(e)
console.log(u) // 始識

URL编码与解码

let e = encodeURI("https://始識的博客")
console.log(e) // https://%E5%A7%8B%E8%AD%98%E7%9A%84%E5%8D%9A%E5%AE%A2
let u = decodeURI(e)
console.log(u) // https://始識的博客

fromCharCode

将 Unicode 编码转为一个字符

var n = String.fromCharCode(65);
// A


[101,118,97,108].map(item=>{
    return String.fromCharCode(item)
})
 ['e', 'v', 'a', 'l']

Base64

btoa atob

let e = btoa("https://www.cnblogs.com/zichliang/p/17265960.html")
console.log(e) // // https://%E5%A7%8B%E8%AD%98%E7%9A%84%E5%8D%9A%E5%AE%A2
let u = atob(e)
console.log(u) // https://www.cnblogs.com/zichliang/p/17265960.html

引用 crypto-js 加密模块

var CryptoJS = require('crypto-js')

function base64Encode() {
  var srcs = CryptoJS.enc.Utf8.parse(text);
  var encodeData = CryptoJS.enc.Base64.stringify(srcs);
  return encodeData
}

function base64Decode() {
  var srcs = CryptoJS.enc.Base64.parse(encodeData);
  var decodeData = srcs.toString(CryptoJS.enc.Utf8);
  return decodeData
}

var text = "https://www.cnblogs.com/zichliang/p/17265960.html"

var encodeData = base64Encode()
var decodeData = base64Decode()

console.log("Base64 编码: ", encodeData)
console.log("Base64 解码: ", decodeData)

// Base64 编码:  aHR0cHM6Ly93d3cuY25ibG9ncy5jb20vemljaGxpYW5nL3AvMTcyNjU5NjAuaHRtbA==
// Base64 解码:  https://www.cnblogs.com/zichliang/p/17265960.html

MD5

// 引用 crypto-js 加密模块
var CryptoJS = require('crypto-js')

function MD5Test() {
    var text = "https://www.cnblogs.com/zichliang"
    return CryptoJS.MD5(text).toString()
}

console.log(MD5Test())  // 50177badb579733de56b628ae57fb972

PBKDF2

// 引用 crypto-js 加密模块
var CryptoJS = require('crypto-js')

function pbkdf2Encrypt() {
    var text = "https://www.cnblogs.com/zichliang"
    var salt = "1234567"
    // key 长度 128,10 次重复运算
    var encryptedData = CryptoJS.PBKDF2(text, salt, {keySize: 128/32,iterations: 10});
    return encryptedData.toString()
}

console.log(pbkdf2Encrypt())  // bcda4be78de797d8f5067331b1a70d40

SHA1

// 引用 crypto-js 加密模块
var CryptoJS = require('crypto-js')

function SHA1Encrypt() {
    var text = "https://www.cnblogs.com/zichliang"
    return CryptoJS.SHA1(text).toString();
}

console.log(SHA1Encrypt())  // ca481c13d5af7135b69d11ffb0a443a635fbc307

HMAC

// 引用 crypto-js 加密模块
var CryptoJS = require('crypto-js')

function HMACEncrypt() {
  var text = "https://www.cnblogs.com/zichliang"
  var key = "secret"
  return CryptoJS.HmacMD5(text, key).toString();
}

console.log(HMACEncrypt())// 20ca7a63f1f4a7047ffd6b722b45319a

DES

// 引用 crypto-js 加密模块
var CryptoJS = require('crypto-js')

function desEncrypt() {
  var key = CryptoJS.enc.Utf8.parse(desKey),
      iv = CryptoJS.enc.Utf8.parse(desIv),
      srcs = CryptoJS.enc.Utf8.parse(text),
      // CBC 加密模式,Pkcs7 填充方式
      encrypted = CryptoJS.DES.encrypt(srcs, key, {
        iv: iv,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
      });
  return encrypted.toString();
}

function desDecrypt() {
  var key = CryptoJS.enc.Utf8.parse(desKey),
      iv = CryptoJS.enc.Utf8.parse(desIv),
      srcs = encryptedData,
      // CBC 加密模式,Pkcs7 填充方式
      decrypted = CryptoJS.DES.decrypt(srcs, key, {
        iv: iv,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
      });
  return decrypted.toString(CryptoJS.enc.Utf8);
}

var text = "https://www.cnblogs.com/zichliang"       // 待加密对象
var desKey = "0123456789ABCDEF"    // 密钥
var desIv = "0123456789ABCDEF"    // 初始向量

var encryptedData = desEncrypt()
var decryptedData = desDecrypt()

console.log("加密字符串: ", encryptedData)
console.log("解密字符串: ", decryptedData)

// 加密字符串:  p+4ovmk1n5YwN3dq5y8VqhngLKW//5MM/qDgtj2SOC6TpJaFgSKEVg==
// 解密字符串:   https://www.cnblogs.com/zichliang

3DES

// 引用 crypto-js 加密模块
var CryptoJS = require('crypto-js')

function tripleDesEncrypt() {
  var key = CryptoJS.enc.Utf8.parse(desKey),
      iv = CryptoJS.enc.Utf8.parse(desIv),
      srcs = CryptoJS.enc.Utf8.parse(text),
      // ECB 加密方式,Iso10126 填充方式
      encrypted = CryptoJS.TripleDES.encrypt(srcs, key, {
        iv: iv,
        mode: CryptoJS.mode.ECB,
        padding: CryptoJS.pad.Iso10126
      });
  return encrypted.toString();
}

function tripleDesDecrypt() {
  var key = CryptoJS.enc.Utf8.parse(desKey),
      iv = CryptoJS.enc.Utf8.parse(desIv),
      srcs = encryptedData,
      // ECB 加密方式,Iso10126 填充方式
      decrypted = CryptoJS.TripleDES.decrypt(srcs, key, {
        iv: iv,
        mode: CryptoJS.mode.ECB,
        padding: CryptoJS.pad.Iso10126
      });
  return decrypted.toString(CryptoJS.enc.Utf8);
}

var text = "https://www.cnblogs.com/zichliang"       // 待加密对象
var desKey = "0123456789ABCDEF"    // 密钥
var desIv = "0123456789ABCDEF"    // 偏移量

var encryptedData = tripleDesEncrypt()
var decryptedData = tripleDesDecrypt()

console.log("加密字符串: ", encryptedData)
console.log("解密字符串: ", decryptedData)

// 加密字符串:   pl/nNfpIrejwK+/X87VmGZIbS3kOB+IpFcx/97wpR4AO6q9HGjxb4w==
// 解密字符串:   https://www.cnblogs.com/zichliang

AES

// 引用 crypto-js 加密模块
var CryptoJS = require('crypto-js')

function aesEncrypt() {
    var key = CryptoJS.enc.Utf8.parse(aesKey),
        iv = CryptoJS.enc.Utf8.parse(aesIv),
        srcs = CryptoJS.enc.Utf8.parse(text),
        // CBC 加密方式,Pkcs7 填充方式
        encrypted = CryptoJS.AES.encrypt(srcs, key, {
            iv: iv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        });
    return encrypted.toString();
}

function aesDecrypt() {
    var key = CryptoJS.enc.Utf8.parse(aesKey),
        iv = CryptoJS.enc.Utf8.parse(aesIv),
        srcs = encryptedData,
        // CBC 加密方式,Pkcs7 填充方式
        decrypted = CryptoJS.AES.decrypt(srcs, key, {
            iv: iv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        });
    return decrypted.toString(CryptoJS.enc.Utf8);
}

var text = "https://www.cnblogs.com/zichliang"       // 待加密对象
var aesKey = "0123456789ABCDEF"   // 密钥,16 倍数
var aesIv = "0123456789ABCDEF"    // 偏移量,16 倍数

var encryptedData = aesEncrypt()
var decryptedData = aesDecrypt()

console.log("加密字符串: ", encryptedData)
console.log("解密字符串: ", decryptedData)

// 加密字符串:  /q8i+1GN8yfzIb8CaEJfDOfDQ74in+XzQZYBtKF2wkAB6dM1qbBZ3HJVlY+kHDE3
// 解密字符串:  https://www.cnblogs.com/zichliang

RC4

// 引用 crypto-js 加密模块
var CryptoJS = require('crypto-js')

function RC4Encrypt() {
    return CryptoJS.RC4.encrypt(text, key).toString();
}

function RC4Decrypt(){
    return CryptoJS.RC4.decrypt(encryptedData, key).toString(CryptoJS.enc.Utf8);
}

var text = "https://www.cnblogs.com/zichliang"
var key = "12345678ASDFG"

var encryptedData = RC4Encrypt()
var decryptedData = RC4Decrypt()

console.log("加密字符串: ", encryptedData)
console.log("解密字符串: ", decryptedData)

// 加密字符串:  U2FsdGVkX19/bT2W57mzjwoF5Fc3Zb4WiyDU+MiNMmHfdJvZeScl0EW9yJWCPiRrsA==
// 解密字符串:  https://www.cnblogs.com/zichliang

Rabbit

// 引用 crypto-js 加密模块
var CryptoJS = require('crypto-js')

function rabbitEncrypt() {
    return CryptoJS.Rabbit.encrypt(text, key).toString();
}

function rabbitDecrypt() {
    return CryptoJS.Rabbit.decrypt(encryptedData, key).toString(CryptoJS.enc.Utf8);
}

var text = "https://www.cnblogs.com/zichliang/p/16653303.html"
var key = "1234567ASDFG"

var encryptedData = rabbitEncrypt()
var decryptedData = rabbitDecrypt()

console.log("加密字符串: ", encryptedData)
console.log("解密字符串: ", decryptedData)

// 加密字符串:  U2FsdGVkX1/pYbHvbNff3/RNpso4yRKIX0XDFta8hoLNxe52K8HSmF+XV8ayYqucTKVPP6AJtGczXS7U9kkxHnw=
// 解密字符串:  https://www.cnblogs.com/zichliang/p/16653303.html

RSA

使用 node-rsa

需要安装一个库

npm install node-rsa

// 引用 node-rsa 加密模块
var NodeRSA = require('node-rsa');

function rsaEncrypt() {
    pubKey = new NodeRSA(publicKey,'pkcs8-public');
    var encryptedData = pubKey.encrypt(text, 'base64');
    return encryptedData
}

function rsaDecrypt() {
    priKey = new NodeRSA(privatekey,'pkcs8-private');
    var decryptedData = priKey.decrypt(encryptedData, 'utf8');
    return decryptedData
}

var key = new NodeRSA({b: 512});                    //生成512位秘钥
var publicKey = key.exportKey('pkcs8-public');    //导出公钥
var privatekey = key.exportKey('pkcs8-private');  //导出私钥
var text = "https://www.cnblogs.com/zichliang/p/16653303.html"

var encryptedData = rsaEncrypt()
var decryptedData = rsaDecrypt()

console.log("公钥:\n", publicKey)
console.log("私钥:\n", privatekey)
console.log("加密字符串: ", encryptedData)
console.log("解密字符串: ", decryptedData)

/*
公钥:
 -----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAN7JoMDNvvpB/po2OMSeSKsromfP5EyI
0fAz6XDVwqdTUBwwAArLlqIzmVNK0yi4nlbj5eF+O8ZjRkRQ6xKP/CMCAwEAAQ==
-----END PUBLIC KEY-----
私钥:
 -----BEGIN PRIVATE KEY-----
MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEA3smgwM2++kH+mjY4
xJ5IqyuiZ8/kTIjR8DPpcNXCp1NQHDAACsuWojOZU0rTKLieVuPl4X47xmNGRFDr
Eo/8IwIDAQABAkEArI0Ps6TnIJ9SmZAbYbWSZPjTvYHXuatSpq8eQ+Vb8Ql003G5
Y2FIoWpQX1jQ9/DsxEZ/1u+71bl08z1eONz2KQIhAPgLZOKanhDDaOn5sO7Y2RM3
TyLS08mCGNGQxEhkEttFAiEA5e7bvnrSNh1lcF/QTxkWPGoXb9kxPljm49CfiTS9
PEcCIDzxX7olTwzDVjWWeZhVgxArmK/vqMVrx3lF3lQC8ncZAiBlpY5nSoybd6tc
Xj8MeJ6n3o6112I5mbuYgqXEVhhCCQIgY6vinhOzMF0dX9MNjBm8x1mUCd4XG2TN
QQcOik3RIGw=
-----END PRIVATE KEY-----
加密字符串:  ZolvYwjFqOp1Yldui7rm75mSN5kz7533nc3B3H6xZGQR9v0elhbcjmI9vXaBsgdLNTuyoVk3bfzWfQdeIpvCpcBCTGe1HG9KrSBYDiWJc4vBgVBz8D57/XaS1zjM0kuAJ/ELu4os7XG5lMQbRbFhHXs7zQsIBq6/m2IZdGWx7HjB2jiQBQPMfszdQUOwQA
bM5o7lRvUgdMVaZkEWpOTEybmUX4kxBP5CvNtB86oTRUw+U7Ex7QB8lWj33hoKvh70
解密字符串:  https://www.cnblogs.com/zichliang/p/16653303.html
*/

使用自带模块crypto:

const crypto = require('crypto');
const nodeRSA = require('node-rsa');

// 生成一个1024长度的密钥对
const key = new nodeRSA({b: 1024});
// 导出公钥
const publicKey = key.exportKey('public');
// 导出私钥
const privateKey = key.exportKey('private');

const secret = 'https://www.cnblogs.com/zichliang/p/16653303.html'
// 使用私钥加密,公钥解密
const encrypt = crypto.privateEncrypt(privateKey, Buffer.from(secret));
const decrypt = crypto.publicDecrypt(publicKey, encrypt);

console.log('加密后:', encrypt.toString('base64'));
console.log('解密后:', decrypt.toString());

RSA 长加密

这个加密是真的麻烦 ,而且还需要导入jsencrypt.min.js
这里贴上 GitHub地址
https://github.com/wangqinglongDo/github_demo/blob/master/libs/jsencrypt.min.js
对了 还需要补环境 而且解密也不是很好用,如果有大佬知道如何解密的 希望在评论区告诉我

var encrypt = new JSEncrypt();
var publickKey = "-----BEGIN PUBLIC KEY-----\
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDLFb8qp1vRFvi/qfgi1Wg7Mi8l\
LcpfAc+tgpyD7aFW9QquQVMm/jG1IJZVQ6LsdkI7TiDutMCzOMCBXbdSC9BCIAGA\
L2Sz3cYVlGb1kYSM0ZMcUMIK5eF4Bptke070XHvbi8wArtysJ0l71RHDd786tNbG\
W0hDSw3zAqTErbxFaQIDAQAB\
-----END PUBLIC KEY-----\
"
encrypt.setPublicKey(publickKey);  //设置公钥加密证书
var data = "https://www.cnblogs.com/zichliang/p/17265960.html";
var commonEncodeData = encrypt.encryptLong(data);   // 普通的加密

console.log(commonEncodeData)

var cnEscapeData = window.btoa(window.encodeURIComponent(data));  //base64 解密后的加密
var encryptData = encrypt.encryptLong(cnEscapeData);  //获取加密后数据。
console.log(encryptData)

生产环境,一个简单的事务方法,提交失败,报 Global lock wait timeout

伪代码如下:

@GlobalTransactional(rollbackFor = Exception.class,timeoutMills = 30000,lockRetryInternal=3000,lockRetryTimes=10)
@Override
public Boolean cancel(Long id, Long userId, Long companyId) {
    // 保存业务数据
    ...
    // 启动工作流
    wkflAppServiceProvider.startProcess(....);
    ...
}

异常如下:

org.springframework.dao.QueryTimeoutException: JDBC commit; Global lock wait timeout; nested exception is io.seata.rm.datasource.exec.LockWaitTimeoutException: Global lock wait timeout
                                                                                             
Caused by: io.seata.rm.datasource.exec.LockWaitTimeoutException: Global lock wait timeout
        at io.seata.rm.datasource.exec.LockRetryController.sleep(LockRetryController.java:63)
        at io.seata.rm.datasource.ConnectionProxy$LockRetryPolicy.doRetryOnLockConflict(ConnectionProxy.java:346)
        at io.seata.rm.datasource.ConnectionProxy$LockRetryPolicy.execute(ConnectionProxy.java:335)
        at io.seata.rm.datasource.ConnectionProxy.commit(ConnectionProxy.java:187)
        at org.springframework.jdbc.datasource.DataSourceTransactionManager.doCommit(DataSourceTransactionManager.java:333)
        ... 57 more
Caused by: io.seata.rm.datasource.exec.LockConflictException: get global lock fail, xid:10.222.248.60:8091:2900686326154883760, lockKeys:wkfl_app_auth:12326192,12326193;act_ge_bytearray:6515890,6515891;act_re_procdef:rediscountClickSubmitCancel_UserTask_0yze6zf_5:1:6515892;act_re_deployment:6515889
        at io.seata.rm.datasource.ConnectionProxy.recognizeLockKeyConflictException(ConnectionProxy.java:159)
        at io.seata.rm.datasource.ConnectionProxy.processGlobalTransactionCommit(ConnectionProxy.java:252)
        at io.seata.rm.datasource.ConnectionProxy.doCommit(ConnectionProxy.java:230)
        at io.seata.rm.datasource.ConnectionProxy.lambda$commit$0(ConnectionProxy.java:188)
        at io.seata.rm.datasource.ConnectionProxy$LockRetryPolicy.doRetryOnLockConflict(ConnectionProxy.java:343)
        ... 60 more

看到“LockWaitTimeoutException: Global lock wait timeout” 我以为是有资源竞争,导致加锁等待超时。但这个疑虑很快被打消了,因为这是必现的一个问题,每次执行到这个方法都报错,甚至在下班后系统没有人使用的情况下,我一点,还是报这个错,这个时候可以确定就我一个人在用,而且查了数据库没有被锁定的数据和事务,所以应该不是资源竞争导致的获取锁等待超时。

于是,我开始翻源码

数据源被代理,本地事务提交走的是io.seata.rm.datasource.ConnectionProxy#commit()

doCommit()方法是放在io.seata.rm.datasource.ConnectionProxy.LockRetryPolicy#execute()中执行的

由于我们这里client.rm.lock.retryPolicyBranchRollbackOnConflict配置的是false,所以这里失败后会重试,如果是true,则不重试

看到这里,我们找到了“Global lock wait timeout”的出处了,原来是因为doCommit()执行过程中抛异常了,再重试次数用完后就会抛出LockWaitTimeoutException。因此,LockWaitTimeoutException只是表象,并不是最根本的原因,根本原因是doCommit()报错了。

接着doCommit()看,我们知道,分支事务提交要先注册,注册成功后才能提交。而注册就是要获取全局锁。

通过观察DEBUG日志,发现保存业务数据部分的分支注册都是成功的

日志太多,截取关键部分,如图所示

结合代码,发现真正的报错发生在调用远程服务启动工作流那里

查看工作流相关服务的日志,发现一开始分支注册就失败了,部分关键日志如下

工作流那个服务里面,分支注册返回的信息是:Global lock acquire failed xid = ....

幸好之前读过Seata的源码,不然此时肯定手足无措

于是,翻开Seata Server的源码,看看为什么返回的消息是这样的

直接快进到io.seata.server.transaction.at.ATCore#branchSessionLock()

具体参见我的另一篇博文
https://www.cnblogs.com/cjsblog/p/16878067.html

在这里,我们找到了“Global lock acquire failed”这个报错信息的出处

证明,在执行branchSession.lock(autoCommit, skipCheckLock)的时候要么失败返回false,要么抛异常了

根据配置,这里是db,所以是DataBaseLockManager

接下来进入到LockStoreDataBaseDAO#acquireLock()开始真正加锁了(往表里插数据)

io.seata.server.storage.db.lock.LockStoreDataBaseDAO#acquireLock(java.util.List<io.seata.core.store.LockDO>, boolean, boolean)

方法太长,不细看了,重点看加锁的SQL语句

由于用的MySQL,所以是io.seata.core.store.db.sql.lock.MysqlLockStoreSql

最终拼接好的SQL是这样的:

insert into lock_table (xid, transaction_id, branch_id, resource_id, table_name, pk, row_key, gmt_create, gmt_modified) values (?, ?, ?, ?, ?, ?, ?, now(), now(), ?)

如果插入成功,则返回true,表示加锁成功,对应的分支事务获取锁成功,分支事务注册成功,皆大欢喜

补充一下,这里面有很多地方配置项

至此,整个分支事务获取锁的逻辑我们都清楚了

接下来,再回头看看lock_table表的各个列,首先看看怎么从客户端传过来的一个lockKey变成List<LockDO>的

因此,假设客户端发过来的lockKey是这样:

offer message: xid=10.222.248.60:8091:2900686326154883760,branchType=AT,resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow,lockKey=wkfl_app_auth:12326192,12326193;act_ge_bytearray:6515890,6515891;act_re_procdef:rediscountClickSubmitCancel_UserTask_0yze6zf_5:1:6515892;act_re_deployment:6515889

那么这里得到的List<LockDO>就是这样的:

LockDO(xid=10.222.248.60:8091:2900686326154883760, transactionId=153490553438167612, branchId=153490553438162971, resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow, tableName=wkfl_app_auth, pk=12326192, status=0, rowKey=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow^^^wkfl_app_auth^^^12326192)
LockDO(xid=10.222.248.60:8091:2900686326154883760, transactionId=153490553438167612, branchId=153490553438162971, resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow, tableName=wkfl_app_auth, pk=12326193, status=0, rowKey=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow^^^wkfl_app_auth^^^12326193)
LockDO(xid=10.222.248.60:8091:2900686326154883760, transactionId=153490553438167612, branchId=153490553438162971, resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow, tableName=act_ge_bytearray, pk=6515890, status=0, rowKey=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow^^^act_ge_bytearray^^^6515890)
LockDO(xid=10.222.248.60:8091:2900686326154883760, transactionId=153490553438167612, branchId=153490553438162971, resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow, tableName=act_ge_bytearray, pk=6515891, status=0, rowKey=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow^^^act_ge_bytearray^^^6515891)
LockDO(xid=10.222.248.60:8091:2900686326154883760, transactionId=153490553438167612, branchId=153490553438162971, resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow, tableName=act_re_procdef, pk=rediscountClickSubmitCancel_UserTask_0yze6zf_5:1:6515892, status=0, rowKey=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow^^^act_re_procdef^^^rediscountClickSubmitCancel_UserTask_0yze6zf_5:1:6515892)
LockDO(xid=10.222.248.60:8091:2900686326154883760, transactionId=153490553438167612, branchId=153490553438162971, resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow, tableName=act_re_deployment, pk=6515889, status=0, rowKey=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow^^^act_re_deployment^^^6515889)

往lock_table表里就会插入这6条数据,最后查看Seata服务端日志发现,是由于字段长度问题,导致插入失败,于是加锁失败

原来pk字段长度只有32,row_key字段长度只有128,修改后的只读长度如上图所示

最后的最后,补充一个知识点

1、在整个全局事务中,每条SQL语句执行的时候都是一样的流程,先注册获取全局锁,然后才能提交,注意是每条SQL

2、所有的RM在执行本地操作的时候都是一样的流程,因为数据源被Seata代理,所以在执行各自本地的逻辑时,设计到数据库操作的,都是首先更改连接为非自动提交,然后进行分支注册,注册成功后连接可以提交了,最后报告分支状态。

3、分支注册会传lockKey,注册的过程就是获取全局锁的过程,也就是对这些lockKey包含的数据加锁的过程。如果store.lock.mode=db的话,就是向lock_table表插数据。

4、在整个全局事务执行过程中,有多少次数据库操作就有多少次分支注册、提交、报告。因为每次跟数据库的交互都要先获取Connection,最终获取到的都是ConnectionProxy

5、 所有RM(Resource Manager)本地事务都提交成功的话,整个全局事务算是提交成功了

Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeUpdate();