使用Redis搭建简单的Java分布式锁

知识储备[SETNX]

抢占锁标记位置 SETNX key value

将 key 的值设为 value ,当且仅当 key 不存在。

若给定的 key 已经存在,则 SETNX 不做任何动作。

SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

设置成功,返回 1 。

设置失败,返回 0 。

更多详情请参考

这里我们用这样一个redis操作解决了抢占标记位置的问题,当一个程序的线程设置该标记位置的值后,其他的程序将不能在设置该标记位置。

do{      
   opt = _lock();//抢占锁表记位置,返回值现在我们只考虑两种OPT_WAIT[抢占失败进入等待],OPT_LOCK[抢占成功]
   if(opt==OPT_WAIT){//如果抢占标记位置为失败进入休眠
       sleep(timeout);
   }
}while (opt!=OPT_LOCK);//抢占成功退出抢占操作,进入加锁代码块

抢占锁标记失败该怎么办

我们应当让程序进入等待,Java中有很多方式让程序进入等待

Thread.sleep(long time);
TimeUnit.[UNIT].sleep(long time);
LockSupport.park(boolean isAbsolute, long time);//这个也许比较少见,有机会再写

 

抢占锁标志位成功的程序线程如何退出且通知其他抢占线程

上文中我们讲到了;没有抢占到标记位置的程序会进入等待,然后开始下一次循环,假设我们,抢占失败的程序休眠10s;而抢占成功的程序在它休眠的第5s中释放了所操作,我们应当怎么办;当然上面代码我们可以看到程序会在休眠结束后继续循环,再次调用抢占代码,但这5s中就相当于白白浪费了。所以我们需要有一个操作可以叫醒休眠的程序。换言之就是程序怎么去获取锁释放消息。

获取锁标记释放消息[PSUBSCRIBE与PUBLISH]

这里涉及到订阅与发布模式,就像你在淘宝看中一样东西,但是价格你暂时觉得有点偏高,那怎么办了,有种方式就是你每天去看一次,就是我们上面的循环,还有一种就是淘宝上面的降价通知,你勾选后;当商品降价的时候会发送一条消息给你。

这个操作就是我们这里要讲的。

SUBSCRIBE channel [channel ...]

订阅给定的一个或多个频道的信息。就相当于你勾选了一个或者多个商品的降价通知。这里我们可能会使用 PSUBSCRIBE pattern [pattern ...] 来简化订阅操作

 PUBLISH channel message

将信息 message 发送到指定的频道 channel 。就相当于淘宝给你发送降价通知。

更多命令详情请参考 PSUBSCRIBE  SUBSCRIBE  PUBLISH

namespace = "...";//在程序一开始我们定义一个命名空间;这个底下用来存放锁相关信息
notifier = new Thread(new Runnable() {//因为订阅时阻塞的。所以程序启动时建立一个通知线程专门来订阅消息
    public void run() {
       try(Jedis resource = jedisPool.getResource()) {
           resource.psubscribe(new LockPubSub(), namespace + '*');//订阅这个命名空间下所有消息
       }
    }
});

private int _lock() {
    Jedis resource = jedisPool.getResource();
    try {
        Long result = resource.setnx(lockKey, holder);//尝试抢占所标记
        if (Long.valueOf(1).equals(result)) {//抢占成功
            resource.incr(countKey);//锁计数器加一
            return OPT_LOCK;//返回加锁成功
        } else {
            String realHolder = resource.get(lockKey);//获取当前加锁的线程标记
            if (holder.equals(realHolder)) {//如果自己持有的锁标记和当前加锁的线程标记一致;则是重入
                resource.incr(countKey);//重入计数加一
                return OPT_LOCK;//返回加锁成功
            } else {
                resource.rpush(queueKey, holder);//加锁标记不一致,尝试将自己加入释放通知队列末尾
                if (resource.exists(lockKey)) {//如果放入队列之后加锁的线程还在操作;则进入真正的等待
                    return OPT_WAIT;
                } else {
                    if(holder.equals(resource.lindex(queueKey,0))) {//否则判断自己是不是等待队列的第一个元素
                        resource.lrem(queueKey, 1, holder);//如果是第一个;则重试一次;
                        return OPT_RETRY;
                    }else{
                        return OPT_WAIT;//否则进入真正的等待
                    }
                }
            }
        }
    } finally {
        resource.close();
    }
}

public void unLock() {
    Jedis resource = jedisPool.getResource();
    try {
        String realHolder = resource.get(lockKey);
        if(holder.equals(realHolder)){//判断锁标记是否与持有者一致;否则应当是程序异常;当抛出失败
            if(resource.decr(countKey)==0){//锁计数减一;如果为0;锁释放完成;
                resource.del(lockKey);//释放锁标记位置
                String first = resource.lpop(queueKey);//向最先订阅释放消息的程序发布释放消息
                if(first!=null) {
                    resource.publish(lockKey, first);
                }
            }
        }else{
            throw new IllegalStateException("holder|excepted [" + realHolder + "] | actual [" + holder + "]");
        }
    } finally {
        resource.close();
    }
}

//holder为线程标志
private void sleep(long timeout){
     waitMap.put(holder,Thread.currentThread());//当程序休眠时把添加到等待队列中去
     try {
          TimeUnit.MILLISECONDS.sleep(timeout);//程序休眠
     } catch (InterruptedException e) {
          //wake 唤醒;响应中断消息
     } finally {
          waitMap.remove(holder);//休眠结束;将线程移除出等待对列
     }
}


