刘光辉
昨天 bb638871a7fb692d80f1b7a758f991dc0879002c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
package jnpf.audit.sdk;
 
import jnpf.audit.AuditApiConsts;
import jnpf.audit.model.AuditEventDTO;
import jnpf.util.RandomUtil;
import org.slf4j.MDC;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
 
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
 
/**
 * 事务级审计缓冲 holder(spec §3.3/§5.5 契约的最小实现)。
 *
 * <p>职责:把同一事务内提交的审计事件缓冲到 afterCommit 才异步发出(回滚不发);operationId 的
 * 生成与 MDC 写入统一归本类单源管理(终验#6:collector 先写 MDC 会破坏 previousMdc 语义)。
 *
 * <p><b>operationId 语义(M2 spec §2)</b>:归属最外层的「操作作用域」({@link #beginOperation}
 * 返回的 OperationScope),作用域内的全部事务——含 REQUIRES_NEW 子事务——共享同一个 id;
 * 没有作用域时退化为每个事务自己一个 id。缓冲仍按事务划分(回滚不发的语义不变),
 * 只是「缓冲身份」与「操作身份」自此解耦。
 *
 * <p>全类只有一条 id 规则:<b>决定 id 时先读 MDC,有值则沿用,无值才生成</b>——
 * {@link #submit} 无事务分支、{@link #beginOperation} 无事务分支、{@link #createBuffer}
 * 三处一致,无例外。跨事务(REQUIRES_NEW)的沿用在挂起点由两条途径承接:
 * {@code previousMdc} 非 null 时靠它本身,为 null 时靠 {@code TxAuditBuffer.scopeOwned}——
 * 两者的等价性依赖下面 {@link #createBuffer} 处标明的不变量。
 *
 * <p>本类的时序注释对应事务级缓冲的并发时序契约,均经过验证——重构时不要随手删改,
 * 每一处都对应一个真实存在过的竞态场景。
 * 回归守卫:{@code bash tasks/audit-opid-checks.sh}(十一项并发时序矩阵)。
 * 平台雪花工具经现场核实为 {@link RandomUtil#uuId()}(= {@code String.valueOf(YitIdHelper.nextId())})。
 */
public class AuditTxHolder {
    private static final Object RESOURCE_KEY = new Object();
 
    /** 事务级缓冲:{operationId + 不可变事件列表};MDC 保存/恢复防线程池串号(评审#6) */
    static class TxAuditBuffer {
        final String operationId;
        final String previousMdc;                       // 进入前的 MDC 旧值,afterCompletion 恢复
        final List<AuditEventDTO> events = new ArrayList<>();
        /**
         * 作用域是否**经由 {@link #beginOperation} 的事务分支**挂在本 buffer 上。
         *
         * <p><b>注意它不等于「本 buffer 的 id 归属某个作用域」</b>:形态甲(作用域在事务外开启、
         * buffer 由 {@link #submit} 在事务内创建)下 id 同样归属该作用域,但本标记为 false——
         * 那种形态的沿用由 {@code previousMdc} 承接,不走本标记。本标记只负责补上
         * {@code previousMdc == null} 那一格。
         *
         * <p>为什么需要它:作用域由事务分支自己创建时 previousMdc 恒为 null,此时若挂起点按
         * previousMdc 恢复会把 MDC 清空、内层子事务另生成 id → 同一次操作分叉
         * (spec §1.3;这正是今日 lims 四处的事务形态)。
         *
         * <p>非 final:作用域可能**晚于** buffer 出现(层 1 先建 buffer,随后才进入标注方法,
         * spec §4 形态丁),故 beginOperation 的创建与复用两条路径都要置位。
         * buffer 绑在 TSM 事务资源上、线程内独占,无并发写风险。
         */
        boolean scopeOwned;
        TxAuditBuffer(String operationId, String previousMdc) {
            this.operationId = operationId; this.previousMdc = previousMdc;
        }
    }
 
