问题

(1)什么是分布式锁?

(2)为什么需要分布式锁?

(3)mysql如何实现分布式锁?

(4)mysql分布式锁的优点和缺点?

简介

随着并发量的不断增加,单机的服务迟早要向多节点或者微服务进化,这时候原来单机模式下使用的synchronized或者ReentrantLock将不再适用,我们迫切地需要一种分布式环境下保证线程安全的解决方案,今天我们一起来学习一下mysql分布式锁如何实现分布式线程安全。

基础知识

mysql中提供了两个函数——get_lock('key', timeout)release_lock('key')——来实现分布式锁,可以根据key来加锁,这是一个字符串,可以设置超时时间(单位:秒),当调用release_lock('key')或者客户端断线的时候释放锁。

它们的使用方法如下:

mysql> select get_lock('user_1', 10);
-> 1
mysql> select release_lock('user_1');
-> 1

get_lock('user_1', 10)如果10秒之内获取到锁则返回1,否则返回0;

release_lock('user_1')如果该锁是当前客户端持有的则返回1,如果该锁被其它客户端持有着则返回0,如果该锁没有被任何客户端持有则返回null;

多客户端案例

为了便于举例【本篇文章由“彤哥读源码”原创,请支持原创,谢谢!】,这里的超时时间全部设置为0,也就是立即返回。

时刻 客户端A 客户端B
1 get_lock('user_1', 0) -> 1 -
2 - get_lock('user_1', 0) -> 0
3 - release_lock('user_1', 0) -> 0
4 release_lock('user_1', 0) -> 1 -
5 release_lock('user_2', 0) -> null -
6 - get_lock('user_1', 0) -> 1
7 - release_lock('user_1', 0) -> 1

Java实现

为了方便快速实现,这里使用 springboot2.1 + mybatis 实现,并且省略spring的配置,只列举主要的几个类。

定义Locker接口

接口中只有一个方法,入参1为加锁的key,入参2为执行的命令。

public interface Locker {
void lock(String key, Runnable command);
}

mysql分布式锁实现

mysql的实现中要注意以下两点:

(1)加锁、释放锁必须在同一个session(同一个客户端)中,所以这里不能使用Mapper接口的方式调用,因为Mapper接口有可能会导致不在同一个session。

(2)可重入性是通过ThreadLocal保证的;

@Slf4j
@Component
public class MysqlLocker implements Locker { private static final ThreadLocal<SqlSessionWrapper> localSession = new ThreadLocal<>(); @Autowired
private SqlSessionFactory sqlSessionFactory; @Override
public void lock(String key, Runnable command) {
// 加锁、释放锁必须使用同一个session
SqlSessionWrapper sqlSessionWrapper = localSession.get();
if (sqlSessionWrapper == null) {
// 第一次获取锁
localSession.set(new SqlSessionWrapper(sqlSessionFactory.openSession()));
}
try {
// 【本篇文章由“彤哥读源码”原创,请支持原创,谢谢!】
// -1表示没获取到锁一直等待
if (getLock(key, -1)) {
command.run();
}
} catch (Exception e) {
log.error("lock error", e);
} finally {
releaseLock(key);
}
} private boolean getLock(String key, long timeout) {
Map<String, Object> param = new HashMap<>();
param.put("key", key);
param.put("timeout", timeout);
SqlSessionWrapper sqlSessionWrapper = localSession.get();
Integer result = sqlSessionWrapper.sqlSession.selectOne("LockerMapper.getLock", param);
if (result != null && result.intValue() == 1) {
// 获取到了锁,state加1
sqlSessionWrapper.state++;
return true;
}
return false;
} private boolean releaseLock(String key) {
SqlSessionWrapper sqlSessionWrapper = localSession.get();
Integer result = sqlSessionWrapper.sqlSession.selectOne("LockerMapper.releaseLock", key);
if (result != null && result.intValue() == 1) {
// 释放锁成功,state减1
sqlSessionWrapper.state--;
// 当state减为0的时候说明当前线程获取的锁全部释放了,则关闭session并从ThreadLocal中移除
if (sqlSessionWrapper.state == 0) {
sqlSessionWrapper.sqlSession.close();
localSession.remove();
}
return true;
}
return false;
} private static class SqlSessionWrapper {
int state;
SqlSession sqlSession; public SqlSessionWrapper(SqlSession sqlSession) {
this.state = 0;
this.sqlSession = sqlSession;
}
}
}

