并发控制
并发情况下,需要做一些控制(加锁),保证共享数据的一致性。
并发操作数据库时,需要给数据库中的数据加锁,确保数据库中数据的一致性。
数据库锁的常见分类
- 按使用方式来分:悲观锁、乐观锁
- 按锁级别来分:共享锁、排它锁
- 按锁粒度来分:行级锁、表级锁、页级锁,页介于行、表之间
悲观锁 Pessimistic Lock
假设是最坏的情况,认为其它线程一定会修改当前线程使用的数据库数据,当前线程一定要给使用的数据库数据加锁。
悲观锁只是个统称,并不是指某一种具体的锁。悲观锁主要包括
- 共享锁(S锁,share),又称为读锁,所有线程都可以访问,但都只能读、不能写
- 排它锁(X锁),又称为写锁,是排它的,同一时刻只能有一个线程读写这部分数据
Java中的synchronized、ReentrantLock等独占锁就是悲观锁思想的实现。
悲观锁一般要借助数据库本身提供的锁机制来实现,以mysql的InnoDB引擎为例:加排它锁
begin; --开始事务
select * from tb_user where id=1 for update; --先给要使用的行加锁
update tb_user set username='chy',password='abcd' where id=1; --修改数据
commit; --提交事务
先用 select…for update 锁住要使用的行,再修改数据。
InnoDB默认使用行级锁,但行级锁是基于索引的,如果sql语句用不到索引,会使用表级锁将整张表锁住。
乐观锁 Optimistic Lock
假设是很好的情况,认为其它线程一般不会修改当前线程使用的数据库数据,使用时不加锁,只在提交更新时检查数据是否被其它线程修改过。
乐观锁常见的实现方式有2种:CAS、版本号。
CAS
Compare and Swap,提交时先和之前从数据库查到的数据比较,数据没有被其它线程修改才和数据库交换数据(提交修改)。
select goods_quantity from tb_goods where goods_id=1; //先查该种商品的库存,假设为100
update tb_goods set goods_quantity=goods_quantity-1 where goods_id=1 and goods_quantity=100; //提交修改时带上条件库存等于100,确保数据没有被修改
CAS方式可能会发生ABA问题:
之前查到库存为100(A),期间某个线程修改了数据,比如售出1件,库存改为99(B),之后又把修改了库存为原来的值100(A),比如买家取消订单。
用来标识的数据和原来的相同,但记录其实已被修改过了,修改的可能是其它字段。
版本号机制(推荐)
设计表时增加version列,每次更新记录时都将该条记录的version+1。
执行更新操作时先查询这要使用记录的version,提交更新时比较version前后是否一致,一致就说明数据未被其它线程修改;不一致说明数据已被其它线程修改,重试指定次数(重试需要自己写代码实现)。
--不加锁
select version from tb_goods where goods_id=1; --先查询这条记录的数据版本号,假设为5
update tb_goods set goods_quantity=goods_quantity-1,version=version+1 where goods_id=1 and version=5; --提交更新时检测版本号是否一致,如果一致,执行时还需要将版本号+1。写操作会返回受影响的记录数,根据返回值判断操作是否成功
不一定要用版本号,时间戳也行
乐观锁的优化写法
如果数据库操作只有一条sql语句,不涉及事务
update tb_goods set goods_quantity=goods_quantity-1 where goods_id=1 and goods_quantity-1>=0; --不用管其它线程是否修改了数据(库存),只要库存够就行
悲观锁、乐观锁的比较、选择
悲观锁是每次操作都要加锁,乐观锁实际上并没有加锁。
悲观锁是假设其它线程一定会修改当前线程使用的数据,对应高并发场景;乐观锁是假设其它线程一般不会修改当前线程使用的数据,对应低并发场景。但实际使用时,高并发一般使用乐观锁。
悲观锁、悲观锁都保证了数据一致性,乐观锁的版本号如果对不上,也不会提交修改。
高并发时,悲观锁每次操作都要加锁,更新成功概率大,但严重拉低性能,频繁使用锁还可能发生死锁;乐观锁虽然更新成功概率低一些,但每次都不加锁,性能高、能扛住高并发。面对高并发,首先要能扛住,如果扛都扛不住,很多请求都是拒绝连接,谈何性能。
并发量小的项目,悲观锁、乐观锁的更新成功率都高,但悲观锁加了锁,更新成功率更高,优先使用悲观锁;并发量大的项目,首先保证扛得住,优先使用乐观锁。