页锁就是在页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。
页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。
每个层级的锁数量是有限制的,因为锁会占用内存空间,锁空间的大小是有限的。当某个层级的锁数量超过了这个层级的阈值时,就会进行锁升级。锁升级就是用更大粒度的锁替代多个更小粒度的锁,
比如InnoDB中行锁升级为表锁,这样做的好处是占用的锁空间降低了,但同时数据的并发度也下降了。
悲观锁是一种思想,顾名思义,就是很悲观,对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。
悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 阻塞 直到它拿到锁(共享资源每次只给一个线程使用,
其它线程阻塞,用完后再把资源转让给其它线程)。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当其他线程想要访问数据时,都需要阻塞挂起。Java中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现
# 例如在商品秒杀的情景中,避免出现超卖的情况
# 商品信息从查询、生成订单、修改的这个过程,查询到数据后,获取锁,知道修改数据后才释放锁
# 需要将执行的业务放到1个事务中
# 1.查询数据
select quantity from items where id = 1001 for update;
# 2. 生成订单
insert into orders(item_id) values(1001);
# 3. 修改库存
update items set quantity = quantity-num where id = 1001;
# 在使用悲观锁时,必须对要查询的对象使用索引
# 悲观锁对于比较多的业务,开销较大
乐观锁认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,也就是不采用数据库自身的锁机制,
而是通过程序来实现。在程序上,我们可以采用 版本号机制 或者 CAS机制 实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在Java中 java.util.concurrent.atomic 包下的
原子变量类就是使用了乐观锁的一种实现方式:CAS实现的
# 实现方式1:版本号机制
# 在表中添加1个字段version,第1个事务第1次读的时候,会拿到这个版本号,最后修改库存的时候,判断版本号是否一致,一致才能修改成功
# 1.查询数据
select quantity from items where id = 1001 for update;
# 2. 生成订单
insert into orders(item_id) values(1001);
# 3. 修改库存
update items set quantity = quantity-num where id = 1001 and version = 1;
# 实现方式2:时间戳机制
# 第1个事务第1次读的时候,会拿到这个时间戳,最后修改库存的时候,判断时间戳是否一致,一致才能修改成功
# 1.查询数据
select quantity from items where id = 1001 for update;
# 2. 生成订单
insert into orders(item_id) values(1001);
# 3. 修改库存
update items set quantity = quantity-num where id = 1001 and updated = 123456;
# 如果数据库是读写分离的表,当主表写入的数据,没有及时同步到从表时,需强制去主表中读数据
# 在使用版本号机制的时候,多个事务第1次读的时候,版本号都是1,当其中1个事务修改后,其他事务则都会修改失败
# 由于每次只有1个事务能够成功,会导致业务感知上有大量的失败操作
# 为了避免以上的现象,可以在最后修改库存时,判断库存不为0则能成功
# 1.查询数据
select quantity from items where id = 1001 for update;
# 2. 生成订单
insert into orders(item_id) values(1001);
# 3. 修改库存
update items set quantity = quantity-num where id = 1001 and quantity-num > 0;
乐观锁适合读操作比较多的场景
悲观锁适合写操作比较多的场景