0
2
2
2
博客/.../

TiDB 8.5 测试环境下的收费长事务与悲观锁等待验证记录

 拍脑袋小助手  发表于  2026-09-23
原创测试

在医院HIS收费程序中,通常是先锁定收费单,再等待支付平台返回,如果外部调用变慢,数据库事务迟迟没有提交,其他处理线程更新同一张收费单时就可能等锁。业务端看到的可能是收费页面一直等待、支付回调处理缓慢,或重复点击后没有返回。

这次测试模拟一笔门诊收费串起排查过程:从事务和连接找到持锁会话,再核对收费主表和支付流水。

一、准备收费表和三个连接

测试库沿用 his_lab。A、B 是两个独立的业务连接,C 用于查询事务、锁和最终数据,不能用同一连接轮流代替。

会话 用途 权限
A 锁定收费单,保留未提交事务 测试表的读写权限
B 更新同一笔收费,观察锁等待 测试表的读写权限
C 查询事务、连接和锁信息 测试表查询权限及 PROCESS 权限

DATA_LOCK_WAITS 和 DEADLOCKS 要求 PROCESS 权限。缺少该权限时,TIDB_TRX 也只能查看当前用户的事务,可能漏掉其他账号持有的锁。

以下初始化操作使用有建库、建表权限的连接执行,供首次准备环境使用:

SET SESSION autocommit = 1;

CREATE DATABASE IF NOT EXISTS his_lab;

CREATE TABLE his_lab.outpatient_charge (
    id              BIGINT NOT NULL AUTO_INCREMENT,
    charge_no       VARCHAR(32) NOT NULL COMMENT '收费流水号',
    visit_no        VARCHAR(32) NOT NULL COMMENT '门诊就诊号',
    patient_no      VARCHAR(32) NOT NULL COMMENT '患者内部编号',
    total_amount    DECIMAL(12,2) NOT NULL,
    paid_amount     DECIMAL(12,2) NOT NULL DEFAULT 0,
    charge_status   VARCHAR(16) NOT NULL COMMENT 'UNPAID/PAYING/PAID/VOID',
    payment_no      VARCHAR(40) DEFAULT NULL,
    cashier_code    VARCHAR(16) DEFAULT NULL,
    created_at      DATETIME(3) NOT NULL,
    updated_at      DATETIME(3) NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY uk_charge_no (charge_no),
    KEY idx_visit_no (visit_no),
    UNIQUE KEY uk_payment_no (payment_no)
);

CREATE TABLE his_lab.outpatient_payment (
    id              BIGINT NOT NULL AUTO_INCREMENT,
    payment_no      VARCHAR(40) NOT NULL,
    charge_no       VARCHAR(32) NOT NULL,
    pay_channel     VARCHAR(16) NOT NULL,
    pay_amount      DECIMAL(12,2) NOT NULL,
    pay_status      VARCHAR(16) NOT NULL COMMENT 'PROCESSING/SUCCESS/FAILED',
    created_at      DATETIME(3) NOT NULL,
    updated_at      DATETIME(3) NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY uk_payment_no (payment_no),
    KEY idx_charge_no (charge_no)
);

收费主表保存收费状态,支付表保存支付流水。 payment_no 是业务侧全局唯一的支付流水号,一张收费单采用单次支付。

准备一笔 15 元的待收费记录:

INSERT INTO his_lab.outpatient_charge
(
    charge_no,
    visit_no,
    patient_no,
    total_amount,
    paid_amount,
    charge_status,
    created_at,
    updated_at
)
VALUES
(
    'SF202609190001872',
    'MZ20260919084236',
    'P202609190318',
    15.00,
    0,
    'UNPAID',
    NOW(3),
    NOW(3)
);

这条记录在自动提交状态下写入,后续 A、B 的回滚不会撤销它。会话 C 也使用独立的新连接,保持 autocommit = 1,不在显式事务中查询最终结果,避免读到旧快照。

二、确认事务模式和超时参数

在 A、B 中分别查询当前会话参数:

SELECT
    @@SESSION.tidb_txn_mode AS txn_mode,
    @@SESSION.innodb_lock_wait_timeout AS lock_wait_timeout,
    @@SESSION.tidb_idle_transaction_timeout AS idle_trx_timeout;

