使用SETNX实现分布式锁

用SETNX实现分布式锁 

利用SETNX非常简单地实现分布式锁。例如:某客户端要获得一个名字foo的锁,客户端使用下面的命令进行获取:
1
如返回0,表明该锁已被其他客户端取得,这时我们可以先返回或进行重试等对方完成或等待锁超时。
上面的锁定逻辑有一个问题:如果一个持有锁的客户端失败或崩溃了不能释放锁,该怎么解决?我们可以通过锁的键对应的时间戳来判断这种情况是否发生了,如果当前的时间已经大于lock.foo的值,说明该锁已失效,可以被重新使用。
C0操作超时了,但它还持有着锁,C1和C2读取lock.foo检查时间戳,先后发现超时了。 

C1 发送DEL lock.foo 

C1 发送SETNX lock.foo 并且成功了。 

C2 发送DEL lock.foo 

C2 发送SETNX lock.foo 并且成功了。 

这样一来,C1,C2都拿到了锁!问题大了!
C3发送SETNX lock.foo 想要获得锁,由于C0还持有锁,所以Redis返回给C3一个0 

C3发送GET lock.foo 以检查锁是否超时了,如果没超时,则等待或重试。 

反之,如果已超时,C3通过下面的操作来尝试获得锁:
1
注意 :为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。
public class Demo1 {

    private static final String lockKey = "Lock.TecentIm_Interface_Counter" ;
    private static final Integer lock_Timeout = 3 ;

    @Autowired
    private RedisTemplate redisTemplate;

    void work() {
        try {
            JedisCmd jedisCmd = redisTemplate.getJedisCmd();

            long lock = 0 ;
            long currentTime = 0 ;
            String lockValue = null ;

            while (lock != 1 ){
                currentTime = new Date().getTime();
                lockValue = String.valueOf(currentTime + lock_Timeout + 1 );
                lock = jedisCmd.setnx(lockKey, lockValue);

                if (lock == 1 || (currentTime > Long.valueOf(jedisCmd.get(lockKey)) && currentTime > Long.valueOf(jedisCmd.getSet(lockKey, lockValue)))){
                    break ;
                } else {
                    Thread.sleep( 10 );
                }
            }

            ## Do your job

            currentTime = new Date().getTime();
            if (currentTime < Long.valueOf(jedisCmd.get(lockKey))){
                jedisCmd.del(lockKey);
            }

        } catch (Exception e){
            jobLog.error(e);
        }
    }
}
2

在项目中使用了这些代码,老大看后,说逻辑没问题,给出了一些建议: 

1. 最好使用面向对象的方式解决; 

2. while()中的代码阅读起来比较吃力;
public class Demo2 {

    private static final String lockKey = "Lock.TecentIm_Interface_Counter" ;

    @Autowired
    private DistributedLockHandler distributedLockHandler;


