Java实现基于Redis的分布式锁

来源:互联网 发布:flash player mac下载 编辑:程序博客网 时间:2024/05/21 10:09

Java实现基于Redis的分布式锁

 单JVM内同步好办, 直接用JDK提供的锁就可以了,但是跨进程同步靠这个肯定是不可能的,这种情况下肯定要借助第三方,我这里实现用Redis,当然还有很多其他的实现方式。其实基于Redis实现的原理还算比较简单的,在看代码之前建议大家先去这里看看原理,我就不翻译了,免得变味了,看懂了之后看代码应该就容易理解了。

 

时间统一问题:各个客户端加锁时需要获取时间,而这个时间都不应当从本地获取,因为各个客户端的时间并不是一致的,因此需要提供一个TimeServer提供获取时间的服务,下面源码中用到的关于时间服务的三个类(包括TimeServer、TimeClient和Time Client Exception)会在另一篇博客《Java NIO时间服务》给源码.

 

我这里不实现JDK的java.util.concurrent.locks.Lock接口,而是自定义一个,因为JDK的有个newCondition()方法我这里暂时没实现。这个Lock提供了5个lock方法的变体,可以自行选择使用哪一个来获取锁,我的想法是

最好用带超时返回的那几个方法,因为不这样的话,假如redis挂了,线程永远都在那死循环了(关于这里,应该还可以进一步优化,如果redis挂了,Jedis的操作肯定会抛异常之类的,可以定义个机制让redis挂了的时候通知使用这个lock的用户,或者说是线程)。

 

 

Java代码 
  1. package cc.lixiaohui.lock;  
  2.   
  3. import java.util.concurrent.TimeUnit;  
  4.   
  5. public interface Lock extends Releasable{  
  6.   
  7.     /** 
  8.      * 阻塞性的获取锁, 不响应中断 
  9.      */  
  10.     void lock();  
  11.       
  12.     /** 
  13.      * 阻塞性的获取锁, 响应中断 
  14.      *  
  15.      * @throws InterruptedException 
  16.      */  
  17.     void lockInterruptibly() throws InterruptedException;  
  18.       
  19.     /** 
  20.      * 尝试获取锁, 获取不到立即返回, 不阻塞 
  21.      */  
  22.     boolean tryLock();  
  23.       
  24.     /** 
  25.      * 超时自动返回的阻塞性的获取锁, 不响应中断 
  26.      *  
  27.      * @param time 
  28.      * @param unit 
  29.      * @return {@code true} 若成功获取到锁, {@code false} 若在指定时间内未获取到锁 
  30.      *          
  31.      */  
  32.     boolean tryLock(long time, TimeUnit unit);  
  33.       
  34.     /** 
  35.      * 超时自动返回的阻塞性的获取锁, 响应中断 
  36.      *  
  37.      * @param time 
  38.      * @param unit 
  39.      * @return {@code true} 若成功获取到锁, {@code false} 若在指定时间内未获取到锁 
  40.      * @throws InterruptedException 在尝试获取锁的当前线程被中断 
  41.      */  
  42.     boolean tryLockInterruptibly(long time, TimeUnit unit) throws InterruptedException;  
  43.       
  44.     /** 
  45.      * 释放锁 
  46.      */  
  47.     void unlock();  
  48.       
  49. }  

 

Releasable.java :

Java代码 
  1. package cc.lixiaohui.lock;  
  2.   
  3. /** 
  4.  * 代表持有资源的对象, 例如 
  5.  * <ul> 
  6.  * <li> 基于jedis的锁自然持有与redis server的连接 </li> 
  7.  * <li> 基于时间统一的的锁自然持有与time server的连接</li> 
  8.  * </ul> 
  9.  * 因此锁应该实现该接口, 并在{@link Releasable#resease() release} 方法中释放相关的连接 
  10.  *  
  11.  * @author lixiaohui 
  12.  * 
  13.  */  
  14. public interface Releasable {  
  15.       
  16.     /** 
  17.      * 释放持有的所有资源 
  18.      */  
  19.     void release();  
  20.       
  21. }  

 

看Lock的抽象实现:

 