TiDB 8.5 的默认值如下,实际配置以查询结果为准:

参数 默认值 含义
tidb_txn_mode pessimistic 默认事务模式
innodb_lock_wait_timeout 50 悲观锁等待超时,单位为秒
tidb_idle_transaction_timeout 0 不启用空闲事务会话超时

新建集群从 v3.0.8 起默认使用悲观事务模式,旧集群升级不会自动改变原来的事务模式。本次测试使用 BEGIN PESSIMISTIC,它的优先级高于 tidb_txn_mode。

tidb_idle_transaction_timeout 从 v7.6.0 引入,判断的是事务中没有正在处理的请求、正在等待客户端下一条请求的空闲时间。正在执行 SQL 或等锁,不属于这种空闲状态。

三、会话 A 保留一笔未提交的收费事务

在 A 中选择默认数据库,记录连接 ID,再开启事务。本轮先在这个测试连接中关闭空闲事务超时,避免观察期间 A 被提前断开;第十七节单独说明该参数。

USE his_lab;

SELECT CONNECTION_ID() AS session_id;

SET SESSION tidb_idle_transaction_timeout = 0;

BEGIN PESSIMISTIC;

SELECT
    charge_no,
    visit_no,
    total_amount,
    paid_amount,
    charge_status
FROM his_lab.outpatient_charge
WHERE charge_no = 'SF202609190001872'
FOR UPDATE;

SELECT ... FOR UPDATE 使用当前读,读取最新已提交的数据并加锁。确认状态为 UNPAID 后,将收费单改为处理中:

UPDATE his_lab.outpatient_charge
SET charge_status = 'PAYING',
    cashier_code = 'CASH01',
    updated_at = NOW(3)
WHERE charge_no = 'SF202609190001872';

在同一个事务中写入支付请求记录:

INSERT INTO his_lab.outpatient_payment
(
    payment_no,
    charge_no,
    pay_channel,
    pay_amount,
    pay_status,
    created_at,
    updated_at
)
VALUES
(
    'ZF202609190009731',
    'SF202609190001872',
    'WECHAT',
    15.00,
    'PROCESSING',
    NOW(3),
    NOW(3)
);

到这里保留 A 连接,不执行 COMMIT 或 ROLLBACK,也不继续发送请求。这对应程序持有收费记录的锁、等待外部接口返回的情况。A 对收费状态的修改和新增支付流水都尚未提交。

其他事务修改同一条收费记录时会等锁;普通快照读可以读取之前已提交的版本,不能因为查询仍能返回数据就认定没有锁。

四、从 CLUSTER_TIDB_TRX 找到事务

TIDB_TRX 显示当前 TiDB Server 的用户事务;CLUSTER_TIDB_TRX 汇总各 TiDB Server 的事务,多一个 INSTANCE 字段。以下排查 SQL 均在会话 C 中执行。

SELECT
    INSTANCE,
    ID AS trx_id,
    START_TIME,
    TIMESTAMPDIFF(
        SECOND,
        START_TIME,
        NOW()
    ) AS trx_seconds,
    STATE,
    WAITING_START_TIME,
    SESSION_ID,
    USER,
    DB,
    MEM_BUFFER_KEYS,
    MEM_BUFFER_BYTES,
    CURRENT_SQL_DIGEST_TEXT
FROM information_schema.CLUSTER_TIDB_TRX
WHERE DB = 'his_lab'
ORDER BY START_TIME;

ID 是事务的 start_ts,SESSION_ID 是连接 ID,二者不能混用。START_TIME 是事务开始时间,trx_seconds 据此计算事务已持续的秒数。MEM_BUFFER_KEYS 和 MEM_BUFFER_BYTES 分别是事务内存缓冲区的键数与键值总字节数,不等同于业务行数和连接总内存。

这里的 DB 是连接当前的默认数据库,不是事务访问过的数据库清单。前面 A 执行了 USE his_lab,才能按这个条件定位。应用只使用 his_lab.表名、没有选择默认库时,该筛选可能漏掉事务,应结合连接 ID、账号或 RELATED_TABLE_IDS 检查。

A 停止发送请求后,预期可以看到:

