悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
悲观锁和乐观锁的区别
悲观锁:一段执行逻辑加上悲观锁,不同线程同时执行时,只能有一个线程执行,其他的线程在入口处等待,直到锁被释放。
乐观锁:一段执行逻辑加上乐观锁,不同线程同时执行时,可以同时进入执行,在最后更新数据的时候要检查这些数据是否被其他线程修改了(版本和执行初是否相同),没有修改则进行更新,否则放弃本次操作。
MySQL InnoDB中使用悲观锁
要使用悲观锁,我们必须关闭MySQL数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。 set autocommit=0;
Mysql乐观锁
- 实现方式: 通过给表字段增加version或者updated_at,每次更新数据后给version+1或者修改updated_at为当前时间, 在更新前去校验当前version值或updated_at是否与数据库中的最新值一致
session1:
MariaDB [blog]> select * from articles;
+----+-------+---------+---------------------+---------------------+
| id | title | content | created_at | updated_at |
+----+-------+---------+---------------------+---------------------+
| 2 | zz | zzz | 0000-00-00 00:00:00 | 2018-05-06 16:59:03 |
| 9 | as | NULL | 0000-00-00 00:00:00 | 0000-00-00 00:00:00 |
+----+-------+---------+---------------------+---------------------+
2 rows in set (0.00 sec)
-----------------------------------------------------------------------------
session2:
MariaDB [blog]> update articles set title = "cc", updated_at = NOW() where id = 2;
Query OK, 1 row affected (17.17 sec)
Rows matched: 1 Changed: 1 Warnings: 0
-----------------------------------------------------------------------------
session1:
MariaDB [blog]> update articles set title = "xx" where id = 2 and
updated_at = "2018-05-06 16:59:03";
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0
Mysql悲观锁
- 分为共享锁和排它锁
- 共享锁:也叫读锁,简称S锁,原理:一个事务获取了一个数据行的共享锁,其他事务能获得该行对应的共享锁,但不能获得排他锁,即一个事务在读取一个数据行的时候,其他事务也可以读,但不能对该数据行进行删除和更新。
session1:
MariaDB [blog]> begin;
Query OK, 0 rows affected (0.00 sec)
MariaDB [blog]> select * from articles where id = 2 lock in share mode;
+----+-------+---------+---------------------+---------------------+
| id | title | content | created_at | updated_at |
+----+-------+---------+---------------------+---------------------+
| 2 | cc | zzz | 0000-00-00 00:00:00 | 2018-05-06 17:00:28 |
+----+-------+---------+---------------------+---------------------+
1 row in set (0.00 sec)
-----------------------------------------------------------------------------
session2:
MariaDB [blog]> select * from articles where id = 2;
+----+-------+---------+---------------------+---------------------+
| id | title | content | created_at | updated_at |
+----+-------+---------+---------------------+---------------------+
| 2 | cc | zzz | 0000-00-00 00:00:00 | 2018-05-06 17:00:28 |
+----+-------+---------+---------------------+---------------------+
1 row in set (0.00 sec)
MariaDB [blog]> update articles set title = "zz" where id = 2;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
MariaDB [blog]> insert into articles(title) values("ss");
Query OK, 1 row affected, 2 warnings (0.00 sec)
MariaDB [blog]> delete from articles where id = 2;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
- 排它锁:也叫写锁,简称x锁,原理:一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的任何锁(排他锁或者共享锁),即一个事务在读取一个数据行的时候,其他事务不能对该数据行进行删除修改和加锁查询。对于update,insert,delete语句会自动加排它锁。
session1:
MariaDB [blog]> begin;
Query OK, 0 rows affected (0.00 sec)
MariaDB [blog]> select * from articles where id = 2 for update;
+----+-------+---------+---------------------+---------------------+
| id | title | content | created_at | updated_at |
+----+-------+---------+---------------------+---------------------+
| 2 | cc | zzz | 0000-00-00 00:00:00 | 2018-05-06 17:00:28 |
+----+-------+---------+---------------------+---------------------+
1 row in set (0.00 sec)
-----------------------------------------------------------------------------
session2:
MariaDB [blog]> begin;
Query OK, 0 rows affected (0.00 sec)
MariaDB [blog]> select * from articles where id = 2;
+----+-------+---------+---------------------+---------------------+
| id | title | content | created_at | updated_at |
+----+-------+---------+---------------------+---------------------+
| 2 | cc | zzz | 0000-00-00 00:00:00 | 2018-05-06 17:00:28 |
+----+-------+---------+---------------------+---------------------+
1 row in set (0.00 sec)
MariaDB [blog]> select * from articles where id = 2 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
MariaDB [blog]> delete from articles where id = 2;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
MariaDB [blog]> select * from articles where id = 2 lock in share mode;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
MySQL InnoDB默认行级锁。行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点需要注意。
AtomicInteger
AtomicInteger中的incrementAndGet方法就是乐观锁的一个实现,使用自旋(循环检测更新)的方式来更新内存中的值并通过底层CPU执行来保证是更新操作是原子操作。方法如下:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
//compareAndSwapInt(obj, offset, expect, update)
return var5;
}
首先这个方法通过getIntVolatile方法,使用对象的引用与值的偏移量得到当前值,然后调用compareAndSwapInt检测如果obj内的value和expect相等,就证明没有其他线程改变过这个变量,那么就更新它为update,如果这一步的CAS没有成功,那就采用自旋的方式继续进行CAS操作。
乐观锁的问题
1.ABA问题
如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。这就是CAS的ABA问题。
常见的解决思路是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
2.循环时间长开销大问题
上面我们说过如果CAS不成功,则会原地自旋,如果长时间自旋会给CPU带来非常大的执行开销。