前言

分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。

 

可靠性

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

 

代码实现

组件依赖

首先我们要通过Maven引入Jedis开源组件,在pom.xml文件加入下面的代码:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

 

加锁代码

 1 package cn.hjf.redis.rediDemo.lock;
 2 
 3 import java.util.UUID;
 4 
 5 import redis.clients.jedis.Jedis;
 6 import redis.clients.jedis.Transaction;
 7 /**
 8  * redis setnx 实现分布式锁
 9  * 真实环境下应该是在分布式多进程的情况下
10  * @author hjf
11  */
12 public class SimpleLock
13 {
14     Jedis conn = new Jedis("127.0.0.1", 6379);
15     
16     private final static String LOCK_NAME = "lock";
17     
18     // 获得锁  重入锁和非重入锁 
19     // 设置超时时间 
20     public String accequireLock(int timeOut){
21         // 随机生成一个uuid
22         String uuid = UUID.randomUUID().toString();
23         // 结束时间 
24         long end = System.currentTimeMillis() + timeOut;
25         // 设置成功返回1  失败则返回0  
26         // 当且仅当key不存在时将key设为value
27         // 若给定的key已经存在 那么setnx不会做任何操作
28         while(System.currentTimeMillis() < end){
29             // setnx()和expire()加起来不是一个原子操作
30             if(conn.setnx(LOCK_NAME, uuid).intValue() == 1){
31                 // 增加redis的超时机制 一旦出现异常等情况可以自动去释放锁
32                 conn.expire(LOCK_NAME, timeOut);   
33                 return uuid;
34             }
35             
36             // 检查是否设置超时机制(保证原子性)
37             if(conn.ttl(LOCK_NAME) == -1){  
38                 conn.expire(LOCK_NAME, timeOut);
39             }
40             
41             try
42             {
43                 // 未获得锁时 休眠一段时间
44                 Thread.sleep(1000);
45             } catch (InterruptedException e)
46             {
47                 e.printStackTrace();
48             }
49         }
50         
51         return null;
52     }
53     
54     // 释放锁
55     public boolean releaseLock(String uuid){
56         while(true){
57             // 添加监听 一旦LOCK_NAME发生变化
58             // 那么下面的事务有效
59             conn.watch(LOCK_NAME);
60             if(uuid.equals(conn.get(LOCK_NAME))){
61                 // 通过事务来操作
62                 Transaction transaction = conn.multi();
63                 transaction.del(LOCK_NAME);
64                 // 执行失败的时候会返回null
65                 if(transaction.exec() == null){
66                     continue;
67                 }
68                 // 执行成功
69                 return true;
70             }
71             // 取消监听
72             conn.unwatch();
73             break;
74         }
75         
76         return false;
77     }
78     
79     public static void main(String[] args)
80     {
81         // 单机测试环境下 可以采用手动去删除redis库中对应的LOCK_NAME
82         // 以便accequireLock可以获取到锁
83         SimpleLock simpleLock = new SimpleLock();
84         String uuid = simpleLock.accequireLock(10000);
85         if(null != uuid){
86             System.out.println("获取锁成功");
87         }else{
88             System.out.println("获取锁失败");
89         }
90     }
91 }

setnx()方法作用就是SET IF NOT EXIST,expire()方法就是给锁加一个过期时间。乍一看好像和前面的set()方法结果一样,然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁。网上之所以有人这样实现,是因为低版本的jedis并不支持多参数的set()方法。

可以在本地安装redis环境的前提下,进行测试。