字段 预期状态
SESSION_ID 与 A 的 CONNECTION_ID() 一致
DB his_lab
STATE Idle
CURRENT_SQL_DIGEST_TEXT 当前没有执行中的 SQL,通常为 NULL
trx_seconds 随事务持续而增加

Idle 表示事务正在等待客户端输入,事务仍然打开,之前取得的锁仍可能保留。只看连接处于空闲状态,容易漏掉这种事务。

五、关联应用连接

把事务和连接按实例、Session ID 关联:

SELECT
    t.INSTANCE,
    t.ID AS trx_id,
    t.START_TIME,
    TIMESTAMPDIFF(
        SECOND,
        t.START_TIME,
        NOW()
    ) AS trx_seconds,
    t.STATE AS trx_state,
    t.SESSION_ID,
    t.USER,
    p.HOST,
    p.COMMAND,
    p.TIME AS process_time,
    LEFT(p.INFO, 200) AS current_sql
FROM information_schema.CLUSTER_TIDB_TRX t
LEFT JOIN information_schema.CLUSTER_PROCESSLIST p
       ON p.INSTANCE = t.INSTANCE
      AND p.ID = t.SESSION_ID
WHERE t.DB = 'his_lab'
ORDER BY t.START_TIME;

用 HOST 对应应用连接来源,再结合账号和连接 ID 定位程序。p.TIME 的单位也是秒,但反映当前命令或状态的持续时间,不能替代从事务开始时间计算的 trx_seconds。

六、回看事务执行过的 SQL

ALL_SQL_DIGESTS 保存事务执行过的 SQL Digest,最多记录前 50 条语句。需要回看 A 做过什么时,再对指定事务调用 TIDB_DECODE_SQL_DIGESTS()。

在 C 中,将下一行的 NULL 替换为第四节查到的目标事务 ID:

SET @trx_id = NULL;

SELECT
    INSTANCE,
    ID AS trx_id,
    SESSION_ID,
    TIDB_DECODE_SQL_DIGESTS(
        ALL_SQL_DIGESTS
    ) AS trx_sqls
FROM information_schema.CLUSTER_TIDB_TRX
WHERE ID = @trx_id;

本例中需要对应到锁定收费单的 SELECT ... FOR UPDATE、修改收费状态的 UPDATE,以及写入支付流水的 INSERT。解码结果是归一化 SQL,业务参数会被替换,不能依靠它恢复完整收费流水号。

解码依赖 Statement Summary 中保存的 SQL 信息,未找到对应语句时可能返回 NULL。函数开销较高,先定位事务,再解析该事务的摘要,不对全部事务逐行调用。

七、会话 B 更新同一笔收费

A 保持未提交状态。在 B 中执行:

USE his_lab;

SELECT CONNECTION_ID() AS session_id;

SET SESSION tidb_idle_transaction_timeout = 0;

SET SESSION innodb_lock_wait_timeout = 15;

BEGIN PESSIMISTIC;

UPDATE his_lab.outpatient_charge
SET payment_no = 'ZF202609190009731',
    paid_amount = 15.00,
    charge_status = 'PAID',
    updated_at = NOW(3)
WHERE charge_no = 'SF202609190001872';

这条 UPDATE 用于构造支付回调更新收费单时的冲突,没有包含支付流水落账的完整业务逻辑。B 修改的记录被 A 持有,预期进入悲观锁等待;持续拿不到锁时,返回错误码 1205。

15 秒只用于缩短本例的等待时间。第八至第十节的查询应事先在 C 中准备好,在 B 等待期间按需执行;如果 B 已超时,等待关系可能已经消失。需要重新观察时,先在 B 执行 ROLLBACK,再重新执行本节的 BEGIN PESSIMISTIC 和 UPDATE,A 保持不动。

锁等待超时会回滚失败的语句,不代表整个显式事务已经结束。本文在第十三节统一结束 A、B 两个事务。

八、查看 LockWaiting 状态

在 C 中查询:

SELECT
    INSTANCE,
    ID AS trx_id,
    START_TIME,
    STATE,
    WAITING_START_TIME,
    SESSION_ID,
    CURRENT_SQL_DIGEST_TEXT
FROM information_schema.CLUSTER_TIDB_TRX
WHERE DB = 'his_lab'
ORDER BY START_TIME;

