insert or update 在使用事务和不使用事务两种情况下的差异

发布时间 2023-09-06 23:32:27作者: 若-飞

insert or update使用事务和不使用事务的核心差异

这样一个语句,在go多协程情况下,采用事务和不采用事务出现的问题:

INSERT INTO web3_data (space_id, user_address, attr_name, attr_value) 
VALUES (198, '0x56c9F75D92948a7BdeB8677b185111EeC3Fddc63', 'chain_interaction_count', 1) 
ON DUPLICATE KEY UPDATE attr_value = attr_value + 1

使用事务(Transaction)情况下:

会报死锁:Deadlock found when trying to get lock; try restarting transaction

不使用事务(Transaction)情况下:

不会报死锁,采用mysql自带的行锁,最多锁住几百毫秒到1秒,但是不会死锁。

=================================================================================

下面详细说明下情况。 

当在数据库操作中考虑并发性和数据一致性时,使用事务和不使用事务之间存在一些核心差异。其中,一个重要的差异是在高并发环境下可能导致死锁的发生。

使用事务和不使用事务的核心差异

使用事务(Transaction)

事务是将一系列数据库操作视为一个单一工作单元的机制。使用事务可以确保数据的一致性和隔离性。在并发环境中,多个事务同时操作相同的数据可能导致死锁。

事务的特点:

  • 原子性(Atomicity):事务中的所有操作要么全部成功提交,要么全部回滚。事务中的任何操作失败都会导致整个事务回滚,保持数据的一致性。
  • 一致性(Consistency):事务开始之前和结束之后,数据库的状态应该保持一致。如果事务在执行过程中发生错误,数据库将回滚到事务开始之前的状态。
  • 隔离性(Isolation):事务的执行应该相互隔离,互不干扰。每个事务应该感知不到其他事务的存在,以避免数据的冲突和不一致。
  • 持久性(Durability):一旦事务提交成功,其所做的更改应该永久保存在数据库中,即使发生系统崩溃或重启。

以下是使用事务执行 SQL 语句的示例代码(使用 Go 语言和 MySQL 数据库):

go
import (
	"database/sql"
	"log"
)

func incrementInteractionCountWithTransaction(db *sql.DB) error {
	tx, err := db.Begin()
	if err != nil {
		return err
	}

	// 在事务中执行插入或更新操作
	_, err = tx.Exec(`
		INSERT INTO web3_data (space_id, user_address, attr_name, attr_value)
		VALUES (198, '0x56c9F75D92948a7BdeB8677b185111EeC3Fddc63', 'chain_interaction_count', 1)
		ON DUPLICATE KEY UPDATE attr_value = attr_value + 1
	`)
	if err != nil {
		tx.Rollback()
		return err
	}

	err = tx.Commit()
	if err != nil {
		return err
	}

	return nil
}

func main() {
	// 初始化数据库连接
	db, err := sql.Open("mysql", "username:password@tcp(hostname:port)/database")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	err = incrementInteractionCountWithTransaction(db)
	if err != nil {
		log.Fatal(err)
	}
}

不使用事务(Non-transaction)

在不使用事务的情况下,每个数据库操作都是独立的,不受其他操作的影响。这意味着每个操作都会立即提交,不会等待其他操作的完成。

以下是不使用事务执行 SQL 语句的示例代码(使用 Go 语言和 MySQL 数据库):

go
import (
	"database/sql"
	"log"
)

func incrementInteractionCountWithoutTransaction(db *sql.DB) error {
	// 直接执行插入或更新操作,不使用事务
	_, err := db.Exec(`
		INSERT INTO web3_data (space_id, user_address, attr_name, attr_value)
		VALUES (198, '0x56c9F75D92948a7BdeB8677b185111EeC3Fddc63', 'chain_interaction_count', 1)
		ON DUPLICATE KEY UPDATE attr_value = attr_value + 1
	`)
	if err != nil {
		return err
	}

	return nil
}

func main() {
	// 初始化数据库连接
	db, err := sql.Open("mysql", "username:password@tcp(hostname:port)/database")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	err = incrementInteractionCountWithoutTransaction(db)
	if err != nil {
		log.Fatal(err)
	}
}

在使用事务的代码示例中,通过调用 db.Begin() 开始了一个事务。然后,在事务中执行插入或更新操作,并在出现错误时回滚事务(tx.Rollback())。最后,通过调用 tx.Commit() 提交事务。这样可以确保在插入或更新操作期间出现错误时,事务会回滚到初始状态,保持数据的一致性。

而在不使用事务的代码示例中,直接执行了插入或更新操作,没有显式地使用事务。每个操作都是独立的,立即提交,不会等待其他操作的完成。

死锁问题

死锁是在并发环境中可能发生的一种情况,其中多个事务相互等待对方释放资源,导致它们无法继续执行。在使用事务的情况下,多个事务同时操作相同的数据,由于事务之间的竞争,可能会导致死锁的发生。

以下是一个可能导致死锁的示例:

事务 A:

BEGIN;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
UPDATE table2 SET value = value + 1 WHERE id = 2;
COMMIT;

事务 B:

BEGIN;
SELECT * FROM table2 WHERE id = 2 FOR UPDATE;
UPDATE table1 SET value = value + 1 WHERE id = 1;
COMMIT;

在上述示例中,事务 A 首先获取了 table1 的行级锁,然后尝试获取 table2 的行级锁。同时,事务 B 首先获取了 table2 的行级锁,然后尝试获取 table1 的行级锁。由于两个事务都在等待对方释放资源,它们陷入了死锁状态。

在不使用事务的情况下,并发操作是独立的,每个操作都是立即提交的,因此不会发生死锁。

结论

使用事务和不使用事务在数据库操作中有着重要的差异。事务的使用可以确保数据的一致性和隔离性,但在高并发环境下,多个事务同时操作相同的数据可能导致死锁的发生。不使用事务的情况下,每个操作是独立的,不受其他操作的影响,但可能导致数据的不一致。

选择使用事务还是不使用事务取决于应用程序对数据一致性和并发性的要求。如果对数据的完整性和一致性非常重要,建议使用事务来确保操作的原子性和隔离性。然而,要注意在高并发环境下使用事务可能导致死锁的问题,需要进行适当的并发控制和资源管理。

希望这篇博客能够帮助你理解使用事务和不使用事务的核心差异,并突出了使用事务可能导致死锁的问题。如果你有任何其他