java 通过redis实现分布式锁

1. 开局

在多线程环境中,经常会碰到需要加锁的情况,由于现在的系统基本都是集群分布式部署,JVM的lock已经不能满足分布式要求,分布式锁就这样产生了。。。

百度一下,网上有很多分布式锁的方案或者例子,琳琅满目,看了之后不知所措,总体来说有以下几种:

  1. 基于数据库
  2. 基于zookeeper
  3. 基于redis
  4. 基于memcached

各有优缺点和实现难度,这里就不一一分析。本文主要是基于redis的setnx实现分布式锁,比较简单有一定的局限性,欢迎大家提出意见建议!

2. 加锁过程
  1. 执行redis的setnx,只有key不存在才能set成功(实际使用的是set(key, value, "NX", "EX", seconds),redis较新版本支持)
  2. 如果set成功(同时也设置了key的过期时间),则表示加锁成功
  3. 如果set失败,则每次sleep(x)毫秒后不断尝试,直到成功或者超时
3. 释放过程
  1. 判断加锁是否成功
  2. 如果成功,则执行redis的del删除
4. 问题思考
  1. 加锁时,锁的redis key过期时间多长合适?
    需要根据业务执行的时间长度来评估,默认30秒满足绝大部分需求,支持动态修改
  2. 加锁时,重试超时时间多长合适?本文设置的是过期时间的1.2倍,目的是在最坏的情况下等待锁过期后,尽量保证获取到锁,否则抛出超时异常。这个设置不完全合理
  3. 加锁时,重试的sleep时间多长合适?本文采用的是随机[50-300)毫秒,避免出现大量线程同时竞争,目的是错峰吧
  4. 释放时,如何避免释放了其他线程的锁(A获取锁后由于挂起导致锁到期自动释放;此时B获取到锁,而A又恢复运行释放了B的锁)?在初始化锁时生个一个唯一字符串,作为redis锁的value;value一致时表明是自己的锁,可以释放
5. 上代码!
  1. 用法

RedisLock lock = new RedisLock(redisHelper, lockKey);
try {
    // 执行加锁,防止并发问题
    lock.tryLock();
    // do somethings
    doSomethings()
}
finally {
    // 释放锁
    lock.release();
}
  1. RedisLock实现(注:依赖RedisHepler类,仅仅是对jedis的一层封装,可自行实现)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * RedisLock
 * 
 * @version 2017-9-21上午11:56:27
 * @author xiaoyun.zeng
 */
public class RedisLock {
    
    private Logger logger = LoggerFactory.getLogger(getClass());
    
    /**
     * key前缀
     */
    private static final String PREFIX = "lock:";
    /**
     * 操作redis的工具类
     */
    private RedisHelper redisHelper;
    /**
     * redis key
     */
    private String redisKey = null;
    /**
     * redis value
     */
    private String redisValue = null;
    /**
     * 锁的过期时间(秒),默认30秒,防止线程获取锁后挂掉无法释放锁
     */
    private int lockExpire = 30;
    /**
     * 尝试加锁超时时间(毫秒),默认为expire的1.2倍
     */
    private int tryTimeout = lockExpire * 1200;
    /**
     * 尝试加锁次数计数器
     */
    private long tryCounter = 0;
    /**
     * 加锁成功标记
     */
    private boolean success = false;
    private long startMillis = 0;
    private long expendMillis = 0;
    
    /**
     * RedisLock
     * 
     * @param redisHelper
     * @param lockKey
     */
    public RedisLock(RedisHelper redisHelper, String lockKey) {
        this.redisHelper = redisHelper;
        this.redisKey = PREFIX + lockKey;
        // 生成redis value,用于释放锁时比对是否属于自己的锁
        // 生成规则 lockKey+时间戳+随机数,避免重复
        // 乐观地认为:
        // 1、同一毫秒内,随机数相同的概率极小
        // 2、释放非自己线程锁的几率极小(release方法有说明这种情况)
        this.redisValue = lockKey + "-" + System.currentTimeMillis() + "-" + this.random(10000);
    }
    
    /**
     * RedisLock
     * 
     * @param redisHelper
     * @param lockKey
     * @param expire
     */
    public RedisLock(RedisHelper redisHelper, String lockKey, int lockExpire) {
        this(redisHelper, lockKey);
        // 过期时间
        this.lockExpire = lockExpire;
        // 超时时间(毫秒),默认为expire的1.2倍
        this.tryTimeout = lockExpire * 1200;
    }
    
    /**
     * 尝试加锁
     * <p>
     * 尝试加锁的过程将会一直阻塞下去,直到加锁成功或超时
     * 
     * @version 2017-9-21下午12:00:07
     * @author xiaoyun.zeng
     * @return
     */
    public void tryLock() throws RuntimeException {
        startMillis = System.currentTimeMillis();
        // 首次直接请求加锁
        if (!lock()) {
            do {
                // 超时判断,避免永远获取不到锁的情况下,一直尝试
                // 超时抛出runtime异常
                if (System.currentTimeMillis() - startMillis >= tryTimeout) {
                    throw new RuntimeException("尝试加锁超时" + tryTimeout + "ms");
                }
                // 随机休眠[50-300)毫秒
                // 避免出现大量线程同时竞争
                try {
                    Thread.sleep(this.random(250) + 50);
                }
                catch (InterruptedException e) {
                    // 出现异常直接抛出
                    throw new RuntimeException(e);
                }
            }
            while (!lock());
        }
    }
    
