This is an automated email from the ASF dual-hosted git repository. zfeng pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-seata-go.git
The following commit(s) were added to refs/heads/master by this push: new 3b1ebc92 Optimize/at build lock key performance (#837) 3b1ebc92 is described below commit 3b1ebc92fe4698f84c7555df877cbb4b4fc2d923 Author: Wiggins <125641755+minat...@users.noreply.github.com> AuthorDate: Thu Jun 19 10:04:11 2025 +0800 Optimize/at build lock key performance (#837) * Refer to buildlockkey2 optimization #829 * Time complexity O(NM)-> O(NK) about buildlockkey and buildlockkey2 Increased readability #829 * update import sort #829 * update Encapsulation into util packages #829 --- pkg/datasource/sql/exec/at/base_executor.go | 35 +------ .../at/base_executor_test.go} | 104 +++++++++++++-------- .../sql/undo/builder/basic_undo_log_builder.go | 46 +-------- .../undo/builder/basic_undo_log_builder_test.go | 19 ++++ pkg/datasource/sql/util/lockkey.go | 75 +++++++++++++++ 5 files changed, 165 insertions(+), 114 deletions(-) diff --git a/pkg/datasource/sql/exec/at/base_executor.go b/pkg/datasource/sql/exec/at/base_executor.go index 05f44057..cda7d1ac 100644 --- a/pkg/datasource/sql/exec/at/base_executor.go +++ b/pkg/datasource/sql/exec/at/base_executor.go @@ -18,7 +18,6 @@ package at import ( - "bytes" "context" "database/sql" "database/sql/driver" @@ -359,37 +358,5 @@ func (b *baseExecutor) buildPKParams(rows []types.RowImage, pkNameList []string) // the string as local key. the local key example(multi pk): "t_user:1_a,2_b" func (b *baseExecutor) buildLockKey(records *types.RecordImage, meta types.TableMeta) string { - var ( - lockKeys bytes.Buffer - filedSequence int - ) - lockKeys.WriteString(meta.TableName) - lockKeys.WriteString(":") - - keys := meta.GetPrimaryKeyOnlyName() - - for _, row := range records.Rows { - if filedSequence > 0 { - lockKeys.WriteString(",") - } - pkSplitIndex := 0 - for _, column := range row.Columns { - var hasKeyColumn bool - for _, key := range keys { - if column.ColumnName == key { - hasKeyColumn = true - if pkSplitIndex > 0 { - lockKeys.WriteString("_") - } - lockKeys.WriteString(fmt.Sprintf("%v", column.Value)) - pkSplitIndex++ - } - } - if hasKeyColumn { - filedSequence++ - } - } - } - - return lockKeys.String() + return util.BuildLockKey(records, meta) } diff --git a/pkg/datasource/sql/undo/builder/basic_undo_log_builder_test.go b/pkg/datasource/sql/exec/at/base_executor_test.go similarity index 63% copy from pkg/datasource/sql/undo/builder/basic_undo_log_builder_test.go copy to pkg/datasource/sql/exec/at/base_executor_test.go index 465bf516..0caffe4e 100644 --- a/pkg/datasource/sql/undo/builder/basic_undo_log_builder_test.go +++ b/pkg/datasource/sql/exec/at/base_executor_test.go @@ -15,42 +15,16 @@ * limitations under the License. */ -package builder +package at import ( - "testing" - "github.com/stretchr/testify/assert" - "seata.apache.org/seata-go/pkg/datasource/sql/types" + "testing" ) -func TestBuildWhereConditionByPKs(t *testing.T) { - builder := BasicUndoLogBuilder{} - tests := []struct { - name string - pkNameList []string - rowSize int - maxInSize int - expectSQL string - }{ - {"test1", []string{"id", "name"}, 1, 1, "(`id`,`name`) IN ((?,?))"}, - {"test1", []string{"id", "name"}, 3, 2, "(`id`,`name`) IN ((?,?),(?,?)) OR (`id`,`name`) IN ((?,?))"}, - {"test1", []string{"id", "name"}, 3, 1, "(`id`,`name`) IN ((?,?)) OR (`id`,`name`) IN ((?,?)) OR (`id`,`name`) IN ((?,?))"}, - {"test1", []string{"id", "name"}, 4, 2, "(`id`,`name`) IN ((?,?),(?,?)) OR (`id`,`name`) IN ((?,?),(?,?))"}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - // todo add dbType param - sql := builder.buildWhereConditionByPKs(test.pkNameList, test.rowSize, "", test.maxInSize) - assert.Equal(t, test.expectSQL, sql) - }) - } -} - -func TestBuildLockKey(t *testing.T) { - var builder BasicUndoLogBuilder +func TestBaseExecBuildLockKey(t *testing.T) { + var exec baseExecutor columnID := types.ColumnMeta{ ColumnName: "id", @@ -69,6 +43,7 @@ func TestBuildLockKey(t *testing.T) { } columnsTwoPk := []types.ColumnMeta{columnID, columnUserId} + columnsThreePk := []types.ColumnMeta{columnID, columnUserId, columnAge} columnsMixPk := []types.ColumnMeta{columnName, columnAge} getColumnImage := func(columnName string, value interface{}) types.ColumnImage { @@ -92,11 +67,29 @@ func TestBuildLockKey(t *testing.T) { types.RecordImage{ TableName: "test_name", Rows: []types.RowImage{ - {[]types.ColumnImage{getColumnImage("id", 1), getColumnImage("userId", "one")}}, - {[]types.ColumnImage{getColumnImage("id", 2), getColumnImage("userId", "two")}}, + {[]types.ColumnImage{getColumnImage("id", 1), getColumnImage("userId", "user1")}}, + {[]types.ColumnImage{getColumnImage("id", 2), getColumnImage("userId", "user2")}}, + }, + }, + "test_name:1_user1,2_user2", + }, + { + "Three Primary Keys", + types.TableMeta{ + TableName: "test2_name", + Indexs: map[string]types.IndexMeta{ + "PRIMARY_KEY": {IType: types.IndexTypePrimaryKey, Columns: columnsThreePk}, + }, + }, + types.RecordImage{ + TableName: "test2_name", + Rows: []types.RowImage{ + {[]types.ColumnImage{getColumnImage("id", 1), getColumnImage("userId", "one"), getColumnImage("age", "11")}}, + {[]types.ColumnImage{getColumnImage("id", 2), getColumnImage("userId", "two"), getColumnImage("age", "22")}}, + {[]types.ColumnImage{getColumnImage("id", 3), getColumnImage("userId", "three"), getColumnImage("age", "33")}}, }, }, - "test_name:1_one,2_two", + "test2_name:1_one_11,2_two_22,3_three_33", }, { name: "Single Primary Key", @@ -125,10 +118,10 @@ func TestBuildLockKey(t *testing.T) { records: types.RecordImage{ TableName: "mixed_key", Rows: []types.RowImage{ - {Columns: []types.ColumnImage{getColumnImage("name", "Alice"), getColumnImage("age", 25)}}, + {Columns: []types.ColumnImage{getColumnImage("name", "mike"), getColumnImage("age", 25)}}, }, }, - expected: "mixed_key:Alice_25", + expected: "mixed_key:mike_25", }, { name: "Empty Records", @@ -152,10 +145,10 @@ func TestBuildLockKey(t *testing.T) { records: types.RecordImage{ TableName: "special", Rows: []types.RowImage{ - {Columns: []types.ColumnImage{getColumnImage("id", "a,b_c")}}, + {Columns: []types.ColumnImage{getColumnImage("id", "A,b_c")}}, }, }, - expected: "special:a,b_c", + expected: "special:A,b_c", }, { name: "Non-existent Key Name", @@ -173,11 +166,46 @@ func TestBuildLockKey(t *testing.T) { }, expected: "error_key:", }, + { + name: "Multiple Rows With Nil PK Value", + metaData: types.TableMeta{ + TableName: "nil_pk", + Indexs: map[string]types.IndexMeta{ + "PRIMARY_KEY": {IType: types.IndexTypePrimaryKey, Columns: []types.ColumnMeta{columnID}}, + }, + }, + records: types.RecordImage{ + TableName: "nil_pk", + Rows: []types.RowImage{ + {Columns: []types.ColumnImage{getColumnImage("id", nil)}}, + {Columns: []types.ColumnImage{getColumnImage("id", 123)}}, + {Columns: []types.ColumnImage{getColumnImage("id", nil)}}, + }, + }, + expected: "nil_pk:,123,", + }, + { + name: "PK As Bool And Float", + metaData: types.TableMeta{ + TableName: "type_pk", + Indexs: map[string]types.IndexMeta{ + "PRIMARY_KEY": {IType: types.IndexTypePrimaryKey, Columns: []types.ColumnMeta{columnName, columnAge}}, + }, + }, + records: types.RecordImage{ + TableName: "type_pk", + Rows: []types.RowImage{ + {Columns: []types.ColumnImage{getColumnImage("name", true), getColumnImage("age", 3.14)}}, + {Columns: []types.ColumnImage{getColumnImage("name", false), getColumnImage("age", 0.0)}}, + }, + }, + expected: "type_pk:true_3.14,false_0", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - lockKeys := builder.buildLockKey2(&tt.records, tt.metaData) + lockKeys := exec.buildLockKey(&tt.records, tt.metaData) assert.Equal(t, tt.expected, lockKeys) }) } diff --git a/pkg/datasource/sql/undo/builder/basic_undo_log_builder.go b/pkg/datasource/sql/undo/builder/basic_undo_log_builder.go index 8c5f20f2..d02604b8 100644 --- a/pkg/datasource/sql/undo/builder/basic_undo_log_builder.go +++ b/pkg/datasource/sql/undo/builder/basic_undo_log_builder.go @@ -22,12 +22,12 @@ import ( "database/sql" "database/sql/driver" "fmt" - "io" - "strings" - "github.com/arana-db/parser/ast" "github.com/arana-db/parser/test_driver" gxsort "github.com/dubbogo/gost/sort" + "io" + "seata.apache.org/seata-go/pkg/datasource/sql/util" + "strings" "seata.apache.org/seata-go/pkg/datasource/sql/types" ) @@ -276,43 +276,5 @@ func (b *BasicUndoLogBuilder) buildLockKey(rows driver.Rows, meta types.TableMet // the string as local key. the local key example(multi pk): "t_user:1_a,2_b" func (b *BasicUndoLogBuilder) buildLockKey2(records *types.RecordImage, meta types.TableMeta) string { - var lockKeys bytes.Buffer - lockKeys.WriteString(meta.TableName) - lockKeys.WriteString(":") - - keys := meta.GetPrimaryKeyOnlyName() - keyIndexMap := make(map[string]int, len(keys)) - - for idx, columnName := range keys { - keyIndexMap[columnName] = idx - } - - primaryKeyRows := make([][]interface{}, len(records.Rows)) - - for i, row := range records.Rows { - primaryKeyValues := make([]interface{}, len(keys)) - for _, column := range row.Columns { - if idx, exist := keyIndexMap[column.ColumnName]; exist { - primaryKeyValues[idx] = column.Value - } - } - primaryKeyRows[i] = primaryKeyValues - } - - for i, primaryKeyValues := range primaryKeyRows { - if i > 0 { - lockKeys.WriteString(",") - } - for j, pkVal := range primaryKeyValues { - if j > 0 { - lockKeys.WriteString("_") - } - if pkVal == nil { - continue - } - lockKeys.WriteString(fmt.Sprintf("%v", pkVal)) - } - } - - return lockKeys.String() + return util.BuildLockKey(records, meta) } diff --git a/pkg/datasource/sql/undo/builder/basic_undo_log_builder_test.go b/pkg/datasource/sql/undo/builder/basic_undo_log_builder_test.go index 465bf516..744f725f 100644 --- a/pkg/datasource/sql/undo/builder/basic_undo_log_builder_test.go +++ b/pkg/datasource/sql/undo/builder/basic_undo_log_builder_test.go @@ -69,6 +69,7 @@ func TestBuildLockKey(t *testing.T) { } columnsTwoPk := []types.ColumnMeta{columnID, columnUserId} + columnsThreePk := []types.ColumnMeta{columnID, columnUserId, columnAge} columnsMixPk := []types.ColumnMeta{columnName, columnAge} getColumnImage := func(columnName string, value interface{}) types.ColumnImage { @@ -98,6 +99,24 @@ func TestBuildLockKey(t *testing.T) { }, "test_name:1_one,2_two", }, + { + "Three Primary Keys", + types.TableMeta{ + TableName: "test2_name", + Indexs: map[string]types.IndexMeta{ + "PRIMARY_KEY": {IType: types.IndexTypePrimaryKey, Columns: columnsThreePk}, + }, + }, + types.RecordImage{ + TableName: "test2_name", + Rows: []types.RowImage{ + {[]types.ColumnImage{getColumnImage("id", 1), getColumnImage("userId", "one"), getColumnImage("age", "11")}}, + {[]types.ColumnImage{getColumnImage("id", 2), getColumnImage("userId", "two"), getColumnImage("age", "22")}}, + {[]types.ColumnImage{getColumnImage("id", 3), getColumnImage("userId", "three"), getColumnImage("age", "33")}}, + }, + }, + "test2_name:1_one_11,2_two_22,3_three_33", + }, { name: "Single Primary Key", metaData: types.TableMeta{ diff --git a/pkg/datasource/sql/util/lockkey.go b/pkg/datasource/sql/util/lockkey.go new file mode 100644 index 00000000..02992982 --- /dev/null +++ b/pkg/datasource/sql/util/lockkey.go @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package util + +import ( + "fmt" + "seata.apache.org/seata-go/pkg/datasource/sql/types" + "strings" +) + +func BuildLockKey(records *types.RecordImage, meta types.TableMeta) string { + var lockKeys strings.Builder + type ColMapItem struct { + pkIndex int + colIndex int + } + + lockKeys.WriteString(meta.TableName) + lockKeys.WriteString(":") + + keys := meta.GetPrimaryKeyOnlyName() + keyIndexMap := make(map[string]int, len(keys)) + for idx, columnName := range keys { + keyIndexMap[columnName] = idx + } + + columns := make([]ColMapItem, 0, len(keys)) + if len(records.Rows) > 0 { + for colIdx, column := range records.Rows[0].Columns { + if pkIdx, ok := keyIndexMap[column.ColumnName]; ok { + columns = append(columns, ColMapItem{pkIndex: pkIdx, colIndex: colIdx}) + } + } + for i, row := range records.Rows { + if i > 0 { + lockKeys.WriteString(",") + } + primaryKeyValues := make([]interface{}, len(keys)) + for _, mp := range columns { + if mp.colIndex < len(row.Columns) { + primaryKeyValues[mp.pkIndex] = row.Columns[mp.colIndex].Value + } + } + for j, pkVal := range primaryKeyValues { + if j > 0 { + lockKeys.WriteString("_") + } + if pkVal == nil { + continue + } + lockKeys.WriteString(fmt.Sprintf("%v", pkVal)) + } + } + } + return lockKeys.String() +} --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@seata.apache.org For additional commands, e-mail: notifications-h...@seata.apache.org