MySQL实现分布式锁

目录
  • 基于MySQL分布式锁实现原理及代码
    • MySQL锁
    • InnoDB
    • 共享锁
    • 排它锁
    • MyISAM
    • 表共享读锁
    • 表独占写锁
    • 分布式锁实现
    • 难点:为什么需要for(;
  • 总结

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

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

MySQL锁

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

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

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

根据show engines;能够列出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的基础锁,那么就会非常简单了。

到此这篇关于MySQL实现分布式锁的文章就介绍到这了,更多相关MySQL分布式锁内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • mysql居然还能实现分布式锁的方法

    前言 之前的文章中通过电商场景中秒杀的例子和大家分享了单体架构中锁的使用方式,但是现在很多应用系统都是相当庞大的,很多应用系统都是微服务的架构体系,那么在这种跨jvm的场景下,我们又该如何去解决并发. 单体应用锁的局限性 在进入实战之前简单和大家粗略聊一下互联网系统中的架构演进. 在互联网系统发展之初,消耗资源比较小,用户量也比较小,我们只部署一个tomcat应用就可以满足需求.一个tomcat我们可以看做是一个jvm的进程,当大量的请求并发到达系统时,所有的请求都落在这唯一的一个tomcat上

  • MySQL借助DB实现分布式锁思路详解

    前言 无论是单机锁还是分布式锁,原理都是基于共享的数据,判断当前操作的行为.对于单机则是共享RAM内存,对于集群则可以借助Redis,ZK,DB等第三方组件来实现.Redis,ZK对分布式锁提供了很好的支持,基本上开箱即用,然而这些组件本身要高可用,系统也需要强依赖这些组件,额外增加了不少成本.DB对于系统来说本身就默认为高可用组件,针对一些低频的业务使用DB实现分布式锁也是一个不错的解决方案,比如控制多机器下定时任务的起调,针对审批回调处理等,本文将给出DB实现分布式锁的一些场景以及解决方案,

  • 使用MySQL实现一个分布式锁

    介绍 在分布式系统中,分布锁是一个最基础的工具类.例如,部署了2个有付款功能的微服务中,用户有可能对一个订单发起2次付款操作,而这2次请求可能被发到2个服务中,所以必须得用分布式锁防止重复提交,获取到锁的服务正常进行付款操作,获取不到锁的服务提示重复操作. 我司封装了大量的基础工具类,当我们想使用分布式锁的时候只要做3件事情 1.在数据库中建globallocktable表 2.引入相应的jar包 3.在代码中写上@Autowired GlobalLockComponent globalLock

  • MySQL实现分布式锁

    目录 基于MySQL分布式锁实现原理及代码 MySQL锁 InnoDB 共享锁 排它锁 MyISAM 表共享读锁 表独占写锁 分布式锁实现 难点:为什么需要for(; 总结 基于MySQL分布式锁实现原理及代码 工欲善其事必先利其器,在基于MySQL实现分布式锁之前,我们要先了解一点MySQL锁自身的相关内容 MySQL锁 我们知道:锁是计算机协调多个进程或者线程并发访问同一资源的机制,而在数据库中,除了传统的机器资源的争用之外,存储下来的数据也属于供用户共享的资源,所以如何保证数据并发的一致性

  • 详细解读分布式锁原理及三种实现方式

    目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题.分布式的CAP理论告诉我们"任何一个分布式系统都无法同时满足一致性(Consistency).可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项."所以,很多系统在设计之初就要对这三者做出取舍.在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证"最终一致性",只要这个最终

  • java基于jedisLock—redis分布式锁实现示例代码

    分布式锁是啥? 单机锁的概念:我们正常跑的单机项目(也就是在tomcat下跑一个项目不配置集群)想要在高并发的时候加锁很容易就可以搞定,java提供了很多的机制例如:synchronized.volatile.ReentrantLock等锁的机制. 为啥需要分布式锁:当我们的项目比较庞大的时候,单机版的项目已经不能满足吞吐量的需求了,需要对项目做负载均衡,有可能还需要对项目进行解耦拆分成不同的服务,那么肯定是做成分布式的项目,分布式的项目因为是不同的程序控制,所以使用java提供的锁并不能完全保

  • 基于redis分布式锁实现秒杀功能

    最近在项目中遇到了类似"秒杀"的业务场景,在本篇博客中,我将用一个非常简单的demo,阐述实现所谓"秒杀"的基本思路. 业务场景 所谓秒杀,从业务角度看,是短时间内多个用户"争抢"资源,这里的资源在大部分秒杀场景里是商品:将业务抽象,技术角度看,秒杀就是多个线程对资源进行操作,所以实现秒杀,就必须控制线程对资源的争抢,既要保证高效并发,也要保证操作的正确. 一些可能的实现 刚才提到过,实现秒杀的关键点是控制线程对资源的争抢,根据基本的线程知识,可

  • 浅谈分布式锁的几种使用方式(redis、zookeeper、数据库)

    Q:一个业务服务器,一个数据库,操作:查询用户当前余额,扣除当前余额的3%作为手续费 synchronized lock dblock Q:两个业务服务器,一个数据库,操作:查询用户当前余额,扣除当前余额的3%作为手续费 分布式锁 我们需要怎么样的分布式锁? 可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行. 这把锁要是一把可重入锁(避免死锁) 这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条) 这把锁最好是一把公平锁(根据业务需求考虑要不要这条) 有高可用

  • Spring Boot基于数据库如何实现简单的分布式锁

    1.简介 分布式锁的方式有很多种,通常方案有: 基于mysql数据库 基于redis 基于ZooKeeper 网上的实现方式有很多,本文主要介绍的是如果使用mysql实现简单的分布式锁,加锁流程如下图: 其实大致思想如下: 1.根据一个值来获取锁(也就是我这里的tag),如果当前不存在锁,那么在数据库插入一条记录,然后进行处理业务,当结束,释放锁(删除锁). 2.如果存在锁,判断锁是否过期,如果过期则更新锁的有效期,然后继续处理业务,当结束时,释放锁.如果没有过期,那么获取锁失败,退出. 2.数

  • 使用redis分布式锁解决并发线程资源共享问题

    前言 众所周知, 在多线程中,因为共享全局变量,会导致资源修改结果不一致,所以需要加锁来解决这个问题,保证同一时间只有一个线程对资源进行操作 但是在分布式架构中,我们的服务可能会有n个实例,但线程锁只对同一个实例有效,就需要用到分布式锁----redis setnx 原理 修改某个资源时, 在redis中设置一个key,value根据实际情况自行决定如何表示 我们既然要通过检查key是否存在(存在表示有线程在修改资源,资源上锁,其他线程不可同时操作,若key不存在,表示资源未被线程占用,允许线程

随机推荐