    /** operationId 的生成与 MDC 写入统一归本类管理(终验#6:collector 先写 MDC 会破坏 previousMdc 语义) */
    public static void submit(AuditEventDTO event, AuditEventPublisher publisher) {
        if (!TransactionSynchronizationManager.isSynchronizationActive()) {
            String outerMdc = MDC.get(AuditApiConsts.MDC_OPERATION_ID);
            String opId = event.getOperationId() != null ? event.getOperationId()
                    : (outerMdc != null ? outerMdc : RandomUtil.uuId());   // 平台雪花工具,类名对照现场
            publisher.publishAsync(Collections.singletonList(event.toBuilder().operationId(opId).build()));
            if (outerMdc == null) {
                MDC.remove(AuditApiConsts.MDC_OPERATION_ID);   // 自己没写过就保持干净
            }
            return;
        }
        TxAuditBuffer buffer = (TxAuditBuffer) TransactionSynchronizationManager.getResource(RESOURCE_KEY);
        if (buffer == null) {
            buffer = createBuffer(publisher);
        }
        buffer.events.add(event.toBuilder().operationId(buffer.operationId).build());
    }
 
    /** 操作作用域:close() 只在**无事务**分支需要恢复 MDC;有事务时由 afterCompletion 统一恢复。 */
    public interface OperationScope extends AutoCloseable {
        String operationId();
        @Override void close();   // 覆写掉 AutoCloseable 的 throws Exception,调用方无需 catch
    }
 
    /**
     * getOrCreate 语义(M2 契约兑现,M1 已在 currentOperationId 的 javadoc 预告):
     * 进入业务方法时即确保「本事务有 operationId 且已写入 MDC」,供内层旧 BizLogAspect 读取。
     *
     * <p>为什么不能靠 {@link #submit}:submit 发生在 proceed() 之后,而内层旧切面写日志更早,
     * 那时 MDC 还没值 → A/B 配不上对。
     *
     * <p>MDC 生命周期仍锁在本类一处(单源契约):有事务走 afterCompletion 恢复,
     * 无事务由返回的 scope.close() 恢复——两条路径都保存进入前旧值,防线程池复用串号。
     */
    public static OperationScope beginOperation(AuditEventPublisher publisher) {
        return beginOperation(publisher, null);
    }
 
    /**
     * 使用可信业务凭证派生的显式关联键开启作用域;已有外层作用域时仍以外层为准。
     */
    public static OperationScope beginOperation(AuditEventPublisher publisher, String preferredOperationId) {
        if (!TransactionSynchronizationManager.isSynchronizationActive()) {
            final String previousMdc = MDC.get(AuditApiConsts.MDC_OPERATION_ID);
            final String opId = previousMdc != null ? previousMdc
                    : (preferredOperationId != null ? preferredOperationId : RandomUtil.uuId());
            MDC.put(AuditApiConsts.MDC_OPERATION_ID, opId);
            return new OperationScope() {
                @Override public String operationId() { return opId; }
                @Override public void close() { restoreMdc(previousMdc); }
            };
        }
        TxAuditBuffer buffer = (TxAuditBuffer) TransactionSynchronizationManager.getResource(RESOURCE_KEY);
        if (buffer == null) {
            buffer = createBuffer(publisher, preferredOperationId);
        }
        // 作用域标记:创建与复用两条路径都要置(spec §4 形态丁——层 1 可能先建了 buffer,
        // 作用域晚于它出现;只在创建路径置位会让该形态的 REQUIRES_NEW 内层重新分叉)
        buffer.scopeOwned = true;
        final String opId = buffer.operationId;
        return new OperationScope() {
            @Override public String operationId() { return opId; }
            @Override public void close() { /* 有事务:MDC 由 afterCompletion 恢复,此处刻意 no-op */ }
        };
    }
 
    private static TxAuditBuffer createBuffer(final AuditEventPublisher publisher) {
        return createBuffer(publisher, null);
    }
 