LockerMapper.xml

定义get_lock()、release_lock()的语句。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="LockerMapper">
<select id="getLock" resultType="integer">
select get_lock(#{key}, #{timeout});
</select> <select id="releaseLock" resultType="integer">
select release_lock(#{key})
</select>
</mapper>

测试类

这里启动1000个线程,每个线程打印一句话并睡眠2秒钟。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class MysqlLockerTest { @Autowired
private Locker locker; @Test
public void testMysqlLocker() throws IOException {
for (int i = 0; i < 1000; i++) {
// 多节点测试
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
locker.lock("lock", ()-> {
// 可重入性测试
locker.lock("lock", ()-> {
System.out.println(String.format("time: %d, threadName: %s", System.currentTimeMillis(), Thread.currentThread().getName()));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
});
}, "Thread-"+i).start();
} System.in.read();
}
}

运行结果

查看运行结果发现每隔2秒打印一个线程的信息,说明这个锁是有效的,至于分布式环境下面的验证也很简单,起多个MysqlLockerTest实例即可。

time: 1568715905952, threadName: Thread-3
time: 1568715907955, threadName: Thread-4
time: 1568715909966, threadName: Thread-8
time: 1568715911967, threadName: Thread-0
time: 1568715913969, threadName: Thread-1
time: 1568715915972, threadName: Thread-9
time: 1568715917975, threadName: Thread-6
time: 1568715919997, threadName: Thread-5
time: 1568715921999, threadName: Thread-7
time: 1568715924001, threadName: Thread-2

总结

(1)分布式环境下需要使用分布式锁,单机的锁将无法保证线程安全;

(2)mysql分布式锁是基于get_lock('key', timeout)release_lock('key')两个函数实现的;

(3)mysql分布式锁是可重入锁;

彩蛋

使用mysql分布式锁需要注意些什么呢?

答:必须保证多个服务节点使用的是同一个mysql库【本篇文章由“彤哥读源码”原创,请支持原创,谢谢!】。

mysql分布式锁具有哪些优点?

答:1)方便快捷,因为基本每个服务都会连接数据库,但是不是每个服务都会使用redis或者zookeeper;

2)如果客户端断线了会自动释放锁,不会造成锁一直被占用;

3)mysql分布式锁是可重入锁,对于旧代码的改造成本低;

mysql分布式锁具有哪些缺点?

答:1)加锁直接打到数据库,增加了数据库的压力;

2)加锁的线程会占用一个session,也就是一个连接数,如果并发量大可能会导致正常执行的sql语句获取不到连接;

3)服务拆分后如果每个服务使用自己的数据库,则不合适;

4)相对于redis或者zookeeper分布式锁,效率相对要低一些;


欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

