分布式锁优化方案

先来段提神醒脑的问题场景描述:

在业务的某一环节,员工获取某张表的数据进行处理,要求不同的员工获取不同(id)的数据。(比如张三获取了id=1的这条数据,其它员工就不能获取该数据,转而获取其它)

STOP!!!

读者可以先思考下,如果是你,会怎么处理这个问题。之后,再和笔者的优化方案作比较。相信我,这样更有收获。

 

 

 

 

 

 

 

 

##############             我是给读者思考空间的帅气的分割线         ################################

 

因为是集群环境,需通过分布式锁(基于redis)进行处理。

 

原代码的逻辑如下:
获取锁setnx(lock_key,overtime),成功后获取list,然后get(0)。finally中释放锁。

 

原逻辑中,先获取锁,成功后再拿列表数据,取第1条。比较容易能想到,如果对列表list中的每个对象单独加锁,多个线程间会有更少的资源竞争,性能也因此提升。
于是构思出方案一:单独对象锁方案

步骤1:
连接redis,如果此处抛异常,进行重试操作3次;重试3次依然不成,中断。
连接成功,从redis存储的指定set集合(payment_handler_set)上获取数据。

步骤2:
如果步骤1的payment_handler_set为空,或者payment_handler_set不存在,则从数据库获取符合条件的数据。

    如果无符合条件数据,return "NoData";
    否则,将这些数据的id组成set,放入redis。
    【注:set元素最大可以包含(2的32次方-1)个元素,目测不会有溢出问题;但考虑到步骤3的乱序操作,这里从数据库获取符合条件的前300条数据】
       
步骤3:
前两部结束后,或获得java代码中的paymentSet,作为数据副本。对paymentSet作乱序操作。

【注:加上“乱序操作”,不同的线程获取的list中元素的对象次序随机,减少资源竞争】

步骤4:
Payment p = null;
循环paymentSet,依次获取对象锁,setnx(对象id,超时时间)。
A.返回1,获取成功;
    首先检查该对象在数据库中的状态,是否还符合条件;
    【注:这里的检查是有必要的。
    线程t1在步骤2从数据库中获取了java版副本paymentSet1,同时t2获取了paymentSet2。然后paymentSet1中的id1处理完数据,锁已释放;paymentSet2就不应该再处理id1数据了】
        
    如果符合条件,根据id获取payment给p赋值,break。
    否则,表示该对象已处理完,从redis中的payment_handler_set中移除当前对象,释放该对象锁,continue;

B.返回0,获取失败,表示该对象正在被其它线程处理,continue。

循环结束,判断p是否为null:
    如果是,表示paymentSet中无可用对象,return "NoData";
    否则,进行业务处理,finally中释放锁。

 

方案 一 over!!

其实,方案一的核心思路,就像前文说的,对list中的每个对象加锁。

 

 

那是一个阳光明媚的周六,本以为搞定方案一后,可以宣告收工,去吃个火锅唱个歌了……但是!!!

笔者在检查方案一,查阅redis相关资料的时候想到:似乎还有更好的方案。

方案二:进阶的操作锁(推荐)

payment_handler_dataset:存放待处理的数据
payment_handler_operset:存放正在处理的数据

步骤1:
连接redis,如果此处抛异常,进行重试操作3次;重试3次依然不成,中断。
连接成功,从redis存储的指定set集合(payment_handler_dataset)上获取数据。

步骤2:
如果步骤1的payment_handler_dataset为空,或者payment_handler_dataset不存在,则从数据库获取符合条件,并且不在payment_handler_operset中的数据。

    如果无符合条件数据,return "NoData";
    否则,以setnx(update_handler_set,超时时间)方式,获取更新数据操作锁:
        返回1,获取成功:将这些数据的id组成set,放入redis中的payment_handler_dataset(考虑到,可能有线程卡死的情况,数据以watch方式更新)。finally中释放锁。

【注:set元素最大可以包含(2的32次方-1)个元素,目测不会有溢出问题。但数据过多的话,这一步可以限制set的大小,比如:只取前500条数据(具体的限制到多少,根据实际情况调整)】
        返回0,获取失败,表示redis中的该集合数据,正由其它线程更新。可以sleep(1000),return 步骤1。

