MySQL实现分布式锁

bangongJIAO1@c 发布于 2025-11-29 阅读(3)
目录
  • 基于MySQL分布式锁实现原理及代码
    • MySQL锁
    • InnoDB
    • 共享锁
    • 排它锁
    • MyISAM
    • 表共享读锁
    • 表独占写锁
    • 分布式锁实现
    • 难点:为什么需要for(;
  • 总结

    基于MySQL分布式锁实现原理及代码

    工欲善其事必先利其器,在基于MySQL实现分布式锁之前,我们要先了解一点MySQL锁自身的相关内容

    MySQL锁

    我们知道:锁是计算机协调多个进程或者线程并发访问同一资源的机制,而在数据库中,除了传统的机器资源的争用之外,存储下来的数据也属于供用户共享的资源,所以如何保证数据并发的一致性,有效性是每个数据库必须解决的问题。

    除此之外,锁冲突也是影响数据库并发性能的主要因素,所以锁对于数据库而言就显得非常重要,也非常复杂。

    存储引擎是MySQL中非常重要的底层组件,主要用来处理不同类型的SQL操作,其中包括创建,读取,删除和修改操作。在MySQL中提供了不同类型的存储引擎,根据其不同的特性提供了不同的存储机制,索引和锁功能。

    根据show engines;能够列出MySQL下支持的存储引擎

    MySQL实现分布式锁

    如果没有特殊指定,那么在MySQL8.0中会设置InnoDB为默认的存储引擎

    在实际工作中,根据需求选择最多的两种存储引擎分别为:

    • InnoDB
    • MyISAM

    所以我们主要针对这两种类型来介绍MySQL的锁

    InnoDB

    InnoDB支持多粒度锁定,可以支持行锁,也可以支持表锁。如果没有升级锁粒度,那么默认情况下是以行锁来设计的。

    关于行锁和表锁的介绍:

    • 行锁对指定数据进行加锁,锁定粒度最小,开销大,加锁慢,容易出现死锁问题,出现锁冲突的概率最小,并发性最高
    • 表锁对整个表进行加锁,锁定粒度大,开销小,加锁快,不会出现死锁,出现锁冲突的概率最大,并发性最低

    这里没法说明那种锁最好,只有合适不合适

    在行级锁中,可以分为两种类型

    • 共享锁
    • 排他锁

    共享锁

    共享锁又称为读锁,允许其他事务读取被锁定的对象,也可以在其上获取其他共享锁,但不能写入。

    举个例子:

    • 事务T在数据A拥有共享锁,那么当前事务T对数据A可以读,但是不能修改。而且事务T2同样可以对数据A拥有共享锁,这样相当于在数据A上分别存在不同事务的共享锁
    • 数据A拥有了事务T的共享锁,那么就不能再拥有其他事务的排他锁

    下面是关于共享锁的具体实现,关键代码:select .. from table lock in share mode

     -- 创建实例表
     create table tb_lock(
         id bigint primary key auto_increment,
         t_name varchar(20)
     ) engine=InnoDB;

    开启两个窗口来测试:

    session1 session2
    set autocommit=0; set autocommit=0;
    select * from tb_lock where t_name = ‘zs’ lock in share mode;  
      select * from tb_lock where t_name = ‘zs’ lock in share mode;
      select * from tb_lock where t_name = ‘lsp’ lock in share mode;
    update tb_lock set t_name = ‘lzs’ where t_name = ‘zs’;  
    update tb_lock set t_name = ‘lsp111’ where t_name = ‘lsp’;  
      select * from tb_lock where t_name = ‘zs’;
    commit;  

    自动提交全部关闭,可以通过select @@autocommit;来查看

    通过以上实验,我们总结:

    • 共享锁基于行锁处理,不同事务可以在同一条数据上获取共享锁
    • 如果多个事务在同一条数据上获取共享锁,当想要修改该条数据的时候,会出现阻塞状态。直到其他事务将锁释放,该能够继续修改

    修改,删除,插入会默认对涉及到的数据加上排他锁

    • 单纯的select操作不会有任何影响,select不会加任何锁
    • 执行commit;自动释放锁

    排它锁

    又叫写锁。只允许获取锁的事务对数据进行操作【更新,删除】,其他事务对相同数据集只能进行读取,不能有跟新或者删除操作。而且也不能在相同数据集获取到共享锁。

    没错,就是这么霸道

    在MySQL中,想要基于排它锁实现行级锁,就需要对表中索引列加锁,否则的话,排它锁就属于表级锁

    下面一一来展示,关键代码:select .. from XX for update

    首先是有索引列状态

    session1 session2
    set autocommit=0; set autocommit=0;
    select * from tb_lock; select * from tb_lock;
    select * from tb_lock where id = 1 for update;  
      select * from tb_lock where id = 1 for update;
    select * from tb_lock where id = 2 for update;  
    commit;  

    通过以上实验,得到结论:

    • 对索引列进行加锁的锁定级别为行级锁,如上所示,当其他事务想要对相同的数据再次加锁的时候,就会进行到阻塞状态。并且如果等待时间过长,会出现如下异常:
     Lock wait timeout exceeded; try restarting transaction
    • 对不同行数据再次加排它锁,是没有任何问题的。
    • 对已经上锁的相同数据做修改和删除操作不需要多说,因为InnoDB默认会对其加入排它锁

    下面是无索引列状态

    session1 session2
    set autocommit=0; set autocommit=0;
    select * from tb_lock; select * from tb_lock;
    select * from tb_lock where t_name = ‘ls’ for update;  
      select * from tb_lock where t_name = ‘ls’ for update;
    commit  

    通过以上实验,得到结论:

    • 对非索引列其中一条数据加入了排它锁后,在其他事务中对不同数据再次加入排它锁,进入了阻塞状态
    • 说明当加锁列属于非索引时,InnoDB会对整个表进行上锁,进入到表级锁

    接下来我们来看看MyISAM的方式

    MyISAM

    MyISAM属于表级锁,被用来防止任何其他事务访问表的锁。

    其中表锁又分为两种形式

    • 表共享读锁: READ
    • 表独占写锁: WRITE

    这里我们要注意:表级锁只能防止其他会话进行不适当的读取或写入。

    • 持有WRITE 锁的会话可以执行表级操作,比如DELETE或者TRUNCATE
    • 持有会话READ锁,不能够执行DELETE或者TRUNCATE操作

    表共享读锁

    不管是READ还是WRITE,都是通过lock table 来获取表锁的,而READ锁拥有如下特性:

    • 持有锁的会话可以读取表,但是不能进行写入操作
    • 多个会话可以同时获取READ表的锁,而其他会话可以在不显式获取READ锁的情况下读取该表:也就是说直接通过select来操作

    那么,接下来我们来看实际操作,关键代码:lock tables table_name read

     create table tb_lock_isam(
         id bigint primary key auto_increment,
         t_name varchar(20)
     ) engine=MyISAM;

    开启两个窗口来进行操作:

    session1 session2
    set autocommit=0; set autocommit=0;
    LOCK TABLES tb_lock_isam READ;  
    select * from tb_lock_isam;  
    select * from tb_lock;  
      select * from tb_lock_isam;
      LOCK TABLES tb_lock_isam READ;
      select * from tb_lock_isam;
      select * from tb_lock;
    unlock tables; insert into tb_lock_isam(t_name) values(‘ll’);
       

    通过以上实战,验证以下结论:

    • 在当前事务下,获取到读锁,直接查询锁定表是没有问题的,但是如果想要读取其他表下的数据,那么就会出现以下异常:因为其他表并没有LOCK在其中
     Table 'tb_lock' was not locked with LOCK TABLES
    • 事务A获取到读锁之后,在其他事务中是可以正常读取的,并且也可以再次获取读锁。
    • 在读锁中如果想要进行插入操作是不会成功的,出现以下异常:
     Table 'tb_lock_isam' was locked with a READ lock and can't be updated
    • 当前表获取到读锁之后,在当前表没有释放读锁之前,再获取写锁会一直进入到阻塞状态。
    • 可以通过非加锁方式来读取数据,但是要注意:一定是在不同的事务下

    表独占写锁

    WRITE锁的特性和排它锁的特性非常相似,都特别霸道:

    • 持有锁的会话可以读写表
    • 只有持有锁的会话才能访问该表。在释放锁之前,没有其他会话可以访问它
    • 其他会话对表的锁请求在WRITE持有锁时被阻塞

    还是通过具体实战来进行演示效果,关键代码:lock tables table_name write

    session1 session2
    select * from tb_lock_isam; select * from tb_lock_isam;
    lock table tb_lock_isam write;  
    select * from tb_lock_isam;  
    insert into tb_lock_isam(t_name) values(‘66’);  
      select * from tb_lock_isam;
    unlock tables;  

    通过以上实战,验证以下结论:

    • 当事务获取到当前表的WRITE锁的时候,在当前事务下可以对获取锁的表进行任何操作,其他事务无法对表进行任意操作。
    • 在不同事务下不会对其他表的操作有影响
    • 在当前事务获取到WRITE锁之后,只能在当前事务下操作获取锁的表,无法操作其他表,否则会出现以下异常
      Table 'tb_index' was not locked with LOCK TABLES'

    注意

    MyISAM在执行查询语句之前,会自动给涉及的所有表加读锁,在执行更新操作前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此用户一般不需要使用命令来显式加锁

    分布式锁实现

    既然已经了解到了MySQL锁相关内容,那么我们就来看看如何实现,首先我们需要创建一张数据表

    当然,只需要初始化创建一次

     create table if not exists fud_distribute_lock(
         id bigint unsigned primary key auto_increment,
         biz varchar(50) comment '业务Key'
         unique(biz)
     ) engine=innodb;

    在其中,biz是为了区分不同的业务,也可以理解为资源隔离,并且对biz设置唯一索引,也能够防止其锁级别变为表级锁

    既然for udpate就是加锁成功,事务提交就自动释放锁,那么这个事情就非常好办了:

     // 省略了构造方法,需要传入DataSource和biz
     ​
     private static final String SELECT_SQL = 
         "SELECT * FROM fud_distribute_lock WHERE `biz` = ? for update";
     private static final String INSERT_SQL = 
         "INSERT INTO fud_distribute_lock(`biz`) values(?)";
     ​
     // 从构造方法中传入
     private final DataSource source;
     private Connection connection;
     ​
     public void lock() {
         PreparedStatement psmt = null;
         ResultSet rs = null;
     ​
         try {
             // while(true); 
             for (; ; ) {
                 connection = this.source.getConnection();
                 // 关闭自动提交事务
                 connection.setAutoCommit(false);
                 
                 psmt = connection.prepareStatement(SELECT_SQL);
                 psmt.setString(1, biz);
                 rs = psmt.executeQuery();
                 if (rs.next()) {
                     return;
                 }
                 connection.commit();
                 close(connection, psmt, rs);
                 // 如果没有相关查询,需要插入
                 Connection updConnection = this.source.getConnection();
                 PreparedStatement insertStatement = null;
                 try {
                     insertStatement = updConnection.prepareStatement(INSERT_SQL);
                     insertStatement.setString(1, biz);
                     if (insertStatement.executeUpdate() == 1) {
                         LOGGER.info("创建锁记录成功");
                     }
                 } catch (Exception e) {
                     LOGGER.error("创建锁记录异常:{}", e.getMessage());
                 } finally {
                     close(insertStatement, updConnection);
                 }
             }
         } catch (Exception e) {
             LOGGER.error("lock异常信息:{}", e.getMessage());
             throw new BusException(e);
         } finally {
             close(psmt, rs);
         }
     }
     ​
     public void unlock() {
         try {
             // 事务提交之后自动解锁
             connection.commit();
             close(connection);
         } catch (Exception e) {
             LOGGER.error("unlock异常信息:{}", e.getMessage());
             throw new BusException(e);
         }
     }
     ​
     public void close(AutoCloseable... closeables) {
         Arrays.stream(closeables).forEach(closeable -> {
             if (null != closeable) {
                 try {
                     closeable.close();
                 } catch (Exception e) {
                     LOGGER.error("close关闭异常:{}", e.getMessage());
                 }
             }
         });
     }

    难点:为什么需要for(;

    如果一个请求是第一次进来的,比如biz=order,在这个表中是不会存储order这条记录,那么select ...for update就不会生效,所以就需要先将order插入到表记录中,也就是执行insert操作。

    insert执行成功之后,记录select...for update,这样获取锁才能生效

    总结

    基于MySQL的分布式锁在实际开发过程中很少使用,但是我们还是要有一个思路在。那么本节针对MySQL的分布式锁实现到这里就结束了,掌握了MySQL的基础锁,那么就会非常简单了。