美文网首页Java
MySQL中的悲观锁和乐观锁

MySQL中的悲观锁和乐观锁

作者: 西敏寺钟声 | 来源:发表于2020-07-22 23:06 被阅读0次

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

    Pessimistic Lock

    概述

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

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

    特性

    • 需要依靠数据库中的锁机制来实现,即通过常用的select ... for update操作来实现悲观锁。
    • 需要开启事务,在事务中实现锁机制。
    • 可以最大程度的保证数据操作的独占性。
    • select for update语句中所有扫描过的行都会被锁上,这一点很容易造成问题。如果用悲观锁请确保用到了索引,否则造成锁表。
    • 长事务中的锁等待,会导致其他用户长时间无法操作。
    • 主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。

    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 id = '2' for update;

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

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

    1. 开启事务

    begin

    1. 执行update操作

    update t_logs set action = '测试用例' where id = '2';

    1. 如果不执行上一个会话的commit操作,会发现此会话一直处于block状态。
    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类
    @Getter
    @Setter
    @ToString
    public class ProductStock {
    
        private Long productId; //商品id
    
        private Integer 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 条件字句上需要建索引,避免全表扫描。

    相关文章

      网友评论

        本文标题:MySQL中的悲观锁和乐观锁

        本文链接:https://www.haomeiwen.com/subject/nxjgkktx.html