一.目前主流的分布式锁三种实现方式:

1.通过zk实现。

2.通过数据库的乐观锁实现。

3.通过redis来实现。

 

 

 

二.作为一个分布式锁需要注意的4点:

  1. 互斥性:在任意深刻只有一个客户端中的一个线程能持有锁。
  2. 死锁 :  持锁的线程崩溃后也有机制让锁自动释放,保证不发生死锁。
  3. 容错性 :只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  4. 解铃还须系铃人。解锁的必须是上锁的。

 

基于以上4点 除了利用redis的高效存取,还需要借助lua脚本来保证上锁解锁的原子性。

那么接下来就看看如何用正确的姿势利用redis来实现分布式锁。

 

我们知道目前redis在java的客户端主要是借助Jedis或者RedisTemple来使用Redis。

①那么这两个客户端都不能很好的支持上锁时候的原子性,为了保证互斥性且不死锁,我们在操作redis的时候会去设置一个唯一key作为分布式锁,谁抢先设置了这个key,谁则获得锁,利用redis的 setNx命令实现, 其次为了保证不死锁,则需要给这个key设置一个过期时间expire,那么问题来了,如果在发送命令setNx成功之后,客户端突然宕机了,来不及发送设置key的expire命令,那么就会造成死锁,这个key永远删除不掉。那怎么做能够保证不会发生这种情况。需要借助lua脚本,我们知道redis是单线程的,如果我们能一次性将setNx和setExpire的命令发送给redis,redis在接收到命令能够保证串行执行命令就能保证上锁成功且不死锁。redis完美得支持了lua脚本,我们可以通过发送lua脚本给Redis,redis又是单线程的,在处理成功某个客户端发来的lua脚本才能处理其他客户端发来的请求命令,这样就能保证上锁和设置过期时间的原子性。(具体的代码实现会在后面给出)。

 

②第一步已经解决了互斥性和死锁的问题,对于容错性而言这是运维需要考虑的,如何保证Redis的高可用性。那么接下来这步就要去完成“解锁人必须是上锁人”的问题。 那么如何去实现,其实也很简单,我们还是需要利用lua脚本来进行完成。因为在删锁的时候也需要对redis进行两步操作

 1.判断此锁是否是自己上。

 2.是自己上的则删除此锁。

可能很多人回疑问我解锁也是在上锁成功之后再去解的锁,就算发生了宕机我也已经给锁已经设置了过期时间,为什么还要保证删锁的是上锁的人。那么我们不妨考虑下某个极端的场景, 客户端A的线程1上锁成功后并且设置了锁的时长为10S,突然发生了宕机,10S后锁自动消失,客户端B的线程1抢到了锁并且设置了锁的时长为10S,如果此时客户端A恢复并且继续执行,那么会去执行删锁的代码,如果删锁成功,那么客户端C就可以去抢锁并且抢锁成功,那么客户端B还未执行完代码就已经被抢走了锁就会造成资源混乱。所以为了避免这种极端现象就必须要保证解锁的是上锁的人。

如何去保证其实也很简单 上锁是利用了 setNx命令保证key的唯一性,我们忽略了还可以设置key对应的value。将此value设置为客户端的地址+线程名称 即可保证全局唯一性。在删锁代码里可以通过判断此时 锁对应的value是否为客户端的(客户端的地址+线程名称)即可判断是否可以删除。如果是则可以不是则不能删除。这里需要保证判断和删除的原子性(如果判断成功了突然又宕机了,过了一会儿恢复再去删也会造成混乱的情况)。所以还是要借助lua脚本来实现操作原子性。

 

废话不多说了上源码。我这边是借用redisTemple来实现的,其实用jedis也一样。只需要保证lua脚本的正确性即可。

 

/**
 * @Author zhh
 * @Date 2019/4/10 15:31
 * @Description
 */


import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Component
public class RedisUtil {

    private static String hostName = null;

    private static DefaultRedisScript<String> lockRedisScript;

    private static DefaultRedisScript<String> releaseLockRedisScript;

    private static RedisSerializer<String> argsSerializer;

    private static RedisSerializer resultSerializer;

    private final static String REDIS_OK = "1";

    static {
        try {
            hostName = InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
        if (null == hostName) {
            hostName = UUID.randomUUID().toString();
        }
        lockRedisScript = new DefaultRedisScript<>();
        lockRedisScript.setResultType(String.class);
        releaseLockRedisScript = new DefaultRedisScript<>();
        releaseLockRedisScript.setResultType(String.class);
        argsSerializer = new StringRedisSerializer();
        resultSerializer = new StringRedisSerializer();

        /*
         * 上锁lua脚本,为了setnx和setExpire的原子性将两个命令通过lua脚本发送给redis-server
         *   key为锁名称,value为 hostname+线程ID作为唯一标识,删锁的时候必须保证是上锁的那台服务器的那条线程。
         * */
        String lockScript = "if redis.call('setnx',KEYS[1],ARGV[1])==1  then  if redis.call('pexpire', KEYS[1],ARGV[2])==1 then return '1' end   end return '0'";
        lockRedisScript.setScriptText(lockScript);


        /*
         * 解锁lua脚本。
         *
         * */
        String releaseScirpt = "if redis.call('get',KEYS[1])==ARGV[1] then if redis.call('del',KEYS[1])==1 then return '1' end  end return '0'";
        releaseLockRedisScript.setScriptText(releaseScirpt);

    }


    @Resource(name = "stringRedisTemplate")
    private RedisTemplate<String, String> redisTemplate;


    /**
     * 上锁代码
     *
     * @param key        为锁名称
     * @param expireTime 过期时间
     * @param timeUnit   时间单位自行选择
     */
    public boolean getLock(String key, long expireTime, TimeUnit timeUnit) {
        long time = timeUnit.toMillis(expireTime);
        String value = hostName + Thread.currentThread().getName();
        String result = redisTemplate.execute(lockRedisScript, argsSerializer, resultSerializer, Collections.singletonList(key), value, String.valueOf(time));
        return REDIS_OK.equals(result);
    }

    /**
     * 解锁代码
     *
     * @param key 为锁名称
     */
    public boolean relaseLock(String key) {
        String value = hostName + Thread.currentThread().getName();
        String result = redisTemplate.execute(releaseLockRedisScript, argsSerializer, resultSerializer, Collections.singletonList(key), value);
        return REDIS_OK.equals(result);

    }


}

最重要的就是lua脚本写对。

上锁的lua脚本:

"if redis.call('setnx',KEYS[1],ARGV[1])==1  then  if redis.call('pexpire', KEYS[1],ARGV[2])==1 then return '1' end   end return '0'";

解锁的lua脚本:

"if redis.call('get',KEYS[1])==ARGV[1] then if redis.call('del',KEYS[1])==1 then return '1' end  end return '0'";

    上述代码复制皆可用,项目中使用的时候将redisTemple相关的参数配置好即可使用,如果使用的是Jdeis的客户端也一样。因为主要是lua脚本要写对!!!!。

  (这个lua脚本写了我很久。。。之前没写过,各种倒腾。哭了!!!)