死磕 java同步系列之mysql分布式锁的更多相关文章

  1. 死磕 java同步系列之zookeeper分布式锁

    问题 (1)zookeeper如何实现分布式锁? (2)zookeeper分布式锁有哪些优点? (3)zookeeper分布式锁有哪些缺点? 简介 zooKeeper是一个分布式的,开放源码的分布式应 ...

  2. 死磕 java同步系列之redis分布式锁进化史

    问题 (1)redis如何实现分布式锁? (2)redis分布式锁有哪些优点? (3)redis分布式锁有哪些缺点? (4)redis实现分布式锁有没有现成的轮子可以使用? 简介 Redis(全称:R ...

  3. 死磕 java同步系列之终结篇

    简介 同步系列到此就结束了,本篇文章对同步系列做一个总结. 脑图 下面是关于同步系列的一份脑图,列举了主要的知识点和问题点,看过本系列文章的同学可以根据脑图自行回顾所学的内容,也可以作为面试前的准备. ...

  4. 死磕 java同步系列之AQS起篇

    问题 (1)AQS是什么? (2)AQS的定位? (3)AQS的实现原理? (4)基于AQS实现自己的锁? 简介 AQS的全称是AbstractQueuedSynchronizer,它的定位是为Jav ...

  5. 死磕 java同步系列之volatile解析

    问题 (1)volatile是如何保证可见性的? (2)volatile是如何禁止重排序的? (3)volatile的实现原理? (4)volatile的缺陷? 简介 volatile可以说是Java ...

  6. 死磕 java同步系列之自己动手写一个锁Lock

    问题 (1)自己动手写一个锁需要哪些知识? (2)自己动手写一个锁到底有多简单? (3)自己能不能写出来一个完美的锁? 简介 本篇文章的目标一是自己动手写一个锁,这个锁的功能很简单,能进行正常的加锁. ...

  7. 死磕 java同步系列之CyclicBarrier源码解析——有图有真相

    问题 (1)CyclicBarrier是什么? (2)CyclicBarrier具有什么特性? (3)CyclicBarrier与CountDownLatch的对比? 简介 CyclicBarrier ...

  8. 死磕 java同步系列之Phaser源码解析

    问题 (1)Phaser是什么? (2)Phaser具有哪些特性? (3)Phaser相对于CyclicBarrier和CountDownLatch的优势? 简介 Phaser,翻译为阶段,它适用于这 ...

  9. 死磕 java同步系列之StampedLock源码解析

    问题 (1)StampedLock是什么? (2)StampedLock具有什么特性? (3)StampedLock是否支持可重入? (4)StampedLock与ReentrantReadWrite ...

随机推荐

  1. POJ 1015 陪审团问题

    题意略. 思路: 这个题目开始我本来打算用个二维dp,令dp[ i ][ j ]为考虑前i个人,有j个名额的时候,我所能获取的最小差,后来发现不好转移.因为dp[ i ][ j ]有可能是+2, 也有 ...

  2. Leetcode之回溯法专题-131. 分割回文串(Palindrome Partitioning)

    Leetcode之回溯法专题-131. 分割回文串(Palindrome Partitioning) 给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串. 返回 s 所有可能的分割方案. ...

  3. python requests接口测试系列:连接mysql,获取mysql查询的值作为接口的入参

    主要思路: 连接mysql数据库,这里数据库需要使用Proxifier来设置代理,然后才能正常连接 获取mysql数据库中某一数据,作为接口的参数信息 将接口返回结果保存至csv数据表中 # -*- ...

  4. 问题.beego路由设置及请求参数传递

    最近项目组安排将一组Lua实现的web服务端代码重构成Go实现,所以顺便学习了下Lua和Go,这里记录下在尝试重构的过程中遇到的几个问题. 1.beego路由设置 路由设置简单说下,主要是调用了pac ...

  5. softRestTemplate 2

    @SuppressWarnings("unchecked")    public User getUser(String id,String name) {        Soft ...

  6. codeforces 862 C. Mahmoud and Ehab and the xor(构造)

    题目链接:http://codeforces.com/contest/862/problem/C 题解:一道简单的构造题,一般构造题差不多都考自己脑补,脑洞一开就过了 由于数据x只有1e5,但是要求是 ...

  7. CF - 1110 C Meaningless Operations

    题目传送门 题解: 首先根据观察,很容易发的是: x != (1<<k) - 1 时候 答案就是, 将x二进制下再最高位后的0都变成1. 然后就是考虑 x == (1<<k) ...

  8. lightoj 1074 - Extended Traffic(spfa+负环判断)

    题目链接:http://www.lightoj.com/volume_showproblem.php?problem=1074 题意:有n个城市,每一个城市有一个拥挤度ai,从一个城市I到另一个城市J ...

  9. 解决问题:SpringMvc中转发的html文件中文是乱码

    目录 1.环境说明,以及前言 2.问题描述: 3.失败的方法(这里写失败并不代表在其他情况不管用) 3.1 html网页本身编码不是UTF-8(推荐尝试) 3.2 web.xml中没有设置配置编码方式 ...

  10. (2)RapidJson的详解及使用

        本节主要介绍RapidJson是如何使用的.   (1)RapidJson是什么 RapidJson是一个跨平台的c++的json的解析器和生成器: 相比较jsoncpp库,RapidJson ...