前言
分布式锁一般有三种实现方式: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>
加锁代码
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环境的前提下,进行测试。