B 正在加悲观锁时,STATE 可以显示为 LockWaiting,WAITING_START_TIME 是该状态下等待开始的时间。但 TiDB 在悲观加锁操作开始时就会进入这个状态,即使没有被其他事务阻塞,也可能短暂出现。

本例确实存在 A 持锁、B 等锁,仍要通过 DATA_LOCK_WAITS 确认对应关系,不能只凭 LockWaiting 下结论。

九、查看谁在等待谁

DATA_LOCK_WAITS 收集整个集群各 TiKV 节点当前的等锁信息。TiDB 8.5 文档说明,它包含悲观事务等锁和乐观事务被阻塞的信息,不是只面向悲观事务。

SELECT
    TRX_ID,
    CURRENT_HOLDING_TRX_ID,
    SQL_DIGEST_TEXT,
    KEY_INFO
FROM information_schema.DATA_LOCK_WAITS;

本例等待关系应对应如下:

字段 对应内容
TRX_ID B 的事务 ID,即 B 的 start_ts
CURRENT_HOLDING_TRX_ID A 的事务 ID,即 A 的 start_ts
SQL_DIGEST_TEXT B 正在等待的归一化 UPDATE,能够查到摘要时显示
KEY_INFO 被等待的 Key 对应的表、行或索引信息

KEY_INFO 是 JSON 格式的文本。本例应能关联到 his_lab.outpatient_charge;具体锁在记录键还是索引键上,要看返回的 handle_value、index_name、index_values 等信息,不能事先写死。不同 Key 类型显示的字段也不同。

乐观事务被阻塞时,SQL_DIGEST 和 SQL_DIGEST_TEXT 当前为 NULL,可以按事务 ID 关联 CLUSTER_TIDB_TRX,再查看该事务的 SQL 摘要。

十、关联等待事务与持锁事务

SELECT
    w.TRX_ID AS waiting_trx_id,
    wt.INSTANCE AS waiting_instance,
    wt.SESSION_ID AS waiting_session_id,
    wt.USER AS waiting_user,
    wt.START_TIME AS waiting_trx_start,
    wt.STATE AS waiting_state,

    w.CURRENT_HOLDING_TRX_ID AS holding_trx_id,
    ht.INSTANCE AS holding_instance,
    ht.SESSION_ID AS holding_session_id,
    ht.USER AS holding_user,
    ht.START_TIME AS holding_trx_start,
    ht.STATE AS holding_state,

    w.SQL_DIGEST_TEXT AS waiting_sql,
    w.KEY_INFO
FROM information_schema.DATA_LOCK_WAITS w
LEFT JOIN information_schema.CLUSTER_TIDB_TRX wt
       ON wt.ID = w.TRX_ID
LEFT JOIN information_schema.CLUSTER_TIDB_TRX ht
       ON ht.ID = w.CURRENT_HOLDING_TRX_ID;

用这条查询把等待关系对应到 A、B 的连接 ID,再结合第五节的 HOST 找到应用连接来源。关联使用事务 ID,不能用 Session ID 去匹配 TRX_ID。

这些系统表不是同一时刻的一致性快照。采集过程中事务结束、连接断开,或持锁事务已经不在当前 TiDB 用户事务列表中,都可能使 LEFT JOIN 后的部分字段为空。此时保留锁表中的事务 ID 和 Key 信息继续核对,不能把空值直接解释为没有阻塞者。

十一、按需查询 DATA_LOCK_WAITS

DATA_LOCK_WAITS 每次查询都需要从所有 TiKV 节点收集信息,添加 WHERE 条件也不能避免这一步。集群较大、负载较高时,高频查询可能造成性能抖动,各 TiKV 返回的数据也不保证属于同一时刻。

日常检查先用 CLUSTER_TIDB_TRX 找持续时间较长或状态异常的事务,需要确认锁关系时再查 DATA_LOCK_WAITS。本例第九节和第十节按排查需要选用,不必反复执行相同的全局锁信息采集。

十二、检查死锁历史

本例只有 B 等待 A,A 没有再等待 B,不构成循环等待。怀疑发生过死锁时,可以查询:

SELECT
    INSTANCE,
    DEADLOCK_ID,
    OCCUR_TIME,
    RETRYABLE,
    TRY_LOCK_TRX_ID,
    CURRENT_SQL_DIGEST_TEXT,
    KEY_INFO,
    TRX_HOLDING_LOCK
