基于redis的分布式锁设计实现

分布式锁介绍

分布式并发环境下,为了保证事务操作的原子性,需要引入分布式锁来保证一连串行为是原子性操作

例如经典的自增1操作

  value = get(a);
  set(a, value+1);

假设当前有两个线程要执行上面的操作,要保证线程a在保存新值之前,线程b不会读取到旧的值进行加一操作,一种实现方案就是引入分布式锁,保证该资源全局仅被一个线程占有

分布式锁设计

分布式锁最重要的操作就是加锁解锁,除此以外还需要设置锁的过期时间,防止死锁问题的发生

加锁与解锁需要明确锁的是什么,对于同一类业务操作同一个资源对象,有且只有一个标识符对应到该资源对象,反之亦然。即锁定该资源对象的标识符需要是全局唯一的,在redis中就是锁对应的key的设计需要全局唯一

分布式锁的实现比较

Github: https://github.com/ChaselX/devbox-spring-boot-starter
Gitee: https://gitee.com/chasel96/devbox-spring-boot-starter

实现方案一 基于redis超时机制的分布式锁实现

最简单的加锁实现就是往redis中放入一个key,通过redis的setIfAbsent()方法,若插入失败(该key对应的记录已经存在)则代表当前对应的资源已经被其他线程锁住了,休眠一段时间后再次尝试获取锁

而解锁就是从redis删除该key对应的记录

为了防止线程长时间占有锁不释放导致死锁,在加锁的时候设置该缓存的过期时间为n秒,当n秒过去,锁自动释放

实现代码:

@Component
public class DistributedLockCacheImpl implements DistributedLockCache {
    private static Logger logger = LoggerFactory.getLogger(DistributedLockCacheImpl.class);

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private final static int     EXPIRE_MS  = 15 * 1000;
    private final static int     TIME_OUT_MS = 10 * 1000;
    private final static String  PREFIX_LOCK   = "demo:distributed-lock:";

    public void lock(String id) {
        String key = PREFIX_LOCK + id;
        int delay = 100;
        int timeout = TIME_OUT_MS;

        try {
            while (timeout >= 0) {
                Boolean result = redisTemplateString.opsForValue().setIfAbsent(key, "", EXPIRE_MS, TimeUnit.MILLISECONDS);
                if (result) {
                    return;
                }
                timeout -= delay;
                Thread.sleep(delay);
            }
            throw new DistributedLockException();
        }catch (Exception ex){
            throw new DistributedLockException();
        }
    }


    public void release(String id) {
        String key = PREFIX_LOCK + id;
        stringRedisTemplate.delete(key);
    }
}

这种实现有个问题:

  1. 若线程阻塞时间大于锁的超时时间,直接删除key会出现问题,有可能锁已经自动释放了,而此时删除的可能是其他线程的锁
优化实现方案一

引入随机值的概念,存入redis的value改为随机值,在释放锁的时候判断当前锁的value是否和随机值一致,若一致才进行删除操作,这需要引入redis事务,事务的使用参见深入理解redis事务

    public boolean acquire(String id, String randomStr) {
        int delay = 100;
        int timeout = TIME_OUT_MS;

        randomStr = UUID.randomUUID().toString();

        try {
            while (timeout >= 0) {
                Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, randomStr, expireMsecs, TimeUnit.MILLISECONDS);
                if (result != null && result) {
                    return true;
                }
                timeout -= delay;
                Thread.sleep(delay);
            }
            throw new DistributedLockException();
        } catch (Exception e) {
            throw new DistributedLockException();
        }
    }

   public void release(String id, String randomStr) {
        String key = PREFIX_LOCK + id;
        stringRedisTemplate.watch(key);
        String currentValue = stringRedisTemplate.opsForValue().get(key);
        if (null==currentValue || !currentValue.equals(randomStr)) {
            return;
        }
        stringRedisTemplate.multi();
        stringRedisTemplate.delete(key);
        stringRedisTemplate.exec();
    }

释放锁这部分事务也可以使用redis脚本代替,Redis脚本也是事务型的。因此,可以通过Redis事务实现的功能,同样也可以通过Redis脚本来实现,而且通常脚本更简单、更快速。

实现方案二 锁超时时间约等于过期时间的分布式锁解决方案

这种实现不依赖于redis超时机制,也不用担心因为键未删除而带来的死锁问题。但由于其实现依赖于系统当前时间,需要保证服务器之间的系统时钟同步问题(linux可参考ntp时钟同步)。一般生产环境机器是默认需要做时钟同步的。

之所以说锁超时时间约等于expireMsecs是因为该方案无法严格保证锁的过期时间为expireMsecs秒,因为在获取锁的时候,过期时间可能会在多线程并发getAndSet()的时候被修改,导致过期时间和当前线程加锁时候放入的值不等同,但个人认为影响不大,实现代码如下:

package com.chasel.demo.cache;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;

