前言
在说乐观锁和悲观锁之前,先说一下什么是锁,为什么要用到锁?
在实际生活中,有很多地方用到了锁,例如:家门上的指纹锁、保险柜上的密码锁和登录微信时的账号密码等等。这些锁保证了我们的人身安全、财富安全和个人信息安全。
而程序中的锁则是保证了数据安全。当有多个线程去访问共享数据的时候,我们可以给数据加上锁,保证了数据的(syncronized)。当多个用户去修改用一个数据的时候,给数据加上锁,可以处理一部分并发问题,保证数据被修改后呈现的效果跟一个用户修改数据后呈现的效果一致。
上图所示,有两个用户同时访问数据库更新数据,数据最终的数据可能是3也可能是5,导致数据的不准确。
乐观锁
乐观锁,就像是生活中很乐观的人。举个例子:有一个公共厕所,乐观的人去上厕所,会认为厕所有人,别人就不会进来,所以就不锁门直接上,因为锁门开门很浪费时间。在代码中,乐观锁在读数据的数据的时候是没有给数据上锁的,所有用户可以随便的去读数据。但是在修改数据的时候就需要去看看有没有人正在修改,或者已经是修改后了的数据。
乐观锁可以利用版本号机制和CAS算法实现。
版本号机制
版本号机制是给表加上一个version字段
具体的实现流程:
- 用户A和用户B同时去查询水杯的剩余数量,查询到数据
inventory_quantity = 1
和version = 1
。 - 用户A带着
version = 1
的条件更新,水杯的库存数量-1并将版本号+1,更新成功,接口返回正确的信息。 - 用户B带着
version = 1
的条件更新,更新完后发现受影响行数为0,更新失败,接口返回错误或者其他的信息。
CAS
说到乐观锁,就必须提到CAS机制。什么是CAS机制?
CAS全称,Compare-And-Set,比较并设置。
比较:比较获取到一个值A,再把它更新成B的同时判断该值是否还为A。
设置:如果是,更新为B,如果不是,回滚重试。
这两步对CPU来说是瞬间完成的。有了CAS就可以实现乐观锁,因为没有就加锁的步骤,所以可以多个线程同时读取,并且只有一个线程可以访问成功。因为乐观锁没有“加锁”和“解锁”的过程,所以也被称为“无锁编程”。
但CAS有一个新的问题,就是ABA问题。
ABA
什么是ABA问题。
一个线程在获取到某个变量的时候值为A,但无法确定这个变量是否被别的线程修改过。其他的线程可以先把值改为B,在做完操作后又把值改为A。
解决办法就是,给表加一个version
字段,操作完数据之后把version字段+1,当然线程在查数据的时候也需要查version
字段,并且在做更新的时候判断version
字段是否为1
悲观锁
悲观锁,就像是生活中很悲观的人。举个例子:假设厕所只有一个坑位,悲观的人在上厕所的时候会把门锁住,这样别人就进不来了。在代码中,给一个共享数据加上悲观锁,那么当一个线程访问到了数据后,会假设别的线程也会访问这个共享数据,就会给数据上锁,导致别的线程访问不到共享数据造成了阻塞。
悲观锁的实现
表结构如上,在不加锁的情况下,多名用户同时去购买会报错。悲观锁的实现是依靠数据库的锁机制:
- 当一个用户对水杯商品下单时,会先给该数据加上悲观锁。
- 加锁成功:此时水杯商品只能由加锁成功的用户操作,其他的用户再对水杯进行抢购的时候,发现数据被加上了悲观锁,只能等待开锁后再进行后续的操作。
-
加锁失败:说明该数据已经被其他用户加上了悲观锁,继续等待或者抛异常(根据实际情况定)。
SQL语句这样写,先关闭mysql的自动提交属性,执行sqlset autocommit=0
,然后再执行悲观锁加锁语句:select * from table where id = 1 for update
。可以用两个窗口来进行测试。
两种锁的使用场景
乐观锁回滚重试,悲观锁阻塞事务,没有孰好孰坏,两种锁的实现场景不一样。
乐观锁适用于写少读多的场景,乐观锁本身没有加锁和解锁的操作,省去了锁的开销,从而提高了吞吐量。
注意:乐观锁因为并未加锁,所以性能会很快,但是一旦锁的力度掌握不好,会导致更新失败,容易发生业务问题。
悲观锁适用于写多的场景,如果线程竞争比较激烈,冲突比较严重,用乐观锁反而增加了线程重试的次数从而影响了性能。
注意:悲观锁实现了“加锁”的操作,但是依赖于数据库,性能方面不如乐观锁。
点赞头上不掉发