Redis分布式锁实现OneByOne组件

背景

随着互联网项目的访问量增大,对系统的要求越来越高。应运而生出分布式系统,高可用集群等技术。而且已经非常成熟,在公司里面访问量再小的应用标配都是2台集群,单机应用已经一去不复返了。
java的多线程加锁的方式已经没有办法支撑这种分布式应用,因为java的线程锁,只起作用于当前运行的JVM中,多个JVM之间是相互分隔,无法控制的。

实现方案

核心就是将锁置于一个集中式的地方管理,让锁只有一把,所有集群应用争夺的就是这把锁。
因为是集中式访问,所以要求的性能较高-轻快,且要保证高可用-稳定:业内选择有比较多,zookeeper,redis等等,早期还有用DB(淘汰了)。
这边介绍下当前项目中设计的利用redis实现的。

实现要点

  1. 利用redis的setNX命令的原子性操作来进行加锁。如果单纯取锁,判断,再加锁存在不原子的情况,极端情况下会产生并发。
  2. 利用redis的lua脚本来释放锁,同样保证原子性
  3. 保证释放锁的动作,必须是当前锁的拥有者
  4. OneByOne阻塞等待:在现实应用中,尽可能的在防止业务并发的情况,保证业务的成功,所以排他锁(获取不到锁就失败)这种场景反而不是最多的。最多的是OneByOne,将并发转串行,尽量等待前一个业务完成之后,后一个业务能继续执行,而不是直接失败。 那势必需要设计一个阻塞的机制,这个阻塞等待并不像需要流量消峰那样用队列来做,只要释放可以有多个竞争者抢锁,抢到就继续执行。

核心代码

OneByOneTemplate模板实现

加锁,释放锁排队等等都是一个标准的流程,所以可以建立一个模板。调用者只要关注加锁代码块的业务逻辑实现。

模板

提供两个方法,一个是默认超时时间及排队时间。另一个可以指定锁的超时时间以及排队等待锁的时间。

定义模板接口
public interface OneByOneTemplate {
    <T> T execute(OneByOne oneByOne, CallBack<T> callBack);

    <T> T execute(OneByOne oneByOne, boolean waitInQueue, int timeoutMsecs, int expireMsecs, CallBack<T> callBack);
}
定义模板回调接口

业务实现逻辑,实现回调接口,并且实现回调方法即可

public interface CallBack<T> {
    T invoke();
}
具体模板实现
public class OneByOneTemplateImpl implements OneByOneTemplate {

    private static final int DEFAULT_TIME_OUT_MSECS = 10000;

    private static final int DEFAULT_EXPIRE_MSECS = 30000;

    @Override
    public <T> T execute(OneByOne oneByOne, CallBack<T> callBack) {
        return execute(oneByOne, Boolean.TRUE, DEFAULT_TIME_OUT_MSECS, DEFAULT_EXPIRE_MSECS, callBack);
    }

    @Override
    public <T> T execute(OneByOne oneByOne, boolean waitInQueue, int timeoutMsecs, int expireMsecs, CallBack<T> callBack) {
        // 需要排队
        if (waitInQueue) {
            // 当参数timeoutMsecs取值小于等于零时,则使用默认的排队10秒
            if (timeoutMsecs <= 0) {
                timeoutMsecs = DEFAULT_TIME_OUT_MSECS;
            } else {
                timeoutMsecs = 0;
            }

            // 不需要排队
        } else {
            timeoutMsecs = 0;
        }

        // 当参数expireMsecs取值小于等于零时,则使用默认的有效期30秒
        if (expireMsecs <= 0) {
            expireMsecs = DEFAULT_EXPIRE_MSECS;
        } 

        return invoke(oneByOne, timeoutMsecs, expireMsecs, callBack);
    }

    private <T> T invoke(OneByOne oneByOne, int timeoutMsecs, int expireMsecs, CallBack<T> callBack) {
        final String key = RedisCacheKeyConstants.REDIS_ONE_BY_ONE + oneByOne.getBizType() + "_" + oneByOne.getBizId();
        SedisLock sedisLock = new SedisLock(RedisUtil.redisClient, key, timeoutMsecs, expireMsecs);
        try {
            if (sedisLock.acquire()) { // 启用锁
                return callBack.invoke();
            } else {
                throw new AppException("bizType:" + oneByOne.getBizType() + ",bizId:" + oneByOne.getBizId() + ",并发执行!");
            }
        } catch (InterruptedException e) {
            throw new AppException("");
        } finally {
            sedisLock.release();
        }
    }

}

锁实现

声明redis锁的类,这段代码是在上面模板里面。这边列出来主要是为了控制释放锁时的owner

SedisLock sedisLock = new SedisLock(RedisUtil.redisClient, key, timeoutMsecs, expireMsecs);
加锁

加锁过程中会进行锁等待排队,利用的是轮询+wait()
redis中存储的这个锁的key,并且要存放这个对象的随机属性,owner属性可以是一个UUID。

public synchronized boolean acquire() throws InterruptedException {
    int timeout = timeoutMsecs;
    while (timeout >= 0) {
        if ("OK".equals(redisClient.execute(new ShardedJedisAction<String>() {
            @Override
            public String doAction(ShardedJedis jedis) {
                Jedis j = jedis.getShard(lockKey);
                // redis中不存在就设置lockKey对应的值,同时设置毫秒级过期时间
                return j.set(lockKey, owner, "NX", "PX", expireMsecs);
            }
        }))) {
            // lock acquired
            locked = true;
            return true;
        }

        int spinWatingTime = random.nextInt(200) + 1;
        timeout -= spinWatingTime;
        wait(spinWatingTime);
    }
    return false;
}

轮询200内的随机毫秒进行一次尝试获取锁,并且排队时间减去相应的时间,一直等待时间小于0则不再尝试。
这边random.nextInt(200)获取的值是0-199,容易忽略会导致wait(0),造成线程长期占用。所以需要+1

锁释放

需要校验这个锁是否加锁状态,并且判断是否是锁拥有者
利用redis支持的LUA脚本来将get和del两个动作做成原子性

public void release() {
    if (locked) {
        // 判断锁拥有者和释放锁
        final String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
        Object result = redisClient.execute(new ShardedJedisAction<Object>() {
            //
            @Override
            public Object doAction(ShardedJedis jedis) {
                List<String> keysList = Collections.singletonList(lockKey);
                List<String> argsList = Collections.singletonList(owner);
                Jedis jedisKey = jedis.getShard(lockKey);
                return jedisKey.eval(script, keysList, argsList);
            }
        });
        if (1 == (Long) result) {
            locked = false;
        }
    }
}

这样整体上一个OneByOne的分布式防并发锁就完成了。性能方面基于redis的高性能读写来说还是比较好的。

优点

  • 基于redis的锁机制,读写性能较高,提高系统的性能。
  • redis的原子性的特性的保证,为高并发下的锁处理变的稳定且简洁
  • 锁等待的设计能很好的提升业务的成功率,超时时间和等待时间设置合适的时间,可以达到很好的吞吐量和成功率。
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值