MySQL之悲观锁和乐观锁

/ MySQL / 0 条评论 / 324人围观

悲观锁(Pessimistic Lock)和乐观锁(Optimistic Lock)是数据库系统中并发控制主要采用的技术手段。针对不同的业务场景,应该选用不同的并发控制方式。不要把它们和数据库中提供的锁机制(行锁、表锁、排他锁、共享锁)混为一谈。

Pessimistic Lock

概述

悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。

Java synchronized 就属于悲观锁的一种实现,每次线程要修改数据时都先获得锁,保证同一时刻只有一个线程能操作数据,其他线程则会被block。

特性

Optimistic Lock

乐观锁,又称乐观并发控制(Optimistic Concurrency Control),乐观地认为不会发生并发问题,只在提交更新操作时检查是否违反数据的一致性。

概述

乐观锁在数据库中的实现完全是逻辑性的,不需要数据库提供特殊的支持。一般的做法是在数据表中增加一个字段(版本号或者时间戳),作为数据的版本标识。读取数据时,将版本号一同读出;之后更新数据时,加入版本号条件,更新成功就将版本号加1。乐观锁的重点在于,更新数据时,加入版本号匹配条件,将数据的版本与数据表中对应记录的当前版本进行匹配更新,如果数据的版本号等于数据表的当前版本号,则获取锁成功,也就是更新成功;否则,更新失败,需要回滚整个业务操作。Java中的atomic包就是乐观锁的一种实现,AtomicInteger 通过CAS(Compare And Set)操作实现线程安全的自增。

实现机制

在数据库中,update同一行的情况是不允许并发的,即数据库每次执行一条update语句时会获取被update行的写锁,直到这一行被成功更新后才释放。因此在业务操作进行前获取需要锁的数据的当前版本号,然后实际更新数据时,以版本号作为条件,再次对比版本号确认与之前获取的相同,并更新版本号,即可确认没有发生并发的修改。如果更新失败即可认为老版本的数据已经被并发修改掉了,此时认为获取锁失败,需要回滚整个业务操作并可根据需要重试整个过程。

特性

示例

悲观锁

用数据库来演示悲观锁,首先悲观锁是必须用到数据库的事务机制。

  1. 开启事务

BEGIN

  1. 执行for update操作。

select * from t_logs where ip = '58.135.82.57' for UPDATE;

  1. 不要执行commit操作,为了模仿并发操作。

在Navicat中开启另一个会话窗口

  1. 开启事务

BEGIN

  1. 执行update操作

update t_logs SET action = '初始化1' WHERE id = '2';

  1. 如果不执行上一个会话的commit操作,会发现此会话一直处于等待状态。
  2. 执行上一个会话的commit操作,提交数据。
  3. 执行此会话的commit操作。

乐观锁

商品的库存量是固定的,保证商品数量不超卖, 需要保证数据一致性:用乐观锁来保证某个人点击秒杀后系统中查出来的库存量和实际扣减库存时库存量是一致的。

  1. 商品表
CREATE TABLE `tb_product_stock` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `product_id` bigint(32) NOT NULL COMMENT '商品ID',
  `number` INT(8) NOT NULL DEFAULT 0 COMMENT '库存数量',
  `create_time` DATETIME NOT NULL COMMENT '创建时间',
  `modify_time` DATETIME NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `index_pid` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品库存表';
  1. POJO类
class ProductStock {
    private Long productId; //商品id
    private Integer number; //库存量

    public Long getProductId() {
        return productId;
    }

    public void setProductId(Long productId) {
        this.productId = productId;
    }

    public Integer getNumber() {
        return number;
    }

    public void setNumber(Integer number) {
        this.number = number;
    }
}
  1. 锁实现
public boolean updateStock(Long productId){
        int updateCnt = 0;
        while (updateCnt == 0) {
            ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId}", productId);
            if (product.getNumber() > 0) {
                // 确保库存不会减为负数
                updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId} AND number>=1", productId);
                if(updateCnt > 0){    //更新库存成功
                    return true;
                }
            } else {    //卖完啦
                return false;
            }
        }
        return false;
    }

UPDATE 语句的WHERE 条件字句上需要建索引,尽量不要使用全表扫描。