基于redis的分布式锁

来源:互联网 发布:手机mysql服务器 编辑:程序博客网 时间:2024/06/07 06:08

1.Redis两个重要的方法

⑴SETNX命令

即 SET IF NOT EXSIT

当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0

redis> EXISTS job                # job 不存在(integer) 0redis> SETNX job "programmer"    # job 设置成功(integer) 1redis> SETNX job "code-farmer"   # 尝试覆盖 job ,失败(integer) 0redis> GET job                   # 没有被覆盖"programmer"
⑵GETSET命令

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

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

redis> GETSET db mongodb    # 没有旧值,返回 nil(nil)redis> GET db"mongodb"redis> GETSET db redis      # 返回旧值 mongodb"mongodb"redis> GET db"redis"
2.解决死锁
如果一个持有锁的客户端失败或崩溃了不能释放锁,该怎么解决?

首先我们要知道,删除锁是由锁的拥有者来执行的。不能简单的通过删除锁来解决死锁!

⑴加入我们通过简单点的删除来解决死锁(这个方法不行):

我们同样假设进程P1已经首先获得了锁 lock.foo,然后进程P1挂掉了
P2和P3进程读取键 lock.foo 的值,检测锁是否已超时(通过比较当前时间和键 lock.foo 的值来判断是否超时)
P2和P3进程发现锁 lock.foo 已超时
P2执行 DEL lock.foo命令
P2执行 SETNX lock.foo命令,并返回1,即P2获得锁
P3执行 DEL lock.foo命令将P2刚刚设置的键 lock.foo 删除(这步是由于P3刚才已检测到锁已超时)
P3执行 SETNX lock.foo命令,并返回1,即P3获得锁
P2和P3同时获得了锁

⑵我们换个姿势来解决死锁:

进程P4执行 SETNX lock.foo 以尝试获取锁
由于进程P1已获得了锁,所以P4执行 SETNX lock.foo 返回0,即获取锁失败
P4执行 GET lock.foo 来检测锁是否已超时,如果没超时,则等待一段时间,再次检测
如果P4检测到锁已超时,即当前的时间大于键 lock.foo 的值,P4会执行以下操作
GETSET lock.foo <current Unix timestamp + lock timeout + 1>
由于 GETSET 操作在设置键的值的同时,还会返回键的旧值,通过比较键 lock.foo 的旧值是否小于当前时间,可以判断进程是否已获得锁
假如另一个进程P5也检测到锁已超时,并在P4之前执行了 GETSET 操作,那么P4的 GETSET 操作返回的是一个大于当前时间的时间戳,这样P4就不会获得锁而继续等待。注意到,即使P4接下来将键 lock.foo 的值设置了比P5设置的更大的值也没影响。

3.代码实现

import org.slf4j.Logger;import org.slf4j.LoggerFactory;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;/** * Redis distributed lock implementation. * * @author zhengcanrui */public class RedisLock {    private static Logger logger = LoggerFactory.getLogger(RedisLock.class);    private RedisTemplate redisTemplate;    private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;    /**     * Lock key path.     */    private String lockKey;    /**     * 锁超时时间,防止线程在入锁以后,无限的执行等待     */    private int expireMsecs = 60 * 1000;    /**     * 锁等待时间,防止线程饥饿     */    private int timeoutMsecs = 10 * 1000;    private volatile boolean locked = false;    /**     * Detailed constructor with default acquire timeout 10000 msecs and lock expiration of 60000 msecs.     *     * @param lockKey lock key (ex. account:1, ...)     */    public RedisLock(RedisTemplate redisTemplate, String lockKey) {        this.redisTemplate = redisTemplate;        this.lockKey = lockKey + "_lock";    }    /**     * Detailed constructor with default lock expiration of 60000 msecs.     *     */    public RedisLock(RedisTemplate redisTemplate, String lockKey, int timeoutMsecs) {        this(redisTemplate, lockKey);        this.timeoutMsecs = timeoutMsecs;    }    /**     * Detailed constructor.     *     */    public RedisLock(RedisTemplate redisTemplate, String lockKey, int timeoutMsecs, int expireMsecs) {        this(redisTemplate, lockKey, timeoutMsecs);        this.expireMsecs = expireMsecs;    }    /**     * @return lock key     */    public String getLockKey() {        return lockKey;    }    private 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) {            logger.error("get redis error, key : {}", key);        }        return obj != null ? obj.toString() : null;    }    private 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) {            logger.error("setNX redis error, key : {}", key);        }        return obj != null ? (Boolean) obj : false;    }    private 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[] ret = connection.getSet(serializer.serialize(key), serializer.serialize(value));                    connection.close();                    return serializer.deserialize(ret);                }            });        } catch (Exception e) {            logger.error("setNX redis error, key : {}", key);        }        return obj != null ? (String) obj : null;    }    /**     * 获得 lock.     * 实现思路: 主要是使用了redis 的setnx命令,缓存了锁.     * reids缓存的key是锁的key,所有的共享, value是锁的到期时间(注意:这里把过期时间放在value了,没有时间上设置其超时时间)     * 执行过程:     * 1.通过setnx尝试设置某个key的值,成功(当前没有这个锁)则返回,成功获得锁     * 2.锁已经存在则获取锁的到期时间,和当前时间比较,超时的话,则设置新的值     *     * @return true if lock is acquired, false acquire timeouted     * @throws InterruptedException in case of thread interruption     */    public synchronized boolean lock() throws InterruptedException {        int timeout = timeoutMsecs;        while (timeout >= 0) {            long expires = System.currentTimeMillis() + expireMsecs + 1;            String expiresStr = String.valueOf(expires); //锁到期时间            if (this.setNX(lockKey, expiresStr)) {                // lock acquired                locked = true;                return true;            }            String currentValueStr = this.get(lockKey); //redis里的时间            if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {                //判断是否为空,不为空的情况下,如果被其他线程设置了值,则第二个条件判断是过不去的                // lock is expired                String oldValueStr = this.getSet(lockKey, expiresStr);                //获取上一个锁到期时间,并设置现在的锁到期时间,                //只有一个线程才能获取上一个线上的设置时间,因为jedis.getSet是同步的                if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {                    //防止误删(覆盖,因为key是相同的)了他人的锁——这里达不到效果,这里值会被覆盖,但是因为什么相差了很少的时间,所以可以接受                    //[分布式的情况下]:如过这个时候,多个线程恰好都到了这里,但是只有一个线程的设置值和当前值相同,他才有权利获取锁                    // lock acquired                    locked = true;                    return true;                }            }            timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS;            /*                延迟100 毫秒,  这里使用随机时间可能会好一点,可以防止饥饿进程的出现,即,当同时到达多个进程,                只会有一个进程获得锁,其他的都用同样的频率进行尝试,后面有来了一些进行,也以同样的频率申请锁,这将可能导致前面来的锁得不到满足.                使用随机的等待时间可以一定程度上保证公平性             */            Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS);        }        return false;    }    /**     * Acqurired lock release.     */    public synchronized void unlock() {        if (locked) {            redisTemplate.delete(lockKey);            locked = false;        }    }}
调用:

RedisLock lock = new RedisLock(redisTemplate, key, 10000, 20000); try {            if(lock.lock()) {                   //需要加锁的代码                }            }        } catch (InterruptedException e) {            e.printStackTrace();        }finally {            //为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,            //操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。 ————这里没有做            lock.unlock();        }

文章来源:

https://www.cnblogs.com/0201zcr/p/5942748.html









































































原创粉丝点击