MySQL基础笔记(17)-悲观锁与乐观锁

一.什么是乐观锁、悲观锁

  1. 乐观锁指的是在对数据进行读取时,默认认为此时没有线程去修改数据(在Java中Atomic原子类就是这么设计的,例如CAS),只是在提交时才去判断是否符合预期从而确定是否需要自旋并再次读取。适用于读多写少的场合。
  2. 悲观锁指的是在对数据进行读取时认为一定有其他线程修改数据,所以直接加上了互斥锁,直到自身对数据的处理完成后才释放锁(Java中的synchronized就是悲观锁),适用于写多读少的场合

二.MySQL中的乐观锁与悲观锁

  1. DDL
CREATE TABLE goods (
  id int(20) NOT NULL AUTO_INCREMENT,
  name varchar(100) DEFAULT NULL,
  stock int(20) DEFAULT NULL,
  PRIMARY KEY (id),
  UNIQUE KEY idx_name (name) USING BTREE
) ENGINE=InnoDB 
  1. DML
INSERT INTO goods VALUES (1, "MacBookPro2015", 1000);
INSERT INTO goods VALUES (2, "MacBookPro2016", 1000);
INSERT INTO goods VALUES (3, "MacBookPro2017", 1000);
INSERT INTO goods VALUES (4, "MacBookPro2018", 1000);
INSERT INTO goods VALUES (5, "MacBookPro2019", 1000);

1.悲观锁

1.互斥锁-for update

首先要说的是互斥锁,类似于Java中的synchronize关键字,使用方式为

select ... for update

其中,查询到的行就是被互斥锁锁住的行,只有当上锁的线程提交了事务之后,互斥锁才会解除,其他线程在此之前都不可以通过for updatelock in share mode去获取到该锁,严格保证了并发下的数据安全,适用于对性能安全有很大要求的情况,但是会大大减少并发量。所以,互斥锁 又被成为写锁。

  1. 线程1
mysql> begin; // 0.开启事务
mysql> select * from goods where id = 1 for update; // 1.使用互斥锁
+----+----------------+-------+
| id | name           | stock |
+----+----------------+-------+
|  1 | MacBookPro2015 |  1000 |
+----+----------------+-------+
1 row in set (0.00 sec)

mysql> update goods set stock = stock - 1 where id = 1; // 2.减少库存
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit; // 4.提交事务
Query OK, 0 rows affected (0.01 sec)
  1. 线程2
mysql> select * from goods where id = 1; // 3.不获取互斥锁进行查询,查询出来的结果是错的
+----+----------------+-------+
| id | name           | stock |
+----+----------------+-------+
|  1 | MacBookPro2015 |  1000 |
+----+----------------+-------+
1 row in set (0.00 sec)

mysql> select * from goods where id = 1 for update; // 3.使用互斥锁查询,获取不到互斥锁,阻塞
^C^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted
mysql> select * from goods where id = 1 lock in share mode;// 3.使用共享锁查询,获取不到共享锁,阻塞
^C^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted
mysql> select * from goods where id = 1 for update; // 5.在线程1提交事务释放互斥锁后可使用互斥锁查询
+----+----------------+-------+
| id | name           | stock |
+----+----------------+-------+
|  1 | MacBookPro2015 |   999 |
+----+----------------+-------+

其他情况如update、delete等都是一样的,在使用了互斥锁后,只有等其释放才可以获取到并执行

2.共享锁-lock in share mode

使用方式:

select...lock in share mode

共享锁也属于悲观锁的一种,但是它和互斥锁不同之处在于:在上锁的行未被修改之前,其他线程也可以使用获取该共享锁并进行数据的读取,但不可修改;在上锁的行被修改之后,其他线程无法获取该共享锁读取数据,共享锁升级为互斥锁。所以,共享锁又被称为读锁。

  1. 线程1
mysql> begin;    // 0.开启事务
Query OK, 0 rows affected (0.00 sec)

mysql> select * from goods where id = 1 lock in share mode; // 1.获取共享锁
+----+----------------+-------+
| id | name           | stock |
+----+----------------+-------+
|  1 | MacBookPro2015 |   999 |
+----+----------------+-------+
1 row in set (0.00 sec)

