Redis 分布式锁实现

1.用redis的setNX命令实现分布式锁

    原理:Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。redis的SETNX命令可以方便的实现分布式锁。

setNX(SET if Not eXists)

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

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

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

返回值:

  设置成功,返回 1 。

  设置失败,返回 0 。

redis> EXISTS job                # job 不存在
(integer) 0

redis> SETNX job "programmer"    # job 设置成功
(integer) 1

redis> SETNX job "code-farmer"   # 尝试覆盖 job ,失败
(integer) 0

redis> GET job                   # 没有被覆盖
"programmer"

代码实现:

    public boolean setNX(final String key, final String value,Long expireTime) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));
                    connection.expire(serializer.serialize(key), expireTime);//#1
                    connection.close();
                    return success;
                }
            });
        } catch (Exception e) {
            log.error("setNX redis error, key : {}", key,e);
        }
        return obj != null ? (Boolean) obj : false;
    }

上述为加锁代码,可是有个问题如果#1出没有正常的设置过期时间,那么这个锁将永远无法过期,就变成了死锁,所以下面再继续优化

2.使用getSET

GETSET key value

将给定 key 的值设为 value ,并返回 key 的旧值(old value)。

当 key 存在但不是字符串类型时,返回一个错误。

返回值:  返回给定 key 的旧值。

  当 key 没有旧值时,也即是, key 不存在时,返回 nil 。

我们把value值设置为过期的时间点,然后如果setNx设置失败,则去查看是否value是否过期,如果已经过期则去拿锁;

先来一种错误的思路

C0操作超时了,但它还持有着锁,C1和C2读取lock.foo检查时间戳,先后发现超时了。 
C1 发送DEL lock.foo 
C1 发送SETNX lock.foo 并且成功了。 
C2 发送DEL lock.foo 
C2 发送SETNX lock.foo 并且成功了。 
这样一来,C1,C2都拿到了锁!问题大了! 

然后 getset这个命令就有勇武之地了;

C3发送SETNX lock.foo 想要获得锁,由于C0还持有锁,所以Redis返回给C3一个0 
C3发送GET lock.foo 以检查锁是否超时了,如果没超时,则等待或重试。 
反之,如果已超时,C3通过下面的操作来尝试获得锁: 
GETSET lock.foo <current Unix time + lock timeout + 1> 
通过GETSET,C3拿到的时间戳如果仍然是超时的,那就说明,C3如愿以偿拿到锁了。 
如果在C3之前,有个叫C4的客户端比C3快一步执行了上面的操作,那么C3拿到的时间戳是个未超时的值,这时,C3没有如期获得锁,需要再次等待或重试。
留意一下,尽管C3没拿到锁,但它改写了C4设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。 

代码:

package com.yxhd.jgg.util;

 

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import javax.annotation.Resource;


public class RedisLock {

    private Logger log= LoggerFactory.getLogger(RedisLock.class);
    //加锁超时时间,单位秒, 即:加锁时间内执行完操作,如果未完成会有并发现象
    //2000  2 秒
    private long lockTimeout=2000;

   @Autowired
   private RedisTemplate redisTemplate;