FROM information_schema.CLUSTER_DEADLOCKS
WHERE OCCUR_TIME >= NOW() - INTERVAL 1 HOUR
ORDER BY OCCUR_TIME DESC;

DEADLOCKS 保存单个 TiDB Server 最近的死锁事件,CLUSTER_DEADLOCKS 汇总各节点。默认每个节点保留最近 10 次事件,通过 pessimistic-txn.deadlock-history-capacity 调整;一次死锁可以对应多行,不能把行数当成事件数。跨节点查看时还要带上 INSTANCE,DEADLOCK_ID 不保证全局唯一。

默认不收集可重试的死锁,可通过 pessimistic-txn.deadlock-history-collect-retryable 控制。记录容量有限且不持久化,查询为空不能证明过去没有发生过死锁。

这次单向等待不应新增由 A、B 构成的死锁事件;集群里已有其他死锁记录时,整张表仍可能有数据。锁等待超时 1205 与死锁错误 1213 也要分开判断。

十三、结束 A、B 两个事务

完成观察后,在 A 中执行:

ROLLBACK;

如果 B 的 UPDATE 仍在等待,它获得锁后可以继续完成,但修改仍在 B 的显式事务中,尚未提交。如果 B 已经返回 1205,失败语句不会在 A 释放锁后自行重试。

本例只验证锁等待,不完成收费入账。等 B 的语句返回成功或超时后,都在 B 中执行:

ROLLBACK;

不能只回滚 A,再把 B 留在未提交状态。也不要在这个验证流程中提交 B:它只更新收费主表,没有完成支付流水的相应处理。事务回滚和断开连接时的回滚行为,可对照 TiDB 事务概览。

现场处理长事务时,先对应收费流水、支付流水和支付平台结果,再确定是否终止连接。TiDB 8.5 默认开启 enable-global-kill,启用时可以跨 TiDB Server 终止连接;如果关闭了该配置,不能沿用这一前提。KILL QUERY 只终止当前语句,不能代替结束一个空闲的持锁事务;终止整个连接使用 KILL CONNECTION,参数是连接 ID。

数据库事务回滚不会撤销外部支付平台已经完成的支付,这部分需要按业务流水另行核对。

十四、核对回滚后的业务数据

A、B 都回滚后,在自动提交的 C 连接中查询收费记录:

SELECT
    charge_no,
    visit_no,
    total_amount,
    paid_amount,
    charge_status,
    payment_no,
    cashier_code,
    updated_at
FROM his_lab.outpatient_charge
WHERE charge_no = 'SF202609190001872';

再查支付流水:

SELECT
    payment_no,
    charge_no,
    pay_channel,
    pay_amount,
    pay_status,
    created_at,
    updated_at
FROM his_lab.outpatient_payment
WHERE charge_no = 'SF202609190001872'
ORDER BY created_at;

没有其他连接修改这笔数据时,预期恢复到初始化状态:

对象 预期结果
收费主记录 仍有一条,初始化时已经提交
total_amount 15.00
paid_amount 0.00
charge_status UNPAID
收费主表的 payment_no NULL
cashier_code NULL
支付流水 ZF202609190009731 不存在

A 回滚撤销的是收费主表的 PAYING、收费员及更新时间修改,以及支付表中新增的整条记录。收费主表的 payment_no 和 paid_amount 由 B 的 UPDATE 修改;B 如果曾获得锁并执行成功,这部分也由 B 的回滚撤销。

十五、同时核对收费金额和支付流水

状态字段之外,把收费金额与成功支付流水一起查:

SELECT
    c.charge_no,
    c.visit_no,
    c.total_amount,
    c.paid_amount,
    c.charge_status,
    COUNT(
        CASE
            WHEN p.pay_status = 'SUCCESS'
            THEN 1
        END
    ) AS success_pay_count,
    COALESCE(
        SUM(
            CASE
                WHEN p.pay_status = 'SUCCESS'
                THEN p.pay_amount
                ELSE 0
            END
        ),
        0
    ) AS success_pay_amount
FROM his_lab.outpatient_charge c
LEFT JOIN his_lab.outpatient_payment p
       ON p.charge_no = c.charge_no