Java代码 
  1. package cc.lixiaohui.lock;  
  2.   
  3. import java.util.concurrent.TimeUnit;  
  4.   
  5. /** 
  6.  * 锁的骨架实现, 真正的获取锁的步骤由子类去实现. 
  7.  *  
  8.  * @author lixiaohui 
  9.  * 
  10.  */  
  11. public abstract class AbstractLock implements Lock {  
  12.   
  13.     /** 
  14.      * <pre> 
  15.      * 这里需不需要保证可见性值得讨论, 因为是分布式的锁,  
  16.      * 1.同一个jvm的多个线程使用不同的锁对象其实也是可以的, 这种情况下不需要保证可见性  
  17.      * 2.同一个jvm的多个线程使用同一个锁对象, 那可见性就必须要保证了. 
  18.      * </pre> 
  19.      */  
  20.     protected volatile boolean locked;  
  21.   
  22.     /** 
  23.      * 当前jvm内持有该锁的线程(if have one) 
  24.      */  
  25.     private Thread exclusiveOwnerThread;  
  26.   
  27.     public void lock() {  
  28.         try {  
  29.             lock(false0nullfalse);  
  30.         } catch (InterruptedException e) {  
  31.             // TODO ignore  
  32.         }  
  33.     }  
  34.   
  35.     public void lockInterruptibly() throws InterruptedException {  
  36.         lock(false0nulltrue);  
  37.     }  
  38.   
  39.     public boolean tryLock(long time, TimeUnit unit) {  
  40.         try {  
  41.             return lock(true, time, unit, false);  
  42.         } catch (InterruptedException e) {  
  43.             // TODO ignore  
  44.         }  
  45.         return false;  
  46.     }  
  47.   
  48.     public boolean tryLockInterruptibly(long time, TimeUnit unit) throws InterruptedException {  
  49.         return lock(true, time, unit, true);  
  50.     }  
  51.   
  52.     public void unlock() {  
  53.         // TODO 检查当前线程是否持有锁  
  54.         if (Thread.currentThread() != getExclusiveOwnerThread()) {  
  55.             throw new IllegalMonitorStateException("current thread does not hold the lock");  
  56.         }  
  57.           
  58.         unlock0();  
  59.         setExclusiveOwnerThread(null);  
  60.     }  
  61.   
  62.     protected void setExclusiveOwnerThread(Thread thread) {  
  63.         exclusiveOwnerThread = thread;  
  64.     }  
  65.   
  66.     protected final Thread getExclusiveOwnerThread() {  
  67.         return exclusiveOwnerThread;  
  68.     }  
  69.   
  70.     protected abstract void unlock0();  
  71.       
  72.     /** 
  73.      * 阻塞式获取锁的实现 
  74.      *  
  75.      * @param useTimeout  
  76.      * @param time 
  77.      * @param unit 
  78.      * @param interrupt 是否响应中断 
  79.      * @return 
  80.      * @throws InterruptedException 
  81.      */  
  82.     protected abstract boolean lock(boolean useTimeout, long time, TimeUnit unit, boolean interrupt) throws InterruptedException;  
  83.   
  84. }  

 

 基于Redis的最终实现(not reentrant),关键的获取锁,释放锁的代码在这个类的lock方法和unlock0方法里,大家可以只看这两个方法然后完全自己写一个:

 

