前面已经介绍了原理性的内容,如果原理看的不爽,想看点实际展示效果的话,那么它来了它来了,它带着代码走来啦:blush:
工欲善其事必先利其器,在基于MySQL实现分布式锁之前,我们要先了解一点MySQL锁自身的相关内容
我们知道: 锁是计算机协调多个进程或者线程并发访问同一资源的机制 ,而在数据库中,除了传统的机器资源的争用之外,存储下来的数据也属于供用户共享的资源,所以如何保证数据并发的一致性,有效性是每个数据库必须解决的问题。
除此之外,锁冲突也是影响数据库并发性能的主要因素,所以锁对于数据库而言就显得非常重要,也非常复杂。
而 存储引擎 是MySQL中非常重要的底层组件,主要用来处理不同类型的SQL操作,其中包括创建,读取,删除和修改操作。在MySQL中提供了不同类型的存储引擎,根据其不同的特性提供了不同的存储机制,索引和锁功能。
根据 show engines;
能够列出MySQL下支持的存储引擎
如果没有特殊指定,那么在 MySQL8.0
中会设置 InnoDB
为默认的存储引擎
在实际工作中,根据需求选择最多的两种存储引擎分别为:
所以我们主要针对这两种类型来介绍MySQL的锁
InnoDB
InnoDB
支持 多粒度锁定 ,可以支持 行锁 ,也可以支持 表锁 。如果没有升级锁粒度,那么默认情况下是以行锁来设计的。
关于行锁和表锁的介绍
这里没法说明那种锁最好,只有合适不合适
在行级锁中,可以分为两种类型
共享锁
共享锁又称为 读锁 ,允许其他 事务 读取被锁定的对象,也可以在其上获取其他共享锁,但不能写入。
举个例子:
下面是关于共享锁的具体实现,关键代码: 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
下面是无索引列状态
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 |
通过以上实验,得到结论:
接下来我们来看看 MyISAM 的方式
MyISAM
MyISAM属于表级锁,被用来防止任何其他 事务 访问表的锁。
其中表锁又分为两种形式
这里我们要注意:表级锁只能防止其他会话进行不适当的读取或写入。
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'); |
通过以上实战,验证以下结论:
Table 'tb_lock' was not locked with LOCK TABLES
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的基础锁,那么就会非常简单了。