步骤3:

payment_handler_dataset以SPOP命令(随机移除并返回一个元素)获取元素String randId

       如果randId为nil,表示集合中已经无元素,return 步骤1。

       否则,先将randId放入payment_handler_operset,表示该数据正在被操作。
             然后根据randId获取payment,进行相应的业务处理。
             finally中将randId移出payment_handler_operset,表示该数据操作完成。
       【注:这里可以用多线程来写,设置超时时间,作线程中断】

 

方案有了,只差代码。笔者懒,读者先自行脑补吧(或许我以后会补上具体代码实现(⊙﹏⊙)b)……

#########################################

现把当年承诺的代码补上:

/**
 * getPayment:(获取数据). <br/>
 *
 * @author liuzijian
 * @since JDK 1.8
 */
public Payment getPayment(Long tenantId,Long userId,Integer status,boolean isQingDan){
    final String dataSetKey = getByTenant(wait_oper, tenantId,status, isQingDan);   //待操作
    final String operingSetKey = getByTenant(doing_oper, tenantId,status, isQingDan);   //操作中
    Payment res = null;
    try {
	long id = redisClient.spop(dataSetKey);
	if(id==0){  
	    /** 无数据 **/
	    logger.info("redis中“{}”中已无数据,尝试从数据库中获取",dataSetKey);
	    
	    /** 查询数据库中待处理数据,最多获取500条 **/    //TODO 这里可以优化成单独线程写,其它线程等待
	    List<Long> idList = getMapper().getWaitOperData(tenantId, status, isQingDan);
	    /** 过滤掉在redis已操作集合中的 **/
	    Iterator<Long> itea = idList.iterator();
	    while(itea.hasNext()){
		Long tempId = itea.next();
		if(redisClient.sIsMember(operingSetKey, tempId)){
		    logger.info("过滤掉redis已操作集合中的数据:id={}",tempId);
		    itea.remove();
		}
	    }
	    
	    if(CollectionUtils.isEmpty(idList)){   //数据库中无符合条件的数据
		logger.info("数据库中同样无status={}的数据,返回null",status);
		return res;
	    }else{
		try {
		    final String updateLock = getByTenant(update, tenantId, status, isQingDan);
		    if(distributedLock.tryLock(updateLock, 3000)){
			logger.info("成功获取了分分布式锁lock={},对redis数据set={}进行更新,更新的内容ids={}:",updateLock,dataSetKey,idList);
			redisClient.sAdd(dataSetKey, idList.toArray());
		    }else{
			TimeUnit.MILLISECONDS.sleep(500L);
		    }
		    
		    return getPayment(tenantId,userId,status,isQingDan);  //数据更新后,再次调用本方法,重新获取
		} catch (DistributedLockException e) {
		    logger.error("获取分布式锁error:"+Utils.getFullErrorMessage(e));
		} catch (InterruptedException e) {
		    logger.error("线程沉睡error:"+Utils.getFullErrorMessage(e));
		}
	    }
	    
	}else{
	    /** 有数据 **/
	    res = findOne(id);
	    logger.info("获取了id={}的数据",id);
				
	    redisClient.sAdd(operingSetKey, id);    //记录正在操作的payment
	    
	    /** 业务逻辑部分,记录操作人员等 start **/
	    /** 状态修改,操作人员记录 **/
	    if (PaymentStatus.INPUT_WAIT.getValue().equals(status)){
		res = checkDataId(res);
		res.setEntryBy(userId);
		res.setStatus(PaymentStatus.INPUT_ING.getValue());
	    } else if (PaymentStatus.CHECK_WAIT.getValue().equals(status)) {
		res.setVerifyBy(userId);
		res.setStatus(PaymentStatus.CHECK_ING.getValue());
	    }
	    res.setUserId(userId);
	    updateSelective(res);
	    /** 业务逻辑部分,记录操作人员等 end **/

	    redisClient.sRem(operingSetKey, id);    //数据库记录id后,redis中可清掉id
	    
	}
    } catch (CacheException e) {
	logger.error("redis随机弹出元素时error:"+Utils.getFullErrorMessage(e));
    }
    return res;
}

 

转载于:https://my.oschina.net/u/2463098/blog/775423

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值