Java代码 
  1. package cc.lixiaohui.lock;  
  2.   
  3. import java.io.IOException;  
  4. import java.net.SocketAddress;  
  5. import java.util.concurrent.TimeUnit;  
  6.   
  7. import redis.clients.jedis.Jedis;  
  8. import cc.lixiaohui.lock.time.nio.client.TimeClient;  
  9.   
  10. /** 
  11.  * <pre> 
  12.  * 基于Redis的SETNX操作实现的分布式锁 
  13.  *  
  14.  * 获取锁时最好用lock(long time, TimeUnit unit), 以免网路问题而导致线程一直阻塞 
  15.  *  
  16.  * <a href="http://redis.io/commands/setnx">SETNC操作参考资料</a> 
  17.  * </pre> 
  18.  *  
  19.  * @author lixiaohui 
  20.  * 
  21.  */  
  22. public class RedisBasedDistributedLock extends AbstractLock {  
  23.       
  24.     private Jedis jedis;  
  25.       
  26.     private TimeClient timeClient;  
  27.       
  28.     // 锁的名字  
  29.     protected String lockKey;  
  30.       
  31.     // 锁的有效时长(毫秒)  
  32.     protected long lockExpires;  
  33.       
  34.     public RedisBasedDistributedLock(Jedis jedis, String lockKey, long lockExpires, SocketAddress timeServerAddr) throws IOException {  
  35.         this.jedis = jedis;  
  36.         this.lockKey = lockKey;  
  37.         this.lockExpires = lockExpires;  
  38.         timeClient = new TimeClient(timeServerAddr);  
  39.     }  
  40.       
  41.     // 阻塞式获取锁的实现  
  42.     protected boolean lock(boolean useTimeout, long time, TimeUnit unit, boolean interrupt) throws InterruptedException{  
  43.         if (interrupt) {  
  44.             checkInterruption();  
  45.         }  
  46.           
  47.         // 超时控制 的时间可以从本地获取, 因为这个和锁超时没有关系, 只是一段时间区间的控制  
  48.         long start = localTimeMillis();  
  49.         long timeout = unit.toMillis(time); // if !useTimeout, then it's useless  
  50.           
  51.         while (useTimeout ? isTimeout(start, timeout) : true) {  
  52.             if (interrupt) {  
  53.                 checkInterruption();  
  54.             }  
  55.               
  56.             long lockExpireTime = serverTimeMillis() + lockExpires + 1;//锁超时时间  
  57.             String stringOfLockExpireTime = String.valueOf(lockExpireTime);  
  58.               
  59.             if (jedis.setnx(lockKey, stringOfLockExpireTime) == 1) { // 获取到锁  
  60.                 // TODO 成功获取到锁, 设置相关标识  
  61.                 locked = true;  
  62.                 setExclusiveOwnerThread(Thread.currentThread());  
  63.                 return true;  
  64.             }  
  65.               
  66.             String value = jedis.get(lockKey);  
  67.             if (value != null && isTimeExpired(value)) { // lock is expired  
  68.                 // 假设多个线程(非单jvm)同时走到这里  
  69.                 String oldValue = jedis.getSet(lockKey, stringOfLockExpireTime); // getset is atomic  
  70.                 // 但是走到这里时每个线程拿到的oldValue肯定不可能一样(因为getset是原子性的)  
  71.                 // 加入拿到的oldValue依然是expired的,那么就说明拿到锁了  
  72.                 if (oldValue != null && isTimeExpired(oldValue)) {  
  73.                     // TODO 成功获取到锁, 设置相关标识  
  74.                     locked = true;  
  75.                     setExclusiveOwnerThread(Thread.currentThread());  
  76.                     return true;  
  77.                 }  
  78.             } else {   
  79.                 // TODO lock is not expired, enter next loop retrying  
  80.             }  
  81.         }  
  82.         return false;  
  83.     }  
  84.       
  85.     public boolean tryLock() {  
  86.         long lockExpireTime = serverTimeMillis() + lockExpires + 1;//锁超时时间  
  87.         String stringOfLockExpireTime = String.valueOf(lockExpireTime);  
  88.           
  89.         if (jedis.setnx(lockKey, stringOfLockExpireTime) == 1) { // 获取到锁  
  90.             // TODO 成功获取到锁, 设置相关标识  
  91.             locked = true;  
  92.             setExclusiveOwnerThread(Thread.currentThread());  
  93.             return true;  
  94.         }  
  95.           
  96.         String value = jedis.get(lockKey);  
  97.         if (value != null && isTimeExpired(value)) { // lock is expired  
  98.             // 假设多个线程(非单jvm)同时走到这里  
  99.             String oldValue = jedis.getSet(lockKey, stringOfLockExpireTime); // getset is atomic  
  100.             // 但是走到这里时每个线程拿到的oldValue肯定不可能一样(因为getset是原子性的)  
  101.             // 假如拿到的oldValue依然是expired的,那么就说明拿到锁了  
  102.             if (oldValue != null && isTimeExpired(oldValue)) {  
  103.                 // TODO 成功获取到锁, 设置相关标识  
  104.                 locked = true;  
  105.                 setExclusiveOwnerThread(Thread.currentThread());  
  106.                 return true;  
  107.             }  
  108.         } else {   
  109.             // TODO lock is not expired, enter next loop retrying  
  110.         }  
  111.           
  112.         return false;  
  113.     }  
  114.       
  115.     /** 
  116.      * Queries if this lock is held by any thread. 
  117.      *  
  118.      * @return {@code true} if any thread holds this lock and 
  119.      *         {@code false} otherwise 
  120.      */  
  121.     public boolean isLocked() {  
  122.         if (locked) {  
  123.             return true;  
  124.         } else {  
  125.             String value = jedis.get(lockKey);  
  126.             // TODO 这里其实是有问题的, 想:当get方法返回value后, 假设这个value已经是过期的了,  
  127.             // 而就在这瞬间, 另一个节点set了value, 这时锁是被别的线程(节点持有), 而接下来的判断  
  128.             // 是检测不出这种情况的.不过这个问题应该不会导致其它的问题出现, 因为这个方法的目的本来就  
  129.             // 不是同步控制, 它只是一种锁状态的报告.  
  130.             return !isTimeExpired(value);  
  131.         }  
  132.     }  
  133.   
  134.     @Override  
  135.     protected void unlock0() {  
  136.         // TODO 判断锁是否过期  
  137.         String value = jedis.get(lockKey);  
  138.         if (!isTimeExpired(value)) {  
  139.             doUnlock();  
  140.         }  
  141.     }  
  142.       
  143.     public void release() {  
  144.         jedis.close();  
  145.         timeClient.close();  
  146.     }  
  147.       
  148.     private void doUnlock() {  
  149.         jedis.del(lockKey);  
  150.     }  
  151.   
  152.     private void checkInterruption() throws InterruptedException {  
  153.         if(Thread.currentThread().isInterrupted()) {  
  154.             throw new InterruptedException();  
  155.         }  
  156.     }  
  157.       
  158.     private boolean isTimeExpired(String value) {  
  159.         // 这里拿服务器的时间来比较  
  160.         return Long.parseLong(value) < serverTimeMillis();  
  161.     }  
  162.       
  163.     private boolean isTimeout(long start, long timeout) {  
  164.         // 这里拿本地的时间来比较  
  165.         return start + timeout > System.currentTimeMillis();  
  166.     }  
  167.       
  168.     private long serverTimeMillis(){  
  169.         return timeClient.currentTimeMillis();  
  170.     }  
  171.       
  172.     private long localTimeMillis() {  
  173.         return System.currentTimeMillis();  
  174.     }  
  175.       
  176. }  

 