/**
 * 锁超时时间约等于expireMsecs的分布式锁解决方案
 *
 * @author XieLongzhen
 * @date 2019/3/22 10:31
 */
@Slf4j
public class RedisDistributedLock implements IDistributedLock {

    private StringRedisTemplate redisTemplate;

    /**
     * 锁的键值
     */
    private String lockKey;
    /**
     * 锁超时, 防止线程得到锁之后, 不去释放锁
     */
    private int expireMsecs;
    /**
     * 锁等待, 防止线程饥饿
     */
    private int timeoutMsecs;
    /**
     * 是否已经获取锁
     */
    private boolean locked = false;

    public RedisDistributedLock(String lockKey, int timeoutMsecs, int expireMsecs, StringRedisTemplate redisTemplate) {
        this.lockKey = lockKey;
        this.timeoutMsecs = timeoutMsecs;
        this.expireMsecs = expireMsecs;
        this.redisTemplate = redisTemplate;
    }

    public String getLockKey() {
        return this.lockKey;
    }

    /**
     * 方法去掉了synchronized关键字
     */
    @Override
    public boolean acquire() {

        int timeout = timeoutMsecs;

        try {

            while (timeout >= 0) {

                long expires = System.currentTimeMillis() + expireMsecs + 1;
                String expiresStr = String.valueOf(expires); // 锁到期时间

                if (redisTemplate.opsForValue().setIfAbsent(lockKey, expiresStr)) {
                    locked = true;
                    log.debug("[1] 成功获取分布式锁!");
                    return true;
                }
                String currentValueStr = redisTemplate.opsForValue().get(lockKey); // redis里的时间

                // 判断是否为空, redis旧锁是否已经过期, 如果被其他线程设置了值, 则第二个条件判断是过不去的
                if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {

                    String oldValueStr = redisTemplate.opsForValue().getAndSet(lockKey, expiresStr);

                    // 获取上一个锁到期时间, 并设置现在的锁到期时间
                    // 如果这个时候, 多个线程恰好都到了这里
                    // 只有一个线程拿到的过期时间是小于当前时间的,后续的线程set进去过期时间但拿到的过期时间会大于当前时间
                    // 只有一个线程的设置值和当前值相同, 那么它才有权利获取锁,其余线程继续等待
                    if (oldValueStr == null || oldValueStr.equals(currentValueStr)) {
                        locked = true;
                        log.debug("[2] 成功获取分布式锁!");
                        return true;
                    }
                }

                timeout -= 100;
                Thread.sleep(100);
            }
        } catch (Exception e) {
            log.error("获取锁出现异常, 必须释放: {}", e.getMessage());
        }

        return false;
    }

    /**
     * 方法去掉了synchronized关键字
     */
    @Override
    public void release() {

        try {
            if (locked) {

                String currentValueStr = redisTemplate.opsForValue().get(lockKey); // redis里的时间

                // 校验是否超过有效期, 如果不在有效期内, 那说明当前锁已经失效, 不能进行删除锁操作
                if (currentValueStr != null && Long.parseLong(currentValueStr) > System.currentTimeMillis()) {
                    redisTemplate.delete(lockKey);
                    locked = false;
                    log.debug("[3] 成功释放分布式锁!");
                }
            }
        } catch (Exception e) {
            log.error("释放锁出现异常, 必须释放: {}", e.getMessage());
        }
    }
}

使用通过DistributedLockComponent来使用,在业务代码中通过DistributedLockComponent获取锁以后只需要简单的acquire()然后release()即可

package com.chasel.demo.cache;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;

/**
 * 分布式锁工具类
 *
 * @author XieLongzhen
 * @date 2019/3/21 10:36
 */
@Component
public class DistributedLockComponent {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private final String PREFIX_KEY = “demo:distributed-lock:”;

    /**
     * 锁等待, 防止线程饥饿
     */
    private final int DEFAULT_TIMEOUT_MSECS = 15 * 1000;
    /**
     * 锁超时, 防止线程得到锁之后, 不去释放锁
     */
    private final int DEFAULT_EXPIRE_MSECS = 15 * 1000;

    /**
     * 获取分布式锁
     * 默认获取锁15s超时, 锁过期时间15s
     */
    public IDistributedLock getRedisLock(String key) {
        return getRedisLock(key, DEFAULT_TIMEOUT_MSECS, DEFAULT_EXPIRE_MSECS);
    }

    /**
     * 获取分布式锁
     */
    public IDistributedLock getRedisLock(String key, int timeoutMsecs) {
        return getRedisLock(key, timeoutMsecs, DEFAULT_EXPIRE_MSECS);
    }

    /**
     * 获取分布式锁
     */
    public IDistributedLock getRedisLock(String key, int timeoutMsecs, int expireMsecs) {
        return new RedisDistributedLock(assembleKey(key), timeoutMsecs, expireMsecs, redisTemplate);
    }

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