Mysql锁机制--乐观锁 & 悲观锁

第一部分 悲观锁

1 概念(来自百科)

悲观锁,正如其名,它指的是对数据被外界(包括当前系统的其它事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排它性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

还可以简单理解,就是Java中的 Synchronized 关键字。只要对代码加了 Synchronized 关键字,JVM 底层就能保证其线程安全性。

那么,在 Mysql 中如何实现悲观锁呢?下面分别以命令行以及Java代码的方式进行演示。

2 命令行演示

2.1 准备数据

复制代码
复制代码
DROP DATABASE IF EXISTS cyhTest;
CREATE DATABASE cyhTest;

USE cyhTest;

DROP TABLE IF EXISTS employee;

CREATE TABLE IF NOT EXISTS employee (
  id      INTEGER NOT NULL,
  money   INTEGER,
  version INTEGER,
  PRIMARY KEY (id)
)
  ENGINE = INNODB;

INSERT INTO employee VALUE (1, 0, 1);

SELECT * FROM employee;
复制代码
复制代码

目前数据库中只有一条记录,且初始Money=0

2.2 测试

测试准备:

  • 还是两个会话(终端),左边会话是白色背景、右边会话是黑色背景
  • 关闭自动提交:set autocommit = 0; 

现在开始测试:

第一步:两个终端均关闭自动提交

左边:

右边:

第二步:左边利用 select .... for update 的悲观锁语法锁住记录

select * from employee where id = 1 for update; 

第三步:右边也尝试利用 select .... for update 的悲观锁语法锁住记录

可以看到,Sql语句被挂起(被阻塞)!

提示:如果被阻塞的时间太长,会提示如下:

第四步:左边执行更新操作并提交事务

Sql语句:

update employee set money = 0 + 1 where id = 1;
commit; 

结果:

分析:

  • Money 的旧值为0,所以更新时 Money=0+1
  • 一执行 commit 后,注意查看右边Sql语句的变化

第五步:查看右边Sql语句的变化

分析:

  • 被左边悲观锁阻塞了 11.33 秒
  • Money=1,这是左边更新后的结果

2.3 结论

可以看到,当左边(事务A)使用了 select ... for update 的悲观锁后,右边(事务B)再想使用将被阻塞,同时,阻塞被解除后事务B能看到事务A对数据的修改,所以,这就可以很好地解决并发事务的更新丢失问题啦(诚然,这也是人家悲观锁的分内事)

3 Java代码演示

Demo 代码地址:https://github.com/cyhbyw/cyh_Spring_IsolationConcurrencyTransaction

Demo 工程名称:usingMybatis

3.1 准备

确保数据库用户名和密码对应;执行 test.sql 文件中的脚本;

3.2 业务逻辑代码

XML 文件中的 Mapper:

    <select id="findByIdWithPessimisticLock" resultType="com.cyh.entity.Employee">
        SELECT * FROM employee WHERE id = #{id} FOR UPDATE
    </select>

提示:这里是 SELCT ... FOR UPDATE

Service 中的业务逻辑:

复制代码
复制代码
1     @Transactional(rollbackFor = RuntimeException.class)
2     public void increaseMoneyWithPessimisticLock(Integer id) {
3         Employee employee = employeeMapper.findByIdWithPessimisticLock(id);
4         final Integer oldMoney = employee.getMoney();
5         LOGGER.info("oldMoney: {}", oldMoney);
6         employee.setMoney(oldMoney + 1);
7         employeeMapper.updateEmployee(employee);
8     }
复制代码
复制代码

提示:第3行中的 findByIdWithPessimisticLock() 方法就对应上面XML文件中的Mapper,有悲观锁

3.3 测试代码

    private void increaseMoneyWithPessimisticLock() {
        int threadCount = 100;
        while (threadCount-- > 0) {
            new Thread(() -> employeeService.increaseMoneyWithPessimisticLock(1)).start();
        }
    }

提示:100根线程,每个线程将Money值加1,预期结果是100

3.4 执行测试&查看结果

SELECT * FROM employee;

Money=100,是预期结果。

3.5 查看日志

  

可以看到,oldMoney 值由0到99依次严格递增且不重复(这就是想要的效果)。

第二部分 乐观锁

1 概念

1.1 理解方式一(来自网上其它小伙伴的博客)

乐观锁认为一般情况下数据不会造成冲突,所以在数据进行提交更新时才会对数据的冲突与否进行检测。如果没有冲突那就OK;如果出现冲突了,则返回错误信息并让用户决定如何去做。

1.2 理解方式二(来自网上其它小伙伴的博客)

乐观锁的特点是先进行业务操作,不到万不得已不会去拿锁。乐观地认为拿锁多半会是成功的,因此在完成业务操作需要实际更新数据的最后一步再去拿一下锁。

1.3 我的理解

理解一:就是 CAS 操作

理解二:类似于 SVN、GIt 这些版本管理系统,当修改了某个文件需要提交的时候,它会检查文件的当前版本是否与服务器上的一致,如果一致那就可以直接提交,如果不一致,那就必须先更新服务器上的最新代码然后再提交(也就是先将这个文件的版本更新成和服务器一样的版本)

2 如何实现乐观锁呢

首先说明一点的是:乐观锁在数据库上的实现完全是逻辑的,数据库本身不提供支持,而是需要开发者自己来实现。

常见的做法有两种:版本号控制及时间戳控制。

版本号控制的原理:

  • 为表中加一个 version 字段;
  • 当读取数据时,连同这个 version 字段一起读出;
  • 数据每更新一次就将此值加一;
  • 当提交更新时,判断数据库表中对应记录的当前版本号是否与之前取出来的版本号一致,如果一致则可以直接更新,如果不一致则表示是过期数据需要重试或者做其它操作(PS:这完完全全就是 CAS 的实现逻辑呀~)

至于时间戳控制,其原理和版本号控制差不多,也是在表中添加一个 timestamp 的时间戳字段,然后提交更新时判断数据库中对应记录的当前时间戳是否与之前取出来的时间戳一致,一致就更新,不一致就重试。

3 Java代码演示

3.1 准备

还是之前的Java代码;这里使用版本号控制来实现乐观锁。

3.2 业务逻辑代码

XMP 文件中的 Mapper:

    <update id="updateEmployeeWithOptimisticLock">
        UPDATE employee SET money = #{e.money}, version = #{e.version} + 1 WHERE id = #{e.id} AND #{e.version} = version
    </update>

提示:

  • SET 中有 version = #{e.version} + 1 的操作
  • WHERE 条件中有 #{e.version} = version 的判断

Service 中的业务逻辑:

复制代码
复制代码
 1     /**
 2      * 失败尝试
 3      * @param id
 4      */
 5     public void increaseMoneyWithOptimisticLock(Integer id) {
 6         int tryTimes = 0;
 7         while (true) {
 8             tryTimes++;
 9             if (internalIncreaseMoneyWithOptimisticLock(id) != 0) {
10                 // 说明更新成功,直接退出
11                 break;
12             }
13             if (tryTimes == 200) {
14                 // 达到最大重试次数,退出
15                 break;
16             }
17             try {
18                 // 休息一段时间后再重试
19                 TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
20             } catch (InterruptedException e) {
21                 e.printStackTrace();
22                 Thread.currentThread().interrupt();
23             }
24         }
25         LOGGER.info("tryTimes: {}", tryTimes);
26     }
27 
28     /**
29      * 查找Employee对象,并进行更新
30      * @param id
31      * @return
32      */
33     @Transactional(rollbackFor = RuntimeException.class)
34     public Integer internalIncreaseMoneyWithOptimisticLock(Integer id) {
35         Employee employee = employeeMapper.findById(id);
36         final Integer oldMoney = employee.getMoney();
37         LOGGER.info("oldMoney: {}", oldMoney);
38         employee.setMoney(oldMoney + 1);
39         return employeeMapper.updateEmployeeWithOptimisticLock(employee);
40     }
复制代码
复制代码

提示:

  • 乐观锁,其实是开发者自己实现的逻辑
  • 更新失败后,休息一段时间后再进行重试

3.3 测试代码

    private void increaseMoneyWithOptimisticLock() {
        int threadCount = 100;
        while (threadCount-- > 0) {
            new Thread(() -> employeeService.increaseMoneyWithOptimisticLock(1)).start();
        }
    }

提示:100根线程,每个线程将Money值加1,预期结果是Money的值在之前的基础上增加100

3.4 执行测试&查看结果

之前Money=100,现在预期增加100,所以现在Money=200符合预期。同时,version 字段的值也由最初的1更新为101(也被更新了100次,每次加一)。

3.5 查看日志

  

可以看到,还是有一些线程会拿到相同的值,但是由于版本控制使得最终只有一根线程能够更新成功,其它更新失败的线程会继续尝试,最终能更新成功。

第三部分 总结&对比

  悲观锁 乐观锁
概念 查询时直接锁住记录使得其它事务不能查询,更不能更新 提交更新时检查版本或者时间戳是否符合
语法 select ... for update 使用 version 或者 timestamp 进行比较
实现者 数据库本身 开发者
适用场景 并发量大 并发量小
类比Java Synchronized关键字 CAS 算法
posted @   大浪不惊涛  阅读(287)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示
历史上的今天:
2017-02-15 关于update 表名 set 字段1 = 值1 and 字段2 = 值2的执行结果说明
点击右上角即可分享
微信分享提示