前言
分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。
可靠性
首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
代码实现
组件依赖
首先我们要通过Maven引入Jedis
开源组件,在pom.xml
文件加入下面的代码:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
加锁代码
package cn.hjf.redis.rediDemo.lock; import java.util.UUID; import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
/**
* redis setnx 实现分布式锁
* 真实环境下应该是在分布式多进程的情况下
* @author hjf
*/
public class SimpleLock
{
Jedis conn = new Jedis("127.0.0.1", 6379); private final static String LOCK_NAME = "lock"; // 获得锁 重入锁和非重入锁
// 设置超时时间
public String accequireLock(int timeOut){
// 随机生成一个uuid
String uuid = UUID.randomUUID().toString();
// 结束时间
long end = System.currentTimeMillis() + timeOut;
// 设置成功返回1 失败则返回0
// 当且仅当key不存在时将key设为value
// 若给定的key已经存在 那么setnx不会做任何操作
while(System.currentTimeMillis() < end){
// setnx()和expire()加起来不是一个原子操作
if(conn.setnx(LOCK_NAME, uuid).intValue() == 1){
// 增加redis的超时机制 一旦出现异常等情况可以自动去释放锁
conn.expire(LOCK_NAME, timeOut);
return uuid;
} // 检查是否设置超时机制(保证原子性)
if(conn.ttl(LOCK_NAME) == -1){
conn.expire(LOCK_NAME, timeOut);
} try
{
// 未获得锁时 休眠一段时间
Thread.sleep(1000);
} catch (InterruptedException e)
{
e.printStackTrace();
}
} return null;
} // 释放锁
public boolean releaseLock(String uuid){
while(true){
// 添加监听 一旦LOCK_NAME发生变化
// 那么下面的事务有效
conn.watch(LOCK_NAME);
if(uuid.equals(conn.get(LOCK_NAME))){
// 通过事务来操作
Transaction transaction = conn.multi();
transaction.del(LOCK_NAME);
// 执行失败的时候会返回null
if(transaction.exec() == null){
continue;
}
// 执行成功
return true;
}
// 取消监听
conn.unwatch();
break;
} return false;
} public static void main(String[] args)
{
// 单机测试环境下 可以采用手动去删除redis库中对应的LOCK_NAME
// 以便accequireLock可以获取到锁
SimpleLock simpleLock = new SimpleLock();
String uuid = simpleLock.accequireLock(10000);
if(null != uuid){
System.out.println("获取锁成功");
}else{
System.out.println("获取锁失败");
}
}
}
setnx()方法作用就是SET IF NOT EXIST,expire()方法就是给锁加一个过期时间。乍一看好像和前面的set()方法结果一样,然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁。网上之所以有人这样实现,是因为低版本的jedis并不支持多参数的set()方法。
可以在本地安装redis环境的前提下,进行测试。