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

本文主要介绍下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;
    }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容