经典问题之乐观锁和悲观锁及使用场景

 

悲观锁

悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。html

悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操做。git

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

乐观锁

乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于读多写少的应用场景,这样能够提升吞吐量。redis

乐观锁:假设不会发生并发冲突,只在提交操做时检查是否违反数据完整性。数据库

乐观锁通常来讲有如下2种方式: 
1. 使用数据版本(Version)记录机制实现,这是乐观锁最经常使用的一种实现方式。何谓数据版本?即为数据增长一个版本标识,通常是经过为数据库表增长一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当咱们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,若是数据库表当前版本号与第一次取出来的version值相等,则予以更新,不然认为是过时数据。 
2. 使用时间戳(timestamp)。乐观锁定的第二种实现方式和第一种差很少,一样是在须要乐观锁控制的table中增长一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version相似,也是在更新提交的时候检查当前数据库中数据的时间戳和本身更新前取到的时间戳进行对比,若是一致则OK,不然就是版本冲突。apache

Java JUC中的atomic包就是乐观锁的一种实现,AtomicInteger 经过CAS(Compare And Set)操做实现线程安全的自增。安全

MySQL隐式和显示锁定

MySQL InnoDB采用的是两阶段锁定协议(two-phase locking protocol)。多线程

在事务执行过程当中,随时均可以执行锁定,锁只有在执行 COMMIT或者ROLLBACK的时候才会释放,而且全部的锁是在同一时刻被释放。并发

前面描述的锁定都是隐式锁定,InnoDB会根据事务隔离级别在须要的时候自动加锁。分布式

 

另外,InnoDB也支持经过特定的语句进行显示锁定,这些语句不属于SQL规范: 
* SELECT … LOCK IN SHARE MODE 
* SELECT … FOR UPDATE

实战

接下来,咱们经过一个具体案例来进行分析:考虑电商系统中的下单流程,商品的库存量是固定的,如何保证商品数量不超卖? 其实须要保证数据一致性:某我的点击秒杀后系统中查出来的库存量和实际扣减库存时库存量的一致性就能够。

假设,MySQL数据库中商品库存表tb_product_stock 结构定义以下:

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='商品库存表';

对应的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;
    }
}

不考虑并发的状况下,更新库存代码以下:

/**
     * 更新库存(不考虑并发)
     * @param productId
     * @return
     */
    public boolean updateStockRaw(Long productId){
        ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId}", productId);
        if (product.getNumber() > 0) {
            int updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId} AND number=#{number}", productId, product.getNumber());
            if(updateCnt > 0){    //更新库存成功
                return true;
            }
        }
        return false;
    }

多线程并发状况下,会存在超卖的可能。

悲观锁

/**
     * 更新库存(使用悲观锁)
     * @param productId
     * @return
     */
    public boolean updateStock(Long productId){
        //先锁定商品库存记录
        ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId} FOR UPDATE", productId);
        if (product.getNumber() > 0) {
            int updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId}", productId);
            if(updateCnt > 0){    //更新库存成功
                return true;
            }
        }
        return false;
    }

乐观锁

/**
     * 下单减库存
     * @param productId
     * @return
     */
    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=#{number}", productId, product.getNumber());
                if(updateCnt > 0){    //更新库存成功
                    return true;
                }
            } else {    //卖完啦
                return false;
            }
        }
        return false;
    }

使用乐观锁更新库存的时候不加锁,当提交更新时须要判断数据是否已经被修改(AND number=#{number}),只有在 number等于上一次查询到的number时 才提交更新。

乐观锁与悲观锁的区别

乐观锁的思路通常是表中增长版本字段,更新时where语句中增长版本的判断,算是一种CAS(Compare And Swep)操做,商品库存场景中number起到了版本控制(至关于version)的做用( AND number=#{number})

悲观锁之因此是悲观,在于他认为本次操做会发生并发冲突,因此一开始就对商品加上锁(SELECT … FOR UPDATE),而后就能够安心的作判断和更新,由于这时候不会有别人更新这条商品库存。

什么场景须要使用锁,什么场景不须要使用锁?

从中咱们也能够知道只要更新数据是依赖读取的数据做为基础条件的,就会有并发更新问题须要乐观锁或者悲观锁取解决,特别实在计数表现明显。

好比在更新数据不依赖查询的数据的就不会有问题,例如修改用户的名称,多人同时修改,结果并不依赖于以前的用户名字,这就不会有并发更新问题。

小结

这里咱们经过 MySQL 乐观锁与悲观锁 解决并发更新库存的问题,固然还有其它解决方案,例如使用 分布式锁。目前常见分布式锁实现有两种:基于Redis和基于Zookeeper,基于这两种 业界也有开源的解决方案,例如 Redisson Distributed locks 、 Apache Curator Shared Lock ,这里就不细说,网上Google 一下就有不少资料。

相关文章
相关标签/搜索