    private static TxAuditBuffer createBuffer(final AuditEventPublisher publisher, String preferredOperationId) {
        // 关键时序:先保存外层旧值,再决定本事务 operationId 并写 MDC(终验#6)
        String previousMdc = MDC.get(AuditApiConsts.MDC_OPERATION_ID);
        // M2 语义(spec §2):operationId 归属最外层操作作用域——MDC 已有值即代表外层已有
        // 作用域,一律沿用;无值才生成。与 submit(:42)/beginOperation(:77) 两条无事务分支的
        // 写法在此拉齐,全类只有一条规则:"先看 MDC,有则沿用"。
        //
        // ⚠️ 本行确立一条**全类依赖的不变量**:凡本方法产出的 buffer,
        //        previousMdc != null  ⟹  operationId == previousMdc
        // suspend 的三目(scopeOwned ? operationId : previousMdc)正是靠它让两支殊途同归——
        // previousMdc 非 null 时两支取值必然相等,故三目只在 previousMdc == null 时才真正分岔
        // (即行为变更半径 = spec §4 形态丙/丁/戊)。**若将来给 id 决定逻辑加旁路(例如允许调用方
        // 显式传入 id)而打破该不变量,suspend 的第二支会静默失去沿用能力——改这里先去看 suspend。**
        final TxAuditBuffer created = new TxAuditBuffer(
                previousMdc != null ? previousMdc
                        : (preferredOperationId != null ? preferredOperationId : RandomUtil.uuId()), previousMdc);
        MDC.put(AuditApiConsts.MDC_OPERATION_ID, created.operationId);
        TransactionSynchronizationManager.bindResource(RESOURCE_KEY, created);
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            private TxAuditBuffer suspended;
            @Override public void suspend() {   // REQUIRES_NEW 内层开始:TSM 与 MDC 同步挂起
                suspended = (TxAuditBuffer) TransactionSynchronizationManager.unbindResource(RESOURCE_KEY);
                // spec §4,按 (scopeOwned, previousMdc) 四组合读:
                //   (true , null) 形态丙/丁/戊 → 恢复 operationId,内层沿用 ←【本次唯一被改变的组合】
                //   (true , X   ) → 恢复 operationId,但由 createBuffer 的不变量 X == operationId,
                //                   与走第二支等价,无行为变化
                //   (false, X   ) 形态甲 → 恢复 previousMdc = X,内层照样沿用(作用域存在但不置标记)
                //   (false, null) 形态乙 → 恢复 null(MDC 清空),内层另生成 id
                // 后两格逐字保留 M1「终验#6:只解绑资源不动 MDC 的话,内层会看到外层 opId」的原行为
                // (S5 断言守之);第一格是本次**有意覆盖**该结论——终验#6 是在「一个事务一个 id」
                // 语义下作出的,M2 引入 beginOperation 后语义改为「一次用户操作一个 id」。
                // 改动前先读 spec §2 与 createBuffer 处标明的不变量。
                restoreMdc(suspended.scopeOwned ? suspended.operationId : suspended.previousMdc);
            }
            @Override public void resume() {
                TransactionSynchronizationManager.bindResource(RESOURCE_KEY, suspended);
                MDC.put(AuditApiConsts.MDC_OPERATION_ID, suspended.operationId);
            }
            @Override public void afterCommit() {
                if (created.events.isEmpty()) {
                    return;   // beginOperation 建了缓冲但本次没有事件(如注解方法内无审计动作),不空发
                }
                publisher.publishAsync(new ArrayList<>(created.events));   // 回滚不发
            }
            @Override public void afterCompletion(int status) {
                if (TransactionSynchronizationManager.hasResource(RESOURCE_KEY)) {
                    TransactionSynchronizationManager.unbindResource(RESOURCE_KEY);   // 防泄漏
                }
                restoreMdc(created.previousMdc);   // 恢复外层值 / 清理防线程池串号
            }
        });
        return created;
    }
 
    private static void restoreMdc(String previous) {
        if (previous != null) {
            MDC.put(AuditApiConsts.MDC_OPERATION_ID, previous);
        } else {
            MDC.remove(AuditApiConsts.MDC_OPERATION_ID);
        }
    }
 
    /**
     * 读接口(终验四轮①,M1 只声明不消费):返回当前事务已有的 operationId;
     * 无活跃缓冲返回 null,**本方法不触发创建**(终验五轮①:javadoc 不承诺超出代码能力的语义)。
     * M2 设计契约:@AuditLog aspect 不得自行生成 operationId——若需要 getOrCreate 语义,
     * M2 时在本类新增对应方法,由 holder 单源生成后写 MDC 供旧 BizLogAspect 读取;
     * aspect 与 holder 双源生成会破坏 A/B 精确配对。
     */
    public static String currentOperationId() {
        TxAuditBuffer buffer = (TxAuditBuffer) TransactionSynchronizationManager.getResource(RESOURCE_KEY);
        return buffer != null ? buffer.operationId : null;
    }
}