//用来建立锁订阅通知
class LockPubSub extends JedisPubSub {

    @Override
    public void onPMessage(String pattern, String channel, String message) {
        Thread first = waitMap.get(message);//message就是上文的holder
        if(first!=null){
            first.interrupt();//触发线程中断;打破休眠状态
        }
    }

}

完整代码


/**
*博客园地址:https://home.cnblogs.com/u/dev-lluo/
 *个人博客地址:https://little.beer/
*/
import
java.util.concurrent.TimeoutException; public interface Lock { void lock(); void tryLock(int count) throws TimeoutException; void unLock(); } public interface LockBuilder { Lock newLock(LockToken lockToken); } public interface LockToken { String getLockToken(); } import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPubSub; import java.lang.management.ManagementFactory; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; public class DistributedLockBuilder implements LockBuilder { private Thread notifier; private JedisPool jedisPool; private final String namespace; private final String HOLDER_PRE; private final int OPT_LOCK, OPT_RETRY, OPT_WAIT; private final Map waitMap; private int timeout; { notifier = new Thread(new Runnable() { public void run() { try (Jedis resource = jedisPool.getResource()) { resource.psubscribe(new LockPubSub(), namespace + '*'); } } }); waitMap = Collections.synchronizedMap(new HashMap()); Map env = System.getenv(); HOLDER_PRE = new StringBuilder(env.get("COMPUTERNAME")).append('[').append(env.get("USERNAME")).append(']').append(ManagementFactory.getRuntimeMXBean().getName()).toString(); OPT_LOCK = 0; OPT_RETRY = 1; OPT_WAIT = 2; } public DistributedLockBuilder(JedisPool jedisPool) { this(jedisPool, "__dtd_lock__:jedis"); } public DistributedLockBuilder(JedisPool jedisPool, String namespace) { this(jedisPool, namespace, 10); } public DistributedLockBuilder(JedisPool jedisPool, String namespace, int timeout) { this.jedisPool = jedisPool; this.namespace = namespace; this.timeout = timeout; notifier.start(); } public Lock newLock(LockToken lockToken) { return new Lock(lockToken); } public class Lock implements com.outlook.lluo.dev.lock.api.Lock { private LockToken lockToken; private String lockKey; private String countKey; private String queueKey; private String holder; private Lock(LockToken lockToken) { this.lockToken = lockToken; this.lockKey = new StringBuilder(namespace).append(':').append(lockToken.getLockToken()).append(":lock").toString(); this.countKey = new StringBuilder(namespace).append(':').append(lockToken.getLockToken()).append(":count").toString(); this.queueKey = new StringBuilder(namespace).append(':').append(lockToken.getLockToken()).append(":queue").toString(); this.holder = new StringBuilder(HOLDER_PRE).append('(').append(Thread.currentThread().getId()).append(")@").append(Thread.currentThread().getName()).toString(); } public void lock() { int opt; do { opt = _lock(); if (opt == OPT_WAIT) { sleep(timeout); } } while (opt != OPT_LOCK); } public void tryLock(int count) throws TimeoutException { int opt; do { opt = _lock(); if (count == 0) { Jedis resource = jedisPool.getResource(); try { resource.lrem(queueKey, 1, holder); throw new TimeoutException(holder); } finally { resource.close(); } } if (opt == OPT_WAIT) { sleep(10); count--; } } while (opt != OPT_LOCK); } private void sleep(long timeout) { waitMap.put(holder, Thread.currentThread()); try { TimeUnit.MILLISECONDS.sleep(timeout); } catch (InterruptedException e) { //wake } finally { waitMap.remove(holder); } } private int _lock() { Jedis resource = jedisPool.getResource(); try { Long result = resource.setnx(lockKey, holder); if (Long.valueOf(1).equals(result)) { resource.incr(countKey); return OPT_LOCK; } else { String realHolder = resource.get(lockKey); if (holder.equals(realHolder)) { resource.incr(countKey); return OPT_LOCK; } else { resource.rpush(queueKey, holder); if (resource.exists(lockKey)) { return OPT_WAIT; } else { if (holder.equals(resource.lindex(queueKey, 0))) { resource.lrem(queueKey, 1, holder); return OPT_RETRY; } else { return OPT_WAIT; } } } } } finally { resource.close(); } } public void unLock() { Jedis resource = jedisPool.getResource(); try { String realHolder = resource.get(lockKey); if (holder.equals(realHolder)) { if (resource.decr(countKey) == 0) { resource.del(lockKey); String first = resource.lpop(queueKey); if (first != null) { resource.publish(lockKey, first); } } } else { throw new IllegalStateException("holder|excepted [" + realHolder + "] | actual [" + holder + "]"); } } finally { resource.close(); } } @Override public String toString() { return "Lock{" + "lockToken=" + lockToken.getLockToken() + ", holder='" + holder + '\'' + '}'; } } class LockPubSub extends JedisPubSub { @Override public void onPMessage(String pattern, String channel, String message) { Thread first = waitMap.get(message); if (first != null) { first.interrupt(); } } } }

 

posted @ 2018-12-22 15:36  dev-lluo  阅读(458)  评论(0编辑  收藏  举报
个人博客:来点啤酒