美文网首页技术干货
java 通过redis实现分布式锁

java 通过redis实现分布式锁

作者: 假程序猿 | 来源:发表于2017-10-12 17:54 被阅读0次
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

相关文章

网友评论

    本文标题:java 通过redis实现分布式锁

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