• 主页
  • 架构
  • 编程语言
  • 数据存储
  • 网络
  • VMware
  • 服务器
  • 组网
  • AI
  • 算法系列
  • 设计模式
  • 读书笔记
  • 思考
  • 工具
  • 其它技术

  • 主页
  • 架构
  • 编程语言
  • 数据存储
  • 网络
  • VMware
  • 服务器
  • 组网
  • AI
  • 算法系列
  • 设计模式
  • 读书笔记
  • 思考
  • 工具
  • 其它技术

MySQL事务的一些奇奇怪怪知识

2025-05-18

Gorm事务有error却不返回会发生什么

Gorm包是大家比较高频使用。正常的用法是,如果有失败返回error,整体rollback,如果不返回error则commit。下面是Transaction的源码:

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
// Transaction start a transaction as a block, return error will rollback, otherwise to commit. Transaction executes an
// arbitrary number of commands in fc within a transaction. On success the changes are committed; if an error occurs
// they are rolled back.
func (db *DB) Transaction(fc func(tx *DB) error, opts ...*sql.TxOptions) (err error) {
panicked := true

if committer, ok := db.Statement.ConnPool.(TxCommitter); ok && committer != nil {
// nested transaction
if !db.DisableNestedTransaction {
err = db.SavePoint(fmt.Sprintf("sp%p", fc)).Error
if err != nil {
return
}
defer func() {
// Make sure to rollback when panic, Block error or Commit error
if panicked || err != nil {
db.RollbackTo(fmt.Sprintf("sp%p", fc))
}
}()
}
err = fc(db.Session(&Session{NewDB: db.clone == 1}))
} else {
tx := db.Begin(opts...)
if tx.Error != nil {
return tx.Error
}

defer func() {
// Make sure to rollback when panic, Block error or Commit error
if panicked || err != nil {
tx.Rollback()
}
}()

if err = fc(tx); err == nil {
panicked = false
return tx.Commit().Error
}
}

panicked = false
return
}

神奇用法

但如果我瞎搞呢?如果有error我仍然返回nil,会发生什么?

1
2
3
4
5
6
7
8
9
10
11
12
err := db.Transaction(func(tx *gorm.DB) error {
var err error
isInserted, err = d.TxCreate(tx, record)
if err != nil {
return nil
}
err = d.TxUpdate(tx, opt)
if err != nil {
return errors.Wrap(err, "failed to update")
}
return nil
})

解释

这种情况下,本质看MySQL自身的设计。事务具有原子性,理论上应该要么全部成功,要么全部失败。但如果在一条语句失败后执行COMMIT,MySQL 会尽力提交已成功执行的语句。例如:

1
2
3
4
5
6
START TRANSACTION;
INSERT INTO test (id, value) VALUES (2, 'data3');
INSERT INTO test (id, value) VALUES (3, 'data4');
-- 故意让这条语句失败(假设id冲突)
INSERT INTO test (id, value) VALUES (2, 'data5');
COMMIT;

在上述例子中,前两条INSERT语句可能已经成功执行并修改了数据库,而第三条失败。执行COMMIT后,前两条语句的修改会被永久保存到数据库中,而失败的语句不会回滚整个事务。

其实十分不建议这么用。数据库状态可能会出现部分操作生效,部分未生效的情况,破坏了事务的原子性。这可能导致数据不一致问题,例如业务逻辑上要求某些数据必须同时存在或不存在,但由于这种部分提交的情况,无法保证这种一致性。

MySQL表数据时间的记录时刻

假设数据库是这样的

1
2
3
4
5
6
7
8
9
CREATE TABLE `conversation_new_message` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
`conversation_new_id` bigint unsigned NOT NULL COMMENT '自增id',
`message` json DEFAULT NULL COMMENT 'conversation history, a json map',
`turn` bigint NOT NULL COMMENT '轮数',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '上一次更新时间',
PRIMARY KEY (`id`),
) ENGINE=InnoDB

起一个事务,按照下面的时间点执行,最终的创建时间是多少?是按照insert时刻的时间来的(不同记录不一样),还是按照真正落库的时间来的(不同记录一样)?

image-20250518224254182

发现是按照真正写入的时间来的。其实也比较好理解,虽然我们没有直接赋值,但CURRENT_TIMESTAMP本身在记录创建的时候就已经设置好值了。

image-20250518224325433

扫一扫,分享到微信

微信分享二维码
go语法大赏
golang之ctx cancel
© 2025 John Doe
Hexo Theme Yilia by Litten