最近工作也特别忙,很久没有进行一个自我学习了,今天特别的去学习了分布式锁,以下是我学习的一些总结,给您、也给我自己做个笔记!不喜勿喷,有理解不对的地方请留言。
首先,为什么我要学习这个分布式锁
在现在很多的系统开发,基本都会用到这个分布式锁,这也将是程序员必备的一个技能。
一.分布式锁的实现方式
加锁的方式有三种:基于redis、基于Zk、基于DB
1.基于redis实现分布式锁
这种也是目前使用的较多的一种方式。
需要再pom.xml中引入依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
方法一,使用SETNX实现分布式锁
public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {
// 第一步:加锁
Long result = jedis.setnx(lockKey, requestId);
if (result == 1) {
// 第二步:设置过期时间
jedis.expire(lockKey, expireTime);
}
}
代码解释:
- setnx命令,意思就是 set if not exist,如果lockKey不存在,把key存入Redis,保存成功后如果result返回1,表示设置成功,如果非1,表示失败,别的线程已经设置过了。
- expire(),设置过期时间,防止死锁,假设,如果一个锁set后,一直不删掉,那这个锁相当于一直存在,产生死锁
- 加锁总共分两步,第一步jedis.setnx,第二步jedis.expire设置过期时间,setnx与expire不是一个原子操作,如果程序执行完第一步后异常了,第二步jedis.expire(lockKey, expireTime)没有得到执行,相当于这个锁没有过期时间,有产生死锁的可能。正对这个问题如何改进?
改进如下:
public class RedisLockDemo {
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/** * 获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean getLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
// 两步合二为一,一行代码加锁并设置 + 过期时间。
if (1 == jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime)) {
return true;//加锁成功
}
return false;//加锁失败
}
}
解锁:使用del命令解锁
public static void unLock(Jedis jedis, String lockKey, String requestId) {
// 第一步: 使用 requestId 判断加锁与解锁是不是同一个客户端
if (requestId.equals(jedis.get(lockKey))) {
// 第二步: 若在此时,这把锁突然不是这个客户端的,则会误解锁
jedis.del(lockKey);
}
}
改进:
public class RedisTool {
private static final Long RELEASE_SUCCESS = 1L;
/** * 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
2.基于Zookeeper
虽然zookeeper的实现比较复杂,但是它提供的模型抽象却是非常简单的。Zookeeper提供一个多层级的节点命名空间(节点称为znode),每个节点都用一个以斜杠(/)分隔的路径表示,而且每个节点都有父节点(根节点除外),非常类似于文件系统。为了保证高吞吐和低延迟,在内存中维护了这个树状的目录结构,这种特性使得Zookeeper不能用于存放大量的数据,每个节点的存放数据上限为1M。
ZK中还有一种名为临时节点的节点,临时节点由某个客户端创建,当客户端与ZK集群断开连接,则该节点自动被删除。EPHEMERAL_SEQUENTIAL为临时顺序节点。
根据ZK中节点是否存在,可以作为分布式锁的锁状态,以此来实现一个分布式锁,下面是分布式锁的基本逻辑:
- 客户端调用create()方法创建名为“/dlm-locks/lockname/lock-”的临时顺序节点。
- 客户端调用getChildren(“lockname”)方法来获取所有已经创建的子节点。
- 客户端获取到所有子节点path之后,如果发现自己在步骤1中创建的节点是所有节点中序号最小的,就是看自己创建的序列号是否排第一,如果是第一,那么就认为这个客户端获得了锁,在它前面没有别的客户端拿到锁。
- 如果创建的节点不是所有节点中需要最小的,那么则监视比自己创建节点的序列号小的最大的节点,进入等待。直到下次监视的子节点变更的时候,再进行子节点的获取,判断是否获取锁。
- 拿到锁后执行自己的业务代码。
- 完成业务流程后,删除对应的子节点释放锁。
释放锁的过程相对比较简单,就是删除自己创建的那个子节点即可,不过也仍需要考虑删除节点失败等异常情况。
3.基于数据库实现
利用 Mysql 的锁表,创建一张表,设置一个 UNIQUE KEY 这个 KEY 就是要锁的 KEY,所以同一个 KEY 在mysql表里只能插入一次了,将KEY作为唯一性约束,这样对锁的竞争就交给了数据库,处理同一个 KEY 数据库保证了只有一个节点能插入成功,其他节点都会插入失败。
DB分布式锁的实现:通过主键id的唯一性进行加锁,说白了就是加锁的形式是向一张表中插入一条数据,该条数据的id就是一把分布式锁,例如当一次请求插入了一条id为1的数据,其他想要进行插入数据的并发请求必须等第一次请求执行完成后删除这条id为1的数据才能继续插入,实现了分布式锁的功能。
这样 lock 和 unlock 的思路就很简单了,伪代码:
def lock :
exec sql: insert into locked—table (xxx) values (xxx)
if result == true :
return true
else :
return false
def unlock :
exec sql: delete from lockedOrder where order_id='order_id'
总结
针对分布式锁的两种实现方法,使用哪种需要取决于业务场景,如果系统接口的读写操作完全是基于内存操作的,那显然使用Redis更合适,Mysql表锁or行锁明显不合适。同样是基于内存的 Redis锁 和 ZK锁具体选用哪一种,要根据是否有具体环境和架构师对哪种技术更为了解,原则就是选你最了解到,目的是能解决问题。