    public boolean setNX(final String key, final String value,Long expireTime) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));
                    connection.expire(serializer.serialize(key), expireTime);
                    connection.close();
                    return success;
                }
            });
        } catch (Exception e) {
            log.error("setNX redis error, key : {}", key,e);
        }
        return obj != null ? (Boolean) obj : false;
    }


    public boolean setNX(final String key, final String value) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));

                    connection.close();
                    return success;
                }
            });
        } catch (Exception e) {
            log.error("setNX redis error, key : {}", key,e);
        }
        return obj != null ? (Boolean) obj : false;
    }

    public String get(final String key) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    byte[] data = connection.get(serializer.serialize(key));
                    connection.close();
                    if (data == null) {
                        return null;
                    }
                    return serializer.deserialize(data);
                }
            });
        } catch (Exception e) {
            log.error("get redis error, key : {}", key,e);
        }
        return obj != null ? obj.toString() : null;
    }

    public String getSet(final String key, final String value) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    byte[] data = connection.getSet(serializer.serialize(key), serializer.serialize(value));
                    connection.close();
                    return serializer.deserialize(data);
                }
            });
        } catch (Exception e) {
            log.error("getSet redis error, key : {}", key,e);
        }
        return obj != null ? obj.toString() : null;
    }

    public boolean del(final String lockKey) {
        try {
            redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    Long del = connection.del(serializer.serialize(lockKey));
                    connection.close();
                    return del;
                }
            });
            return true;
        } catch (Exception e) {
            log.error("del redis error, key : {}", lockKey);
        }
        return false;
    }

    public boolean expire(final String lockKey, final Long expireTime) {
        Object obj = null;
        try {
            obj =  redisTemplate.execute(new RedisCallback<Boolean>() {
                @Override
                public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    boolean bool = connection.expire(serializer.serialize(lockKey), expireTime);
                    connection.close();
                    return bool;
                }
            });
            log.info("设置过期时间"+expireTime);
            return (Boolean)obj;
        } catch (Exception e) {
            log.error("expire redis error, key : {}", lockKey,e);
        }
        return false;
    }

    /**
     * 加锁
     * 取到锁加锁,取不到锁就返回
     *
     * @param lockKey
     * @param threadName
     * @return
     */
    public synchronized long lock(String lockKey, String threadName) {
        log.info(threadName + "开始执行加锁");
        //锁时间   过期时间
        Long lock_timeout = currtTimeForRedis() + lockTimeout + 1;
        // 锁失效时间为10 秒,value里存入过期时间,如果超过过期时间视为无效锁,其他线程可以重新获取锁
        if (setNX(lockKey, String.valueOf(lock_timeout),10l)) {
            //如果加锁成功
            log.info(threadName + "加锁成功+1");
            //设置超时时间,释放内存

            return lock_timeout;
        } else {
            //获取redis里面的时间
            Object result = get(lockKey);
            Long currt_lock_timeout_str = result == null ? null : Long.parseLong(result.toString());
            //锁已经失效
            if (currt_lock_timeout_str != null && currt_lock_timeout_str < currtTimeForRedis()) {
                //判断是否为空,不为空时,如果被其他线程设置了值,则第二个条件判断无法执行
                //获取上一个锁到期时间,并设置现在的锁到期时间  #2
                Long old_lock_timeout_Str = Long.valueOf(getSet(lockKey, String.valueOf(lock_timeout)));
                if (old_lock_timeout_Str != null && old_lock_timeout_Str.equals(currt_lock_timeout_str)) {
                    //多线程运行时,多个线程签好都到了这里,但只有一个线程的设置值和当前值相同,它才有权利获取锁
                    log.info(threadName + "加锁成功+2");
                    //设置超时间,释放内存  这个是 10 L
                    expire(lockKey, 10l);
                    //返回加锁时间
                    return lock_timeout;
                }
            }
        }
        return -1;
    }
    /**
     * 解锁
     *
     * @param lockKey
     * @param lockValue
     * @param threadName
     */
    public synchronized void unlock(String lockKey, long lockValue, String threadName) {
        log.info(threadName + "执行解锁==========");//正常直接删除 如果异常关闭判断加锁会判断过期时间
        //获取redis中设置的时间
        String result = get(lockKey);
        log.info("获取到的locakValue为:[{}],当前线程为:[{}]", result, threadName);
        Long currt_lock_timeout_str = result == null ? null : Long.valueOf(result);
        //如果是加锁者,则删除锁, 如果不是,则等待自动过期,重新竞争加锁
        if (currt_lock_timeout_str != null && currt_lock_timeout_str == lockValue) {
            del(lockKey);
            log.info(threadName + "解锁成功------------------");
        }
    }
    /**
     * 多服务器集群,使用下面的方法,代替System.currentTimeMillis(),获取redis时间,避免多服务的时间不一致问题!!!
     *
     * @return
     */
    public long currtTimeForRedis() {
        Object obj = redisTemplate.execute(new RedisCallback<Object>() {
            @Override
            public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                return redisConnection.time();
            }
        });
        return obj == null ? -1 : Long.parseLong(obj.toString());
    }
}

这是我的代码,注意:#2处 当很多线程都去抢锁的时候,会进入第一个if,然后通过getSet拿锁,
可是只有一个返回的old_lock_timeout_Str(旧锁的value)才能匹配成功,
然后才能进入第二个if,可是old_lock_timeout_Str不一定是当前线程的value,不过差距小可以忽略

c0去拿锁SetNx失败,查看是否过期,
c1也去拿锁SetNx失败,查看是否过期,
c0 ,c1 发现锁已经过期 同时进入第一个if语句
c0 先调用getSet方法,返回过期锁的过期时间(value)
c1 后调用getSet方法,返回的却是c1的过期时间(value)
c0 通过返回的value与过期锁的value进行匹配,发现匹配成功,成功获取锁,进入第二个if,然后设置锁过期时间
c1 通过它getSet返回的value(这时他返回的c1的value)发现与过期的锁匹配失败,说明获取锁失败
可是发现这个c0 拿到了锁,可是c0的过期时间,却不是c0设置的,是c1设置的

原文地址:http://www.cnblogs.com/0201zcr/p/5942748.html

猜你喜欢

转载自blog.csdn.net/qq_30285985/article/details/80591786