如果将来还换一种实现方式(比如zookeeper之类的),到时直接继承AbstractLock并实现lock(boolean useTimeout, long time, TimeUnit unit, boolean interrupt), unlock0()方法即可(所谓抽象嘛)

 

测试

 

模拟全局ID增长器,设计一个IDGenerator类,该类负责生成全局递增ID,其代码如下:

 

Java代码 
  1. package cc.lixiaohui.lock.example;  
  2.   
  3. import java.math.BigInteger;  
  4. import java.util.concurrent.TimeUnit;  
  5.   
  6. import cc.lixiaohui.lock.Lock;  
  7. import cc.lixiaohui.lock.Releasable;  
  8.   
  9. /** 
  10.  * 模拟分布式环境中的ID生成  
  11.  * @author lixiaohui 
  12.  * 
  13.  */  
  14. public class IDGenerator implements Releasable{  
  15.   
  16.     private static BigInteger id = BigInteger.valueOf(0);  
  17.   
  18.     private final Lock lock;  
  19.   
  20.     private static final BigInteger INCREMENT = BigInteger.valueOf(1);  
  21.   
  22.     public IDGenerator(Lock lock) {  
  23.         this.lock = lock;  
  24.     }  
  25.       
  26.     public String getAndIncrement() {  
  27.         if (lock.tryLock(3, TimeUnit.SECONDS)) {  
  28.             try {  
  29.                 // TODO 这里获取到锁, 访问临界区资源  
  30.                 System.out.println(Thread.currentThread().getName() + " get lock");  
  31.                 return getAndIncrement0();  
  32.             } finally {  
  33.                 lock.unlock();  
  34.             }  
  35.         }  
  36.         return null;  
  37.         //return getAndIncrement0();  
  38.     }  
  39.       
  40.     public void release() {  
  41.         lock.release();  
  42.     }  
  43.   
  44.     private String getAndIncrement0() {  
  45.         String s = id.toString();  
  46.         id = id.add(INCREMENT);  
  47.         return s;  
  48.     }  
  49. }  

 

测试主逻辑:同一个JVM内开两个线程死循环地(循环之间无间隔,有的话测试就没意义了)获取ID(我这里并不是死循环而是跑20s),获取到ID存到同一个Set里面,在存之前先检查该ID在set中是否存在,如果已存在,则让两个线程都停止。如果程序能正常跑完20s,那么说明这个分布式锁还算可以满足要求,如此测试的效果应该和不同JVM(也就是真正的分布式环境中)测试的效果是一样的,下面是测试类的代码:

 