WHERE c.charge_no = 'SF202609190001872'
GROUP BY
    c.charge_no,
    c.visit_no,
    c.total_amount,
    c.paid_amount,
    c.charge_status;

按前面两个事务都回滚的流程,成功支付笔数和金额应为 0、0.00。如果另一次核对中出现下面的数据,就需要查明收费状态与支付流水为什么没有对应上:

字段 不一致状态示例
charge_status UNPAID
paid_amount 0.00
success_pay_count 1
success_pay_amount 15.00

这不是前面双回滚流程的预期结果,也不能据此再次发起扣款。应对应支付平台流水,确认是否需要补记或冲正;这条 SQL 只能核对数据库中已记录的数据,不能证明外部平台的实际支付状态。

十六、支付流水的唯一约束

支付表用 UNIQUE KEY uk_payment_no (payment_no) 保证支付流水号唯一,收费主表用 UNIQUE KEY uk_charge_no (charge_no) 保证收费流水号唯一。收费主表的 payment_no 则用于关联本例的单次支付。

相同支付流水号重复写入时,唯一约束可以阻止重复记录最终提交,但不能阻止应用用不同流水号再次发起扣款,也不能替代完整的业务幂等处理。唯一约束检查与事务提交的关系,可参阅 TiDB 事务与约束检查。

按收费单检查多条成功支付记录:

SELECT
    charge_no,
    COUNT(*) AS success_count,
    SUM(pay_amount) AS success_amount
FROM his_lab.outpatient_payment
WHERE pay_status = 'SUCCESS'
GROUP BY charge_no
HAVING COUNT(*) > 1;

本例采用单次支付,多条 SUCCESS 记录需要核对。分次支付、组合支付业务允许一张收费单对应多条支付记录,不能直接按这个查询结果认定重复收费。

十七、设置空闲事务保护

前面的锁等待验证结束后,可以在单独的测试连接中设置会话级保护:

SET SESSION tidb_idle_transaction_timeout = 300;

该连接进入事务后,如果没有正在处理的请求,持续等待客户端新请求超过 300 秒,TiDB 会终止会话,未提交事务随连接关闭回滚。正在等锁的 B 有执行中的请求,应由锁等待超时等机制处理,不能靠这个参数限制。

需要修改新建连接的默认值时,全局设置写法为:

SET GLOBAL tidb_idle_transaction_timeout = 300;

全局值修改后,已有连接的会话值不会自动变成 300。连接池里的旧连接尤其需要留意,可以在相应连接中核对:

SELECT
    @@SESSION.tidb_idle_transaction_timeout AS session_timeout,
    @@GLOBAL.tidb_idle_transaction_timeout AS global_timeout;

设置前要确认正常事务可能空闲多久,以及应用能否处理连接被关闭。把外部支付调用放在持锁事务中等待,仍需从程序的事务边界处理,超时参数只能限制持续影响的时间。

十八、长事务日志阈值

tidb_expensive_txn_time_threshold 从 v7.2.0 引入,TiDB 8.5 默认是 600 秒。事务持续时间超过阈值、仍未提交或回滚时,TiDB 会记录 expensive transaction 日志。这个参数用于记录日志,不会自动终止事务。

它虽然是 GLOBAL 变量,但只作用于当前连接的 TiDB 实例,不持久化到整个集群。多 TiDB Server 环境下,不能只检查一个节点就认为所有节点设置相同。收费已经出现等待时,直接检查事务和锁关系,不要等日志阈值触发再处理。

十九、长事务与 GC

活跃事务可能阻碍 GC Safe Point 推进。tidb_gc_max_wait_time 控制这种阻碍允许持续的最长时间,TiDB 8.5 默认是 86400 秒;它不负责终止数据库连接。

这个值也不表示悲观事务可以持锁 24 小时。TiDB 的 performance.max-txn-ttl 默认是 3600000 毫秒,即 1 小时,用于限制事务持锁时长。超过后,锁可能被其他事务清理,原事务不能再假定可以正常提交。

结束验证时,需要先核对 A、B 的事务已经结束,原来的锁等待关系不再出现,再确认收费单恢复为 UNPAID、支付流水不存在。实际收费处理中,锁等待消失后仍要核对支付平台结果,不能把数据库超时直接当成扣款失败。

0
2
2
2

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论