在多线程环境中从Redis List弹出值会将重复值返回给少数线程

时间:2018-03-07 14:57:57

标签: redis spring-batch

在DB中有一些记录与cId&我得到了一些复杂的数据库查询。 用例是获取与cId&对应的dataId的完整列表。 mdd组合并将其推送到redis。 我们得到了cId& mId从输入csv文件到我的批处理作业的组合。在文件中将有多个记录对应于相同的组合 批处理作业配置有10个并行线程和一个读取记录的线程。我们想要的只是当一个线程从文件读取特定组合时,我们从数据库获取所有记录并将它们上传到redis以解决2个问题: Db点击 2.并发问题,即两个线程不应该基于cId& amp;来自db获得相同的记录。 mId组合

private static Long DEFAULT_REDIS_OBJECTID = 0L;


        public DataObject getDataObject(C cId, M mId) {
                String redisListKey = new StringBuilder().append(LIST-).append(cId)
                        .append("-").append(mId).toString();
    if (BooleanUtils.isFalse(redisTemplate.hasKey(redisListKey))) {
                pushRedisData(cId, mId, redisListKey);
            }
        }
    Long dataId = redisTemplate.opsForList().leftPop(redisListKey);
            if (Objects.isNull(dataId ) || 0L.equals(dataId )) {
//    Again creating the key in order to make sure another thread request for this should not shouldn't go in the db as we know we dont have data in db for this combination         
redisTemplate.opsForList().leftPush(redisListKey, 0L);
                //create a new dataObject and return it
            } else {
                //Get the dataObject based on dataId and return it
            }


    public synchronized void pushRedisData(Long cId, Long mId, String redisListKey) {
            if (BooleanUtils.isFalse(redisTemplate.hasKey(redisListKey))) {
                List<Long> dataToPush = dataService.getDataIdListFromCIdAndMIdCombination(cId,
                        mId);
                if (CollectionUtils.isNotEmpty(dataToPush )) {
                    redisTemplate.opsForList().leftPushAll(redisListKey, dataToPush );
                    redisTemplate.expire(redisListKey, 5, TimeUnit.HOURS);
                } else {
                    if (redisTemplate.opsForList().size(redisListKey) == 0) {
                        redisTemplate.opsForList().leftPush(redisListKey, DEFAULT_REDIS_ITEMID);
                    }

                }

我已经使用synchronized方法将记录推送到redis,这样只有一个线程能够将数据发布到与key相对应的redis,而其他线程只是从redis中弹出数据。 如果数据库中没有找到任何cId&amp; mId组合,然后我在redis上使用默认的0值创建密钥,这样具有这种组合的线程就不应该进行数据库调用。

问题:当我在文件&amp;中执行1000条记录的批处理作业时配置10个线程来处理这些记录,我看到3-5个线程获得已经分配给其他线程的重复dataId,并且对于具有重复dataId的线程,对象处于Stalestate异常的处理结果中。 我还发现在前几个记录的作业开始阶段遇到了这个问题。

1 个答案:

答案 0 :(得分:0)

TL; DR

由于同步泄漏,您正在遇到竞争条件。

解释

您尝试同步的方式是漏洞,这允许多个线程执行相同的工作。这是你已经注意到的事情。没有原子块可以防止两个并发线程执行相同的语句 - 这里没有提到锁。

如果您使用相同的对象实例但这不是解决方案,那么介绍synchronized方法可能会在本地解决该问题。它将阻止其他并发线程阻止这些进程。

为什么不通过例如Redis的属性进行同步?一套?

可以使用Redis集来确保只有一个线程/进程能够通过将该特定元素添加到Redis集来处理元素。 Redis将回应是否添加该元素是否成功。此信息将通过检查响应来帮助您解决竞争问题。如果元素可以添加到集合中,那么当前线程是第一个触及dataId的线程,并且可以继续执行昂贵的工作(数据库获取,...)。

当向同步集添加元素失败时,您知道其他一些进程已经在执行昂贵的工作,您可以继续从缓存中查找数据。您需要注意,虽然另一个线程可能已经赢得了非阻塞同步,但是另一个第一个线程不一定要填充缓存。然后你可以:

  1. 轮询缓存,直到值存在。
  2. 暂停(您无法确定其他进程是否处于活动状态)然后执行重复工作以继续执行您的实际任务。
  3. 还要考虑更糟糕的情况:等待缓存值并阻止导入进度或多次查询昂贵的数据源。您可以针对这两种情况进行优化,但您需要自己决定。