分布式锁 并发 自旋 优化


记录本人在实际业务中遇到的问题和解决方案。

业务场景:影院营销活动 肯定是需要一套统一的解决方案
1.线上做活动,活动有资源限制,比如总金额1W元,1单补贴一定数量金额,无资源后停止活动
2.活动的并发量有高有低,最高甚至存在秒抢活动,最低可能1小时就1单
3.活动存在资源恢复,即取消订单等,取消后要恢复本单的资源消耗,恢复的量级虽比消耗低,但是低的也有限,并发高的活动恢复也高


个人考虑解决方案:
1.系统是分布式多节点的,因此在单节点上加锁肯定是不行的
2.前期没多想直接在活动开始时,资源放redis,然后用分布式锁进行资源控制
3.这里不考虑redis可能宕机后的降级方案


问题:
之前没有秒抢类型的高并发活动,一切ok没毛病,但前断时间来了个秒抢就悲剧了,具体下面分析


实际代码,就只说消耗了,恢复是同理的

    RedisDisLock.getLock(ActivityResouceCache.LOCK+activityResourceItem.getActivityCode());
    try {  
            result = ActivityResouceCache.resouceDeduct(activityResourceItem, params.getOrderCode(), params.getMobile(), countUsed, thisuse);
        } catch (Exception e) {
            logger.error("");
            return false;
        } finally {
            RedisDisLock.releaseLock(ActivityResouceCache.LOCK+activityResourceItem.getActivityCode());
        }
    这是消耗的主要代码,resouceDeduct只进行了资源数量读取,判断,回写redis的操作,判断资源不足甚至回写都没,也就只是对redis进行了一次读和可能存在的一次写,基本不存在再优化的空间了
那下面看看redis分布式锁的写法
    public static void getLock(String lockKey) {
        boolean lockFlag = true;
        while (lockFlag) {//自旋等待拿锁
            if (acquireLock(lockKey)) {
                lockFlag = false;
            }
        }
    }
    
    public static boolean acquireLock(String lock) {
        boolean success = false;
        long acquired = JedisClient.setnx(lock, "1");
        if (acquired==1) {
            success = true;
            JedisClient.expire(lock, expired);//设置超时
        }    
        return success;
    }
    
    一个活动一个锁,非常简单

这里可能网上别家的分布式锁比写的简单了点,说下我的考虑吧:
  在获取锁acquireLock时,我没有加else进行所谓的其它线程持有锁超时判断,原因是基于严格限制,没有锁就必须等待有锁的请求处理完,如果使用超时判断,那么超时就能获取锁可能会造成资源数不正确的,
因此我就没用超时,或者等到设置的expire自动超时取消锁,我设置的是10s,一般情况下的执行效率是不可能会等到自然超时的,但是比如节点1获取到锁后挂了,那其它节点就等吧,真节点挂了等10s是可以接收的,因此用了expire。
实际线上是没问题的,也没遇到获取锁后宕机问题,因为是finally中释放锁,期间也没有别的逻辑,因此除非redis出问题,不然也不会有问题的。

getLock中使用自旋,最主要的问题就是这个自旋了,我的一切问题都是自旋引起的,详细说:

我的第一版不是这么写的加了休眠
    public static void getLock(String lockKey) {
        MCSLock lock = geMCSLock(lockKey);
        lock.lock();
        boolean lockFlag = true;
        while (lockFlag) {//循环等待拿锁
            if (acquireLock(lockKey)) {
                lockFlag = false;
            }else {
                try {
                    Thread.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

问题:
1.并发稍微大点就有问题,除获取到锁的之外其它请求全部要自旋,注意其它请求基本都是相隔非常短ns级别的对redis进行setnx请求,休眠结束后也是非常短的时间进行的请求,这么干会把redis性能打低,
    获取到锁的请求处理业务逻辑本来2ms完事,这里就可能需要4ms,随着持续自旋给redis压力,处理时间可能会更长变成10ms,就是个恶性循环
2.存在获取锁延迟,比如第一个请求释放锁,其它请求全部在休眠,需要一定的时候后再去争抢锁,导致中间会有空档
3.存在不公平锁竞争导致有饥饿线程,即某一个请求一直没抢到锁,一直等待。线上我这里是对外提供http的api,api默认30s超时,在持续性的低并发下因为这个问题就会有超时发生
4.休眠时间不好设计,短了redis压力大,高了2,3问题会加重

使用随机区间时间进行休眠,怎么样呢?其实问题一样的
1.区间小了比如5-10ms休眠,1问题缓解了点,注意只是缓解一点而已,实际还是会拖慢redis,但2、3问题加重
2.区间大5-20ms,1问题缓解很多,但2、3一样加的更重


基于以上就要进行优化了,我的优化方案:
1.改进自旋,不是加java的锁,改为自旋本地变量,不旋redis了,这样就可以固定时间休眠,且短时间,比如休眠1ms,这就基本解决了1问题,2问题还存在但基本也不是大问题了
2.使用队列,让请求按队列方式去请求锁,就没3的问题了

注意这里也还是要用redis锁的,只是改进自旋,不是第一个请求就一定能拿到锁的,因为是多节点的服务。

优化方案后面新开

原文地址:https://www.cnblogs.com/zhaojj/p/7874800.html