MySQL实现散布式锁
基于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; |
通过以上实验,得到结论:
- 对索引列进行加锁的锁定级别为行级锁,如上所示,当其他事务想要对相同的数据再次加锁的时候,就会进行到阻塞状态。并且如果等待时间太长,会出现以下异常:
- 对区别行数据再次加排它锁,是没有任何问题的。
- 对已上锁的相同数据做修改和删除操作不需要多说,由于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
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在其中
- 事务A获得到读锁以后,在其他事务中是可以正常读取的,并且也能够再次获得读锁。
- 在读锁中如果想要进行插入操作是不会成功的,出现以下异常:
- 当前表获得到读锁以后,在当前表没有释放读锁之前,再获得写锁会一直进入到阻塞状态。
- 可以通过非加锁方式来读取数据,但是要注意:一定是在区别的事务下
表独占写锁
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锁
以后,只能在当前事务下操作获得锁的表,没法操作其他表,否则会出现以下异常
【注意】
MyISAM
在履行查询语句之前,会自动给触及的所有表加读锁,在履行更新操作前,会自动给触及的表加写锁,这个进程其实不需要用户干预,因此用户一般不需要使用命令来显式加锁
散布式锁实现
既然已了解到了MySQL锁相关内容,那末我们就来看看怎么实现,首先我们需要创建一张数据表
固然,只需要初始化创建一次
id bigint unsigned primary key auto_increment,
biz varchar(50) comment ‘业务Key’
unique(biz)
) engine=innodb;
在其中,biz
是为了辨别区别的业务,也能够理解为资源隔离,并且对biz
设置唯一索引,也能够避免其锁级别变成表级锁
既然for udpate
就是加锁成功,事务提交就自动释放锁,那末这个事情就非常好办了:
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散布式锁内容请搜索之前的文章或继续浏览下面的相关文章希望大家以后多多支持!
文章来源:丸子建站
文章标题:MySQL实现散布式锁
https://www.wanzijz.com/view/58459.html