Vanillaxi opened a new issue, #1182: URL: https://github.com/apache/incubator-seata-go/issues/1182
### ✅ 验证清单 - [x] 🔍 我已经搜索过 [现有 Issues](https://github.com/apache/incubator-seata-go/issues),确信这不是重复问题 - [x] 🛠️ 我愿意自己处理这个议题 ### 🚀 Go 版本 go1.24.3 darwin/arm64 ### 📦 Seata-go 版本 master, commit 3bf7358 (fetched 2026-09-02) ### 💾 操作系统 🍎 macOS ### 📝 Bug 描述 当前元数据缓存以 DBType 为键保存在全局注册表中,注册第二个同类型数据源时,会替换第一个数据源的缓存引用。执行器仍按 DBType 获取缓存,因此第一个数据源可能使用第二个数据源的连接池查询元数据,或直接命中第二个数据源的同名表缓存。 `pkg/datasource/sql/datasource/datasource_manager.go` ``` func RegisterTableCache(dbType types.DBType, tableMetaCache TableMetaCache) { //line37 tableMetaCacheMap[dbType] = tableMetaCache } func GetTableCache(dbType types.DBType) TableMetaCache { return tableMetaCacheMap[dbType] } ``` ### 🔄 重现步骤 1. 在 Seata-go 源码仓库根目录(包含 go.mod 的目录)新建 `cache_isolation_test.go`。 2. 将下面折叠区中的完整代码保存到该文件。 3. 在仓库根目录执行: ``` go test -count=1 -v ./cache_isolation_test.go ``` <details> <summary>完整复现代码:cache_isolation_test.go</summary> ```go package investigation_test import ( "context" "database/sql" "testing" "github.com/DATA-DOG/go-sqlmock" "seata.apache.org/seata-go/v2/pkg/datasource/sql/datasource" cachemysql "seata.apache.org/seata-go/v2/pkg/datasource/sql/datasource/mysql" "seata.apache.org/seata-go/v2/pkg/datasource/sql/types" ) // Use production cache, registry and INFORMATION_SCHEMA loader. Only SQL I/O is mocked. // A nil config disables unrelated background refresh; each lookup still passes DBName. func newSource(t *testing.T) (*sql.DB, sqlmock.Sqlmock, datasource.TableMetaCache) { t.Helper() db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = db.Close() }) return db, mock, cachemysql.NewTableMetaInstance(db, nil) } func expectMeta(mock sqlmock.Sqlmock, schema, pk string) { mock.ExpectPrepare("SELECT (.+) FROM INFORMATION_SCHEMA.COLUMNS"). ExpectQuery().WithArgs(schema, "account"). WillReturnRows(sqlmock.NewRows([]string{ "TABLE_NAME", "TABLE_SCHEMA", "COLUMN_NAME", "DATA_TYPE", "COLUMN_TYPE", "COLUMN_KEY", "IS_NULLABLE", "COLUMN_DEFAULT", "EXTRA", }).AddRow("account", schema, pk, "bigint", "bigint", "PRI", "NO", nil, "")) mock.ExpectPrepare("SELECT (.+) FROM `INFORMATION_SCHEMA`.`STATISTICS`"). ExpectQuery().WithArgs(schema, "account"). WillReturnRows(sqlmock.NewRows([]string{"INDEX_NAME", "COLUMN_NAME", "NON_UNIQUE"}). AddRow("PRIMARY", pk, 0)) } func lookup(t *testing.T, cache datasource.TableMetaCache, schema string) string { t.Helper() meta, err := cache.GetTableMeta(context.Background(), schema, "account") if err != nil { t.Fatal(err) } pks := meta.GetPrimaryKeyOnlyName() if len(pks) != 1 { t.Fatalf("unexpected PKs: %v", pks) } return pks[0] } func verifySQL(t *testing.T, mocks ...sqlmock.Sqlmock) { t.Helper() for _, mock := range mocks { if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } } func TestWarmCacheKeepsSourceIsolation(t *testing.T) { previous := datasource.GetTableCache(types.DBTypeMySQL) t.Cleanup(func() { datasource.RegisterTableCache(types.DBTypeMySQL, previous) }) _, mockA, cacheA := newSource(t) _, mockB, cacheB := newSource(t) expectMeta(mockA, "db_a", "id_a") expectMeta(mockB, "db_b", "id_b") datasource.RegisterTableCache(types.DBTypeMySQL, cacheA) before := lookup(t, datasource.GetTableCache(types.DBTypeMySQL), "db_a") if before != "id_a" { t.Fatalf("invalid control: %s", before) } datasource.RegisterTableCache(types.DBTypeMySQL, cacheB) if got := lookup(t, datasource.GetTableCache(types.DBTypeMySQL), "db_b"); got != "id_b" { t.Fatalf("invalid B fixture: %s", got) } // Same lookup as updateExecutor.beforeImage and BaseUndoLogManager.Undo. after := lookup(t, datasource.GetTableCache(types.DBTypeMySQL), "db_a") direct := lookup(t, cacheA, "db_a") verifySQL(t, mockA, mockB) t.Logf("A before B registration=%s; A after B registration=%s; direct cacheA=%s; global cache is B=%v", before, after, direct, datasource.GetTableCache(types.DBTypeMySQL) == cacheB) if after != "id_a" { t.Errorf("source isolation violated: lookup(db_a, account) expected PK id_a, got %s", after) } } func TestColdCacheKeepsSourceConnection(t *testing.T) { previous := datasource.GetTableCache(types.DBTypeMySQL) t.Cleanup(func() { datasource.RegisterTableCache(types.DBTypeMySQL, previous) }) _, mockA, cacheA := newSource(t) _, mockB, cacheB := newSource(t) expectMeta(mockA, "app", "id_on_server_a") // Server B has an identically named schema/table with different metadata. expectMeta(mockB, "app", "id_on_server_b") datasource.RegisterTableCache(types.DBTypeMySQL, cacheA) if got := lookup(t, datasource.GetTableCache(types.DBTypeMySQL), "app"); got != "id_on_server_a" { t.Fatalf("invalid A control: %s", got) } datasource.RegisterTableCache(types.DBTypeMySQL, cacheB) actual := lookup(t, datasource.GetTableCache(types.DBTypeMySQL), "app") direct := lookup(t, cacheA, "app") verifySQL(t, mockA, mockB) t.Logf("A caller after B registration: metadata PK=%s; direct A PK=%s; B SQL expectations consumed", actual, direct) if actual != "id_on_server_a" { t.Errorf("wrong metadata connection: expected server A PK, got %s", actual) } } ``` </details> 场景 | 正确预期 | 实际结果 -- | -- | -- A、B 是不同数据库,都有 account 表;B 已缓存该表 | A 查询得到自己的主键 id_a | 返回 B 的主键 id_b A、B 是不同服务器,库名、表名相同;B 缓存尚未命中 | A 的元数据查询走 A 连接池 | 查询走 B 连接池,返回 B 的元数据 ### ✅ 预期行为 A 的元数据始终来自 A 的缓存与连接池,两个隔离性断言均应通过。 ### ❌ 实际行为 ``` === RUN TestWarmCacheKeepsSourceIsolation A before B registration=id_a; A after B registration=id_b; direct cacheA=id_a; global cache is B=true source isolation violated: lookup(db_a, account) expected PK id_a, got id_b --- FAIL: TestWarmCacheKeepsSourceIsolation === RUN TestColdCacheKeepsSourceConnection A caller after B registration: metadata PK=id_on_server_b; direct A PK=id_on_server_a; B SQL expectations consumed wrong metadata connection: expected server A PK, got id_on_server_b --- FAIL: TestColdCacheKeepsSourceConnection ``` 两个测试分别验证缓存命中和缓存未命中时的数据源隔离。当前实现下,两个测试的隔离性断言均失败:前者返回另一数据源的主键,后者使用另一数据源的连接池查询元数据。 ### 💡 可能的解决方案 创建数据源时,把缓存存入对应资源,在 `driver.go `创建 cache 后赋给 `DBResource.metaCache`,取消 AT 路径中按 DBType 全局注册缓存实例的做法。 然后再完善缓存内部的 key:使用明确的数据库/Schema/表名组合,避免同一资源内访问不同 Schema 的同名表时混用。 -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