    /**
     * 释放锁
     * 
     * @version 2017-9-21下午12:00:21
     * @author xiaoyun.zeng
     * @param lockKey
     */
    public void release() {
        // 加锁成功才执行释放
        if (success) {
            // 释放前,检查redis value是否一致
            // 避免A获取锁后由于挂起导致锁到期自动释放
            // 此时B获取到锁,而A又恢复运行释放了B的锁
            String value = redisHelper.get(redisKey);
            if (redisValue.equals(value)) {
                redisHelper.del(redisKey);
                logger.debug("已释放锁:{}", redisValue);
            }
        }
    }
    
    /**
     * 加锁
     * 
     * @version 2017-9-21下午6:25:58
     * @author xiaoyun.zeng
     * @param key
     * @param value
     * @param lockExpire
     * @return
     */
    private boolean lock() {
        // 加锁计数器+1
        tryCounter++;
        // 调用redis setnx完成加锁,返回true表示加锁成功,否则失败
        success = redisHelper.setNx(redisKey, redisValue, lockExpire);
        // 计算总耗时
        expendMillis = System.currentTimeMillis() - startMillis;
        // 记录日志
        if (success) {
            logger.debug("加锁成功:尝试{}次,耗时{}ms,{}", tryCounter, expendMillis, redisValue);
        }
        return success;
    }
    
    /**
     * 产生随机数
     * 
     * @version 2017-9-22上午10:05:52
     * @author xiaoyun.zeng
     * @param max
     * @return
     */
    private int random(int max) {
        return (int) (Math.random() * max);
    }
    
}

6. 测试代码

单元测试:

@RunWith(SpringRunner.class)  
@SpringBootTest
public class RedisLockTest {
    
    @Autowired
    private RedisHelper redisHelper;
    
    @Test
    public void test() {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    RedisLock lock = new RedisLock(redisHelper, "zxy");
                    try {
                        lock.tryLock();
                        try {
                            Thread.sleep(2 * 1000);
                        }
                        catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    finally {
                        lock.release();
                    }
                }
            }).start();
        }
        while(true) {
        }
    }
}

日志输出:

2017/10/12 17:47:28.335 [Thread-8]  DEBUG [RedisLock.161] 加锁成功:尝试1次,耗时4ms,zxy-1507801648330-6665
2017/10/12 17:47:30.340 [Thread-8]  DEBUG [RedisLock.137] 已释放锁:zxy-1507801648330-6665
2017/10/12 17:47:30.351 [Thread-14] DEBUG [RedisLock.161] 加锁成功:尝试12次,耗时2018ms,zxy-1507801648333-6866
2017/10/12 17:47:32.356 [Thread-14] DEBUG [RedisLock.137] 已释放锁:zxy-1507801648333-6866
2017/10/12 17:47:32.396 [Thread-11] DEBUG [RedisLock.161] 加锁成功:尝试22次,耗时4065ms,zxy-1507801648331-5217
2017/10/12 17:47:34.400 [Thread-11] DEBUG [RedisLock.137] 已释放锁:zxy-1507801648331-5217
2017/10/12 17:47:34.430 [Thread-12] DEBUG [RedisLock.161] 加锁成功:尝试39次,耗时6098ms,zxy-1507801648332-7708
2017/10/12 17:47:36.433 [Thread-12] DEBUG [RedisLock.137] 已释放锁:zxy-1507801648332-7708
2017/10/12 17:47:36.453 [Thread-17] DEBUG [RedisLock.161] 加锁成功:尝试50次,耗时8119ms,zxy-1507801648334-2362
2017/10/12 17:47:38.457 [Thread-17] DEBUG [RedisLock.137] 已释放锁:zxy-1507801648334-2362
2017/10/12 17:47:38.494 [Thread-9]  DEBUG [RedisLock.161] 加锁成功:尝试57次,耗时10164ms,zxy-1507801648330-7086
2017/10/12 17:47:40.497 [Thread-9]  DEBUG [RedisLock.137] 已释放锁:zxy-1507801648330-7086
2017/10/12 17:47:40.587 [Thread-13] DEBUG [RedisLock.161] 加锁成功:尝试70次,耗时12254ms,zxy-1507801648333-8881
2017/10/12 17:47:42.590 [Thread-13] DEBUG [RedisLock.137] 已释放锁:zxy-1507801648333-8881
2017/10/12 17:47:42.611 [Thread-15] DEBUG [RedisLock.161] 加锁成功:尝试82次,耗时14276ms,zxy-1507801648335-2509
2017/10/12 17:47:44.614 [Thread-15] DEBUG [RedisLock.137] 已释放锁:zxy-1507801648335-2509
2017/10/12 17:47:44.699 [Thread-16] DEBUG [RedisLock.161] 加锁成功:尝试89次,耗时16365ms,zxy-1507801648334-5791
2017/10/12 17:47:46.702 [Thread-16] DEBUG [RedisLock.137] 已释放锁:zxy-1507801648334-5791
2017/10/12 17:47:46.802 [Thread-10] DEBUG [RedisLock.161] 加锁成功:尝试106次,耗时18471ms,zxy-1507801648331-7347
2017/10/12 17:47:48.805 [Thread-10] DEBUG [RedisLock.137] 已释放锁:zxy-1507801648331-7347
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容