mysql> update goods set stock = stock - 1 where id = 1;  // 3.更新库存
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;
Query OK, 0 rows affected (0.00 sec) // 5.提交事务
  1. 线程2
mysql> select * from goods where id = 1; // 2. 普通查询,不加锁,可以查询
+----+----------------+-------+
| id | name           | stock |
+----+----------------+-------+
|  1 | MacBookPro2015 |   999 |
+----+----------------+-------+
1 row in set (0.00 sec)

mysql> select * from goods where id = 1 for update; // 2.无法获取互斥锁
^C^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted
mysql> select * from goods where id = 1 lock in share mode; // 2. 可以获取共享锁进行查询
+----+----------------+-------+
| id | name           | stock |
+----+----------------+-------+
|  1 | MacBookPro2015 |   999 |
+----+----------------+-------+
1 row in set (0.00 sec)

mysql> update goods set stock = stock - 1 where id = 2; // 2.无法获取共享锁进行修改
^C^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted

mysql> select * from goods where id = 1 lock in share mode; // 4.无法获取共享锁,无法查询,因为线程1已经修改了库存,此时升级为互斥锁
^C^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted
mysql> select * from goods where id = 1 lock in share mode; // 6.线程1释放共享锁,线程2获取共享锁
+----+----------------+-------+
| id | name           | stock |
+----+----------------+-------+
|  1 | MacBookPro2015 |   998 |
+----+----------------+-------+
1 row in set (2.30 sec)
3.update、insert、delete自动加行锁

上面两种情况是在SQL语句中手动上锁的情况,在Innodb引擎中,updateinsertdelete这三个语句是默认上互斥锁的,如果使用update...for updateupdate...lock in share mode则会报错:

mysql> update goods set stock = stock - 1 for update;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'for update' at line 1
mysql> update goods set stock = stock - 1 lock in share mode;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'lock in share mode' at line 1

简单地测试一下:

  1. 线程1
mysql> begin;
Query OK, 0 rows affected (0.01 sec)

mysql> update goods set stock = stock - 1 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
  1. 线程2
mysql> select * from goods where id = 1 for update;
^C^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted
mysql> select * from goods where id = 1 lock in share mode;
^C^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted

2.乐观锁-版本号控制

要在MySQL应用乐观锁,一般是通过在行中添加版本号字段来实现,当我们进行操作时,首先查询出该数据的版本号,当我们完成了数据的修改后,更新时再确定一遍版本号即可避免版本不一致问题。成功修改的数据要将版本号自增。若版本号与原先不同则说明被修改,那么重复完成上述操作即可,(在InnoDB中也使用了这个机制来解决RR情况下的幻读问题,被称为MVCC多版本并发控制)。以下使用Golang来简单实现乐观锁逻辑。

mysql> alter table goods add version int(20)  not null default 0;//添加版本号字段
type Goods struct {
	Id int `gorm:"column:id"`
	Name string `gorm:"column:name"`
	Stock int `gorm:"column:stock"`
	Version int `gorm:"column:version"`
}

func main() {
	db := getDb()
	var good Goods
	db.Raw("select * from goods where id = 1").Scan(&good)
	fmt.Println(good)
	for good.Id != 0 && good.Stock > 0 {
		// 获取到版本号
		version := good.Version
		// 减库存并增加版本号,以当前版本号为条件之一
		exec := db.Exec("update goods set stock = stock - 1, version = ? where id = 1 and version = ?", version+1, version)
		if exec.RowsAffected != 0 {
			fmt.Println("更新成功")
			break
		}
		fmt.Println("更新失败,重试")
	}
	db.Raw("select * from goods where id = 1").Scan(&good)
	fmt.Println(good)
}

结果:

{1 MacBookPro2015 997 3}
更新成功
{1 MacBookPro2015 996 4}
发布了309 篇原创文章 · 获赞 205 · 访问量 30万+

猜你喜欢

转载自blog.csdn.net/pbrlovejava/article/details/103862808