    @Override
    void work() {
        try {
            Long time = System.currentTimeMillis() + 5 * 1000 ;
            boolean lock = redisLockHandler.getLock(lockKey, time);

            if (lock){
                ### Do your job


        } catch (Exception e){
            logger.error(e);
        } finally {
            redisLockHandler.realseLock(lockKey);
        }
    }

RedisLockHandler.java
版本三: 

DistributedLockHandler.java

63
版本四:
DistributedLockHandler.java

心得:

SETNX lock.foo <current Unix time + lock timeout + 1 >
如返回1,则该客户端获得锁,把lock.foo的键值设置为时间值表示该键已被锁定,该客户端最后可以通过DEL lock.foo来释放该锁。
解决死锁
发生这种情况时,可不能简单的通过DEL来删除锁,然后再SETNX一次,当多个客户端检测到锁超时后都会尝试去释放它,这里就可能出现一个竞态条件,让我们模拟一下这个场景:
幸好这种问题是可以避免的,让我们来看看C3这个客户端是怎样做的:
GETSET lock.foo <current Unix time + lock timeout + 1 >
通过GETSET,C3拿到的时间戳如果仍然是超时的,那就说明,C3如愿以偿拿到锁了。 

如果在C3之前,有个叫C4的客户端比C3快一步执行了上面的操作,那么C3拿到的时间戳是个未超时的值,这时,C3没有如期获得锁,需要再次等待或重试。留意一下,尽管C3没拿到锁,但它改写了C4设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。

代码示例1(最初始版本)

其实以上两点都是从代码的可读性以及可扩展性上说的; 

所以有了以下的版本二:


@Service ( "redisLockHandler" )
public class RedisLockHandler   {


    private static final Integer Lock_Timeout = 3 ;

    @Autowired
    private RedisTemplate redisTemplate;

    public boolean getLock (String lockKey, Long time){

        try {
            JedisCmd jedisCmd = redisTemplate.getJedisCmd();

            long currentTime = System.currentTimeMillis();
            String lockValue = String.valueOf(currentTime + Lock_Timeout + 1 );
            Long result = jedisCmd.setnx(lockKey, lockValue);

            if (result == 1 || checkIfTimeout(currentTime, lockKey, lockValue, jedisCmd)){
                return true ;
            } else {
                Thread.sleep( 10 );
                if (currentTime > time){
                    return false ;
                } else {
                    return getLock(lockKey, time);
                }
            }

        } catch (Exception e){
            jobLog.info( "Failed to run RedisLockHandler.getLock method." ,e);
            return false ;
        }
    }


    public void realseLock (String lockKey){
        JedisCmd jedisCmd = redisTemplate.getJedisCmd();
        if (System.currentTimeMillis() < Long.valueOf(jedisCmd.get(lockKey))){
            jedisCmd.del(lockKey);
        }
    }

    public boolean checkIfTimeout (Long currentTime, String lockKey, String lockValue, JedisCmd jedisCmd){
        Long value2long_1 = Long.valueOf(jedisCmd.get(lockKey));
        Long value2long_2 = Long.valueOf(jedisCmd.getSet(lockKey, lockValue));
        if (currentTime > value2long_1 && currentTime > value2long_2){
            return true ;
        } else {
            return false ;
        }
    }

}

这里又提出了几点建议: 

1. checkIfTimeout这个方法中,既检查了超时条件,又使用了getSet加锁,不建议这样 

2. checkIfTimeout中一些变量名称命名不好 

3. 不推荐使用递归,容易出现问题,而且到时候定位问题不好定位
@Service ( "distributedLockHandler" )
public class DistributedLockHandler   {

    private static final Integer Lock_Timeout = 3 ;

     @Autowired
     private RedisTemplate redisTemplate;

     public boolean getLock (String lockKey){
         try {
             JedisCmd jedisCmd = redisTemplate.getJedisCmd();
             Long result = jedisCmd.setnx(lockKey, lockTimeDuration);
             if (result == 1 ){
                 return true ;
             } else {
                 return false ;
             }


     public boolean tryLock (String lockKey, Long timeout){
         try {
            JedisCmd jedisCmd = redisTemplate.getJedisCmd();
            boolean isLocked = false ;
            Long currentTime = System.currentTimeMillis();


            do {
                isLocked = getLock(lockKey);
                if (!isLocked){
                    if (checkIfLockTimeout(currentTime, lockKey)){
                        String lockTimeDuration = String.valueOf(currentTime + Lock_Timeout + 1 );
                        String preLockTimeDuration = jedisCmd.getSet(lockKey,lockTimeDuration);
                        if (currentTime > Long.valueOf(preLockTimeDuration)){
                            isLocked = true ;
                        }
                    }
                }
                Thread.sleep( 100 );
            } while (isLocked);

            return true ;

         } catch (Exception e){
             jobLog.error( "Failed to run DistributedLockHandler.getLock method." , e);
             return false ;


    public void realseLock (String lockKey){
        JedisCmd jedisCmd = redisTemplate.getJedisCmd();
        jedisCmd.del(lockKey);
    }


    public boolean checkIfLockTimeout (Long currentTime, String lockKey){
        JedisCmd jedisCmd = redisTemplate.getJedisCmd();
        if (currentTime > Long.valueOf(jedisCmd.get(lockKey))){
            return true ;
        } else {
            return false ;
        }
    }

}

这个版本我当时想的是基本上已经很ok了,但是老大仍有几点建议: 

1. tryLock职责分明(高内聚,低耦合),tryLock仅仅负责返回一个是否超时或者是否得到锁的结果, 

至于怎么获取锁,则不管;(获取锁的所有步骤都在innerLock中实现)
public class Demo4   {

    private static final String lockKey = "Lock.TecentIm_Interface_Counter" ;

    @Autowired
    private DistributedLockHandler distributedLockHandler;

    @Override
    void work() {
        try {
            boolean getLock = distributedLockHandler.tryLock(lockKey, Long.valueOf( 5 ));

            if (getLock){
                ### Do your job
            }

        } catch (Exception e){
            logger.error(e);
        } finally {
            distributedLockHandler.realseLock(lockKey);
        }
    }

}

@Service ( "distributedLockHandler" )
public class DistributedLockHandler   {

    private static final Integer Lock_Timeout = 3 ;

    @Autowired
    private RedisTemplate redisTemplate;

    private boolean innerTryLock (String lockKey){

        JedisCmd jedisCmd = redisTemplate.getJedisCmd();

        long currentTime = System.currentTimeMillis();
        String lockTimeDuration = String.valueOf(currentTime + Lock_Timeout + 1 );
        Long result = jedisCmd.setnx(lockKey, lockTimeDuration);

        if (result == 1 ){
            return true ;
        } else {
            if (checkIfLockTimeout(currentTime, lockKey)){
                String preLockTimeDuration = jedisCmd.getSet(lockKey,lockTimeDuration);
                if (currentTime > Long.valueOf(preLockTimeDuration)){
                    return true ;
                }
            }
            return false ;
        }

    }


    public boolean tryLock (String lockKey, Long timeout){
        try {
            Long currentTime = System.currentTimeMillis();
            boolean result = false ;

            while ( true ){
                if ((System.currentTimeMillis() - currentTime)/ 1000 > timeout){
                    jobLog.info( "Execute DistributedLockHandler.tryLock method, Time out." );
                    break ;
                } else {
                    result = innerTryLock(lockKey);
                    if (result){
                        break ;
                    } else {
                        jobLog.debug( "Try to get the Lock,and wait 100 millisecond...." );
                        Thread.sleep( 100 );
                    }
                }
            }
            return result;
        } catch (Exception e){
            jobLog.error( "Failed to run DistributedLockHandler.getLock method." , e);
            return false ;
        }
    }


    public void realseLock (String lockKey){
        JedisCmd jedisCmd = redisTemplate.getJedisCmd();
        jedisCmd.del(lockKey);
    }


    public boolean checkIfLockTimeout (Long currentTime, String lockKey){
        JedisCmd jedisCmd = redisTemplate.getJedisCmd();
        if (currentTime > Long.valueOf(jedisCmd.get(lockKey))){
            return true ;
        } else {
            return false ;
        }
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值