Java代码 
  1. package cc.lixiaohui.DistributedLock.DistributedLock;  
  2.   
  3. import java.net.InetSocketAddress;  
  4. import java.net.SocketAddress;  
  5. import java.util.HashSet;  
  6. import java.util.Set;  
  7.   
  8. import org.junit.Test;  
  9.   
  10. import redis.clients.jedis.Jedis;  
  11. import cc.lixiaohui.lock.Lock;  
  12. import cc.lixiaohui.lock.RedisBasedDistributedLockV0_0;  
  13. import cc.lixiaohui.lock.RedisBasedDistributedLock;  
  14. import cc.lixiaohui.lock.example.IDGenerator;  
  15.   
  16. public class IDGeneratorTest {  
  17.       
  18.     private static Set<String> generatedIds = new HashSet<String>();  
  19.       
  20.     private static final String LOCK_KEY = "lock.lock";  
  21.     private static final long LOCK_EXPIRE = 5 * 1000;  
  22.       
  23.     @Test  
  24.     public void testV1_0() throws Exception {  
  25.           
  26.         SocketAddress addr = new InetSocketAddress("localhost"9999);  
  27.           
  28.         Jedis jedis1 = new Jedis("localhost"6379);  
  29.         Lock lock1 = new RedisBasedDistributedLock(jedis1, LOCK_KEY, LOCK_EXPIRE, addr);  
  30.         IDGenerator g1 = new IDGenerator(lock1);  
  31.         IDConsumeTask consume1 = new IDConsumeTask(g1, "consume1");  
  32.           
  33.         Jedis jedis2 = new Jedis("localhost"6379);  
  34.         Lock lock2 = new RedisBasedDistributedLock(jedis2, LOCK_KEY, LOCK_EXPIRE, addr);  
  35.         IDGenerator g2 = new IDGenerator(lock2);  
  36.         IDConsumeTask consume2 = new IDConsumeTask(g2, "consume2");  
  37.           
  38.         Thread t1 = new Thread(consume1);  
  39.         Thread t2 = new Thread(consume2);  
  40.         t1.start();  
  41.         t2.start();  
  42.           
  43.         Thread.sleep(20 * 1000); //让两个线程跑20秒  
  44.           
  45.         IDConsumeTask.stopAll();  
  46.           
  47.         t1.join();  
  48.         t2.join();  
  49.     }  
  50.       
  51.     static String time() {  
  52.         return String.valueOf(System.currentTimeMillis() / 1000);  
  53.     }  
  54.       
  55.     static class IDConsumeTask implements Runnable {  
  56.   
  57.         private IDGenerator idGenerator;  
  58.           
  59.         private String name;  
  60.           
  61.         private static volatile boolean stop;  
  62.           
  63.         public IDConsumeTask(IDGenerator idGenerator, String name) {  
  64.             this.idGenerator = idGenerator;  
  65.             this.name = name;  
  66.         }  
  67.           
  68.         public static void stopAll() {  
  69.             stop = true;  
  70.         }  
  71.           
  72.         public void run() {  
  73.             System.out.println(time() + ": consume " + name + " start ");  
  74.             while (!stop) {  
  75.                 String id = idGenerator.getAndIncrement();  
  76.                 if (id != null) {  
  77.                     if(generatedIds.contains(id)) {  
  78.                         System.out.println(time() + ": duplicate id generated, id = " + id);  
  79.                         stop = true;  
  80.                         continue;  
  81.                     }   
  82.                       
  83.                     generatedIds.add(id);  
  84.                     System.out.println(time() + ": consume " + name + " add id = " + id);  
  85.                 }  
  86.             }  
  87.             // 释放资源  
  88.             idGenerator.release();  
  89.             System.out.println(time() + ": consume " + name + " done ");  
  90.         }  
  91.           
  92.     }  
  93.       
  94. }  

 

说明一点,我这里停止两个线程的方式并不是很好,我是为了方便才这么做的,因为只是测试,最好不要这么做。

 

测试结果

跑20s打印的东西太多,前面打印的被clear了,只有差不多跑完的时候才有,下面截图。说明了这个锁能正常工作:

 

 

当IDGererator没有加锁(即IDGererator的getAndIncrement方法内部获取id时不上锁)时,测试是不通过的,非常大的概率中途就会停止,下面是不加锁时的测试结果:

 

这个1秒都不到:

 
这个也1秒都不到:


 

 


0 0
原创粉丝点击