MySQL悲观锁与乐观锁的实现方案

目录
  • 前言
  • 实战
  • 1、无锁
  • 2、悲观锁
  • 3、乐观锁
  • 总结

前言

悲观锁和乐观锁是用来解决并发问题的两种思想,在不同的平台有着各自的实现。例如在java中,synchronized就可以认为是悲观锁的实现(不严谨,有锁升级的过程,升级到重量级锁才算),atomic***原子类可以认为是乐观锁的实现。

悲观锁

具有强烈的独占和排他特性,在整个处理过程中将数据处于锁定状态,一般是通过系统的互斥量来实现。当其他线程想要获取锁时会被阻塞,直到持有锁的线程释放锁。

乐观锁

对数据的修改和访问持乐观态度,假设不会发生冲突,只有当数据提交更新时才会对数据冲突与否进行检测,如果没有冲突则顺利提交更新,否则快速失败,返回一个错误给用户,让用户选择接下来该如何去做,一般来说失败后会继续重试,直到提交更新成功为止。

mysql本身就支持锁机制,例如我们有一个「先查再写」的需求,我们希望整个流程是一个原子操作,中间不能被打断,这时候就可以通过给查询的数据行加「排他锁」来实现。只要当前事务不释放锁,其他事务要想获得排他锁,mysql就会将其阻塞,直到当前事务释放锁。这种mysql底层的排他锁就称作「悲观锁」。

mysql本身不提供乐观锁的功能,需要开发者自己实现。普遍的做法是在表中加一个version列,用来标记数据行的版本,当我们需要更新数据时,必须比对version版本,version一致说明这个期间数据没有被其他事务修改过,否则说明数据已经被其他事务修改,需要自旋重试了。

实战

假设数据库有两张表:商品表和订单表。

用户下单后需要执行两个操作:

  1. 商品表减去库存。
  2. 订单表创建一条记录。

初始数据:id为1的商品有100的库存,订单表数据为空。

客户端启动10个线程并发下单,分别在无锁、悲观锁、乐观锁的场景下有哪些表现。

如下是创建表的sql语句:

-- 商品表
create table `goods` (
  `id` bigint(20) not null auto_increment,
  `goods_name` varchar(50) not null,
  `price` decimal(10,2) not null,
  `stock` int(11) default '0',
  `version` int(10) unsigned not null default '0',
  primary key (`id`)
) engine=innodb auto_increment=1 default charset=utf8

-- 订单表
create table `t_order` (
  `id` bigint(20) not null auto_increment,
  `goods_id` bigint(20) not null,
  `order_time` datetime not null,
  primary key (`id`) using btree
) engine=innodb auto_increment=1 default charset=utf8

1、无锁

不做任何处理。

// 下单
private boolean order(){
    goods goods = goodsmapper.selectbyid(1l);
    boolean success = false;
    if (goods.getstock() > 0) {
        goods.setstock(goods.getstock() - 1);
        // 更新库存
        goodsmapper.updatebyid(goods);
        // 创建订单
        ordermapper.save(goods.getid());
        success = true;
    }
    return success;
}

控制台输出结果:

2、悲观锁

查询商品时加for update,给数据行加排他锁,这样其他线程再查询时就会被阻塞,直到当前线程的事务提交并释放锁,其他线程才能继续下单。这种方式并发性能不高。

sql语句

@select("select * from goods where id = #{id} for update")
goods selectforupdate(long id);

控制台输出结果:

注意:for update必须在事务中才有效,查询和更新必须在同一个事务中!!!

3、乐观锁

实现思路是:每次更新时校验版本号,如果版本号一致说明期间数据没有被其他线程改过,当前线程可以正常提交更新,否则说明数据已经被其他线程改过了,当前线程需要自旋重试,直到业务成功为止。

更新数据的同时版本号必须自增!!!

@update("update goods set stock = #{stock},version = version+1 where id = #{id} and version = #{version}")
int updatebyversion(long id, integer stock, integer version);

业务代码

boolean order(){
    goods goods = goodsmapper.selectbyid(1l);
    boolean success = false;
    if (goods.getstock() > 0) {
        goods.setstock(goods.getstock() - 1);
        // 更新库存,带上版本号
        int result = goodsmapper.updatebyversion(goods.getid(), goods.getstock(), goods.getversion());
        if (result <= 0) {
            // 更新失败,说明期间数据已经被其他线程修改,需要递归重试
            return order();
        }
        // 创建订单
        ordermapper.save(goods.getid());
        success = true;
    }
    return success;
}

控制台输出结果:

总结

到此这篇关于mysql悲观锁与乐观锁方案的文章就介绍到这了,更多相关mysql悲观锁与乐观锁内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!

(0)
上一篇 2022年3月21日
下一篇 2022年3月21日

相关推荐