美文网首页Spring-Bootredisredis知识库
Redis分布式锁(一):锁的实现

Redis分布式锁(一):锁的实现

作者: heichong | 来源:发表于2018-10-09 16:32 被阅读16次

    本文主要介绍下Redis实现分布式锁的过程,

    • redis版本:redis 4.0,单实例,暂不考虑redis高可用
    • 客户端:Spring-data-redis

    分布式锁满足的条件

    1.互斥性。在任意时刻,只有一个客户端能持有锁。
    2.不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
    3.解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
    

    1. 获取锁

    • 唯一性:利用Redis中SETNX key value
    将key的值设为 value ,当且仅当 key 不存在;
    若给定的 key 已经存在,则 SETNX 不做任何动作;
    
    • 自动过期性:利用Redis中SETEX key seconds value
    将值 value 关联到 key ,并将 key 的生存时间设为 seconds (以秒为单位)。
    

    以上两个命令必须进行原子操作,以防止设置完key以后没设置自动过期,从而可能导致锁无法被释放。

    redisTemplate没有直接提供同时操作者两个命令的接口,但通过RedisCallback可以实现

    具体代码如下:

    /**
         * 获取锁
         *
         * @param lockedKey
         * @param expire
         * @return
         */
        public boolean getLock(String lockedKey,  long expire) {
            lockedValue = UUID.randomUUID().toString() ;
            //获取锁
            String exeResult = stringRedisTemplate.execute((RedisCallback<String>) connection -> {
                JedisCommands commands = (JedisCommands) connection.getNativeConnection();
                /**
                 * NX: 表示只有当锁定资源不存在的时候才能 SET 成功。利用 Redis 的原子性,
                 *      保证了只有第一个请求的线程才能获得锁,而之后的所有线程在锁定资源被释放之前都不能获得锁。
                 *
                 * PX: expire 表示锁定的资源的自动过期时间,单位是毫秒。具体过期时间根据实际场景而定
                 */
                return commands.set(lockedKey, lockedValue, "NX", "PX", expire);
            });
    
            //是否获取到锁
            boolean result = LOCK_SUCCESS.equals(exeResult);
    
            return result;
        }
    
    • 同时还提供带自动重试功能的方法来获取锁,当获取不到锁时,在一定时间内继续重试。
    /**
         * 获取锁
         * 如果获取不到,自动尝试多次,直到花费的时间超过tryTimeOut时间
         * @param lockedKey
         * @param expire
         * @param tryTimeOut
         * @return
         */
        public boolean getLock(String lockedKey, long expire, long tryTimeOut) {
            //单位都是毫秒
            long startTime = System.currentTimeMillis() ;
            Random random = new Random();
    
            while ((System.currentTimeMillis() - startTime) <= tryTimeOut){
                if( getLock(lockedKey,expire) ){
                    return true ;
                }
                try {
                    Thread.sleep(50, random.nextInt(100));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return false ;
        }
    

    2. 释放锁

    • 注意以下情况:
    1.线程A获取到锁以后,锁超时时间为1s,A在执行其他操作花费的时间超过1s时,锁因超时会被自动删除。
    2.此时B线程尝试获取锁时,得到锁,并设置超时时间为1s;如果此时线程的A的操作完成,要进行释放锁
    3.但是锁的持有者已经变成B了,所有要区分锁的持有者,否则就会导致线程A把线程B的锁释放掉了
    
    • 为了解决以上问题,在释放锁的时候,一定要区分锁的持有者是否等于释放者。
    • 为了保证多线程环境下的正确性,要保证 判断锁持有者操作和删除锁的操作是一个院子操作。

    这里通过Lua脚本实现:

    /**
         * 释放锁
         *
         * @param lockedKey
         * @return
         */
        public boolean releaseLock(String lockedKey) {
    
            if (lockedValue == null || lockedValue.length() == 0){
                return  false ;
            }
            // 使用Lua脚本删除Redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
            // 删除前要通过value来判断是否为自己的锁
            String script = new StringBuffer()
                    .append("if redis.call('get', KEYS[1]) == ARGV[1] then ")
                    .append("   return redis.call('del', KEYS[1]) ")
                    .append("else ")
                    .append("   return 0 ")
                    .append("end ")
                    .toString();
    
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<Long>();
            redisScript.setResultType(Long.class);
            redisScript.setScriptText(script);
            //执行脚本
            Long exeResult = stringRedisTemplate.execute(redisScript, Arrays.asList(lockedKey), lockedValue);
    
            boolean result = (RELEASE_SUCCESS == exeResult);
    
            return result;
        }
    

    3. 完整源码

    
    import org.springframework.data.redis.core.RedisCallback;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.core.script.DefaultRedisScript;
    import redis.clients.jedis.JedisCommands;
    
    import java.util.Arrays;
    import java.util.Random;
    import java.util.UUID;
    
    /**
     * 通过redis实现分布式锁
     *
     */
    public class RedisLock {
        private static final String LOCK_SUCCESS = "OK";
        private static final Long RELEASE_SUCCESS = 1L;
    
        private String lockedValue ;
        /**
         * redis操作类
         */
        StringRedisTemplate stringRedisTemplate;
    
        public RedisLock(StringRedisTemplate stringRedisTemplate){
            this.stringRedisTemplate = stringRedisTemplate ;
        }
    
        /**
         * 获取锁
         *
         * @param lockedKey
         * @param expire
         * @return
         */
        public boolean getLock(String lockedKey,  long expire) {
            lockedValue = UUID.randomUUID().toString() ;
            //获取锁
            String exeResult = stringRedisTemplate.execute((RedisCallback<String>) connection -> {
                JedisCommands commands = (JedisCommands) connection.getNativeConnection();
                /**
                 * NX: 表示只有当锁定资源不存在的时候才能 SET 成功。利用 Redis 的原子性,
                 *      保证了只有第一个请求的线程才能获得锁,而之后的所有线程在锁定资源被释放之前都不能获得锁。
                 *
                 * PX: expire 表示锁定的资源的自动过期时间,单位是毫秒。具体过期时间根据实际场景而定
                 */
                return commands.set(lockedKey, lockedValue, "NX", "PX", expire);
            });
    
            //是否获取到锁
            boolean result = LOCK_SUCCESS.equals(exeResult);
    
            return result;
        }
    
        /**
         * 获取锁
         * 如果获取不到,自动尝试多次,直到花费的时间超过tryTimeOut时间
         * @param lockedKey
         * @param expire
         * @param tryTimeOut
         * @return
         */
        public boolean getLock(String lockedKey, long expire, long tryTimeOut) {
            //单位都是毫秒
            long startTime = System.currentTimeMillis() ;
            Random random = new Random();
    
            while ((System.currentTimeMillis() - startTime) <= tryTimeOut){
                if( getLock(lockedKey,expire) ){
                    return true ;
                }
                try {
                    Thread.sleep(50, random.nextInt(100));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return false ;
        }
        /**
         * 释放锁
         *
         * @param lockedKey
         * @return
         */
        public boolean releaseLock(String lockedKey) {
    
            if (lockedValue == null || lockedValue.length() == 0){
                return  false ;
            }
            // 使用Lua脚本删除Redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
            // 删除前要通过value来判断是否为自己的锁
            String script = new StringBuffer()
                    .append("if redis.call('get', KEYS[1]) == ARGV[1] then ")
                    .append("   return redis.call('del', KEYS[1]) ")
                    .append("else ")
                    .append("   return 0 ")
                    .append("end ")
                    .toString();
    
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<Long>();
            redisScript.setResultType(Long.class);
            redisScript.setScriptText(script);
            //执行脚本
            Long exeResult = stringRedisTemplate.execute(redisScript, Arrays.asList(lockedKey), lockedValue);
    
            boolean result = (RELEASE_SUCCESS == exeResult);
    
            return result;
        }
    }
    
    

    相关文章

      网友评论

        本文标题:Redis分布式锁(一):锁的实现

        本文链接:https://www.haomeiwen.com/subject/mhhyaftx.html