springboot微服务提高数据查询效率的新操作-先查主、后异步合并

此次以mysql数据库为例来讲解如何提升查询效率。

目前大多数数据的查询都是老套路,一个一个的查询数据,然后返回数据,这种查询在很多情况下都会查询到相同的数据。那么有没有把请求查询合并且不会出现查相同数据的方式呢?答案肯定是有的,只是把原本的一次操作换成2次操作就行了,即先查主数据后异步请求合并参数。

什么是先查主数据?举个简单的例子,例如分页查询多个用户信息时,先查询用户信息的总数和用户信息id,然后在根据用户id列表查询用户信息。前一步多请求是同步操作,后一步操作是异步操作,把多个请求根据id查询用户信息放在一次异步操作上,异步操作拿到所有数据后把数据分发给各自请求即可。

先查主数据的好处,一是减少查询多个字的耗时,二是可以统一缓存处理。大多数情况下,多个请求查询用户信息所用的字段不一样,这样缓存数据也不一样,间接导致了缓存数据的管理混乱问题。

多请求异步的好处就是一次把多个请求数据查询出来,不会查到相同的数据,坏处就是要把需要用到的用户信息查全。看似是坏处,其实也有好处,主要体现在数据缓存上,多个操作只用到同一份缓存,减少了数据消耗。异步造作主要用到了线程池、队列、Future,把请求参数、查询数据方式、Future封装到一个数据上,然后放到队列,线程池从队列拿到请求封装数据进行处理,需要注意的是不是立即处理,而是需要等待一定时间,当等待条件不满足后就把多个请求归类,按分类型理,只有分类后才能把同一类型的参数合并并查询,然后把同类型的请求数据分到各请求封装Future上。

以上就是主要思路,接下来看代码实现。

1是Future、2是封装对象、3是把封装对象放到队列、4是从Future拿到结果。

接下来看看AsyncTo的内容,如下图所示:

future负责拿结果、optType负责查询那个方法、参数的话看实际情况,这里的参数是idList。

接下来看看AsyncOpt的内容,如下所示:

ini 复制代码
package com.provider.man.async;

import com.alibaba.fastjson.JSON;
import com.entity.system.UserEntity;
import com.provider.man.common.AsyncEnum;
import com.provider.man.common.PoolEnum;
import com.provider.man.common.RedisUtil;
import com.provider.man.mapper.system.UserMapper;
import com.to.AsyncTo;
import com.util.CommonUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.util.List;
import java.util.Set;
import java.util.Map;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Component
public class AsyncOpt {

    private static final Logger log = LoggerFactory.getLogger(AsyncOpt.class);
    // 线程池
    @Resource
    private ThreadPoolTaskExecutor executor;
    // 请求封装队列
    @Resource(name = "asyncTaskQueue")
    private BlockingQueue<AsyncTo> queue;
    // 从数据库查询用户数据的接口
    @Resource(name = "userMapper")
    private UserMapper userMapper;

    @PostConstruct
    public void inti() {
        // 启动task0任务来执行任务,注意时间设置,任务数量多大或者时间过长都会导致各个请求响应变慢,需要根据实际仔细斟酌。
        task0(30, 50, TimeUnit.MILLISECONDS, 2);
    }

    /**
     * @param taskCount             任务数量,达到该数量就执行
     * @param timeUnit              任务阻塞时间单位
     * @param interruptTimeInterval 当没有任务时,等待时间依次按此时间间隔递增
     */
    protected void task0(int taskCount, int maxWaitTime, TimeUnit timeUnit, int interruptTimeInterval) {
        Runnable task = () -> {
            List<AsyncTo> list = new ArrayList<>(taskCount);
            // task0会一直执行
            while (true) {
                try {
                    log.info("task0 start");
                    // 阻塞队列,直到拿到请求封装对象
                    list.add(queue.take());
                    // 当任务数量未达到阈值,或者时间没有到达,且任务不为null,添加到list,数量+1
                    // 按等待条件等待,有数据就拿数据,直到不满足就退出,执行数据处理
                    // 当阻塞时,等待时间依次按时间间隔递增阻塞
                    // 等待时间超过最大等待时间时,重新开始循环
                    long maxWaitMillis = timeUnit.toMillis(maxWaitTime);
                    long intervalMillis = timeUnit.toMillis(interruptTimeInterval);
                    long waitedMillis = 0;
                    long pollTimeout = intervalMillis;
                    int index = 0;
                    // 等待条件, 不满足退出,执行后面数据操作
                    while (list.size() < taskCount && waitedMillis < maxWaitMillis) {
                        AsyncTo po = queue.poll(pollTimeout, timeUnit);
                        if (po != null) {
                            list.add(po);
                            waitedMillis = 0;          // 取到数据,重置累计等待
                            pollTimeout = intervalMillis; // 重置递增超时
                        } else {
                            waitedMillis += pollTimeout;
                            pollTimeout = Math.min(pollTimeout + intervalMillis, maxWaitMillis - waitedMillis);
                        }
                        log.info("index: {} waitedMillis: {}, pollTimeout: {}", index++, waitedMillis, pollTimeout);
                    }
                } catch (InterruptedException e) {
                    log.error("task0 err: ", e);
                }
                if (list.size() > 0) {
                    // 数据处理
                    doWith(list, taskCount);
                    // 清空列表,执行上面的相同逻辑
                    list.clear();
                }
            }
        };
        // 执行task0任务,
        executor.execute(task);
    }

    // 把任务添加到队列
    public void addAsyncTask(AsyncTo asyncTo) {
        this.queue.add(asyncTo);
    }

    // 具体的执行逻辑
    private void doWith(List<AsyncTo> taskList, int taskCount) {
        Map<String, List<AsyncTo>> taskMap = new HashMap<>();
        // 请求封装分类
        taskList.forEach(task -> {
            List<AsyncTo> list = taskMap.get(task.getOptType());
            if (list == null) {
                list = new ArrayList<>(taskCount);
                taskMap.put(task.getOptType(), list);
            }
            list.add(task);
        });
        // 
        for (Map.Entry<String, List<AsyncTo>> ety : taskMap.entrySet()) {
            String key = ety.getKey();
            List<AsyncTo> list = ety.getValue();
            // 按类型开启新任务价到队列并执行
            Runnable curTask = () -> {
                // 从redis查询数据
                if (AsyncEnum.multiGetFromRedis.getKey().equals(key)) {
                    multiGet(list);
                } else if (AsyncEnum.multiGetUserFromDatabase.getKey().equals(key)) {
                    // 从mysql查询数据
                    multiGetUserFromDatabase(list);
                }
            };
            executor.execute(curTask);
        }
    }

    private void multiGetUserFromDatabase(List<AsyncTo> list) {
        Set<Long> idSet = new HashSet<>();
        List<Long> allIdList = new ArrayList<>();
        int index = 0;
        for (AsyncTo to : list) {
            List<Long> toIds = to.getIdList();
            log.info("multiGetUserFromDatabase {} id: {}", index++, JSON.toJSONString(toIds));
            // 把id合并到一起并去重复
            idSet.addAll(toIds);
        }
        allIdList.addAll(idSet);
        log.info("multiGetUserFromDatabase {} total id: {}", index, JSON.toJSONString(allIdList));
        // 实际mysql查询
        List<UserEntity> resultList = userMapper.queryUserEntity(allIdList);
        if (!CollectionUtils.isEmpty(resultList)) {
            // 有数据
            setResult(list, resultList);
        } else {
            // 没数据
            setNullResult(list);
        }
    }

    private void multiGet(List<AsyncTo> list) {
        // id合并且去重复
        List<String> cacheIds = new ArrayList<>();
        StringBuilder stringBuilder = new StringBuilder();
        Set<Long> idSet = new HashSet<>();
        int index = 0;
        for (AsyncTo to : list) {
            List<Long> toIds = to.getIdList();
            log.info("multiGet {} id: {}", index++, JSON.toJSONString(toIds));
            for (Long id : toIds) {
                // id去重复
                if (idSet.contains(id)) {
                    continue;
                }
                stringBuilder.append(to.getCacheKey());
                stringBuilder.append(id.toString());
                String str = stringBuilder.toString();
                cacheIds.add(str);
                idSet.add(id);
                stringBuilder.setLength(0);
            }
        }
        // 从redis获取数据
        List<String> resultList= RedisUtil.multiGet(cacheIds);
        if (!CollectionUtils.isEmpty(resultList)) {
            // 结果合并解析
            String jsonStr = CommonUtil.listJsonStrToArrayJsonStr(resultList, stringBuilder);
            List<UserEntity> userList = JSON.parseArray(jsonStr, UserEntity.class);
             // 有数据
            setResult(list, userList);
        } else {
            // 没数据
            setNullResult(list);
        }
    }

    // 处理mysql数据
    private void setResult(List<AsyncTo> list, List<UserEntity> userList) {
        // curMap存放所有结果
        Map<Long, UserEntity> curMap = new HashMap<>(userList.size());
        userList.forEach(u -> {
            curMap.put(u.getId(), u);
        });

        List<UserEntity> curList = new ArrayList<>(userList.size());
        for (AsyncTo to : list) {
            // 根据id获取实际数据,完成数据分发
            to.getIdList().forEach(id -> {
                curList.add(curMap.get(id));
            });
            if (CollectionUtils.isEmpty(curList)) {
                // 实际数据为null, 返回null,必须进行complete()
                to.getFuture().complete(null);
            } else {
                // 实际数据不为null 返回实际数据
                List<UserEntity> returnList = new ArrayList<>(curList.size());
                curList.forEach(c -> {
                    returnList.add(c);
                });
                // 数据写入,complete
                to.getFuture().complete(returnList);
            }
            curList.clear();
        }
    }

    private void setNullResult(List<AsyncTo> list) {
     // 实际数据为null 返回null,必须进行complete()
        for (AsyncTo to : list) {
            to.getFuture().complete(null);
        }
    }

    @PreDestroy
    public void destroy() {
        log.info("destroy");
        executor.shutdown();
        executor = null;
        queue = null;
    }

}

以上就是全部的代码了,需要注意的是任务总数和时间的配置。此外这种方式跟业务深度耦合,没做到通用处理,后面如果有优化,也会发出来和各位看官一起交流。

这就是我的一点小见解,各位看官有更666的操作可以私聊我。有问题可以一起讨论。

条条大路通罗马,想看看官的罗马。

相关推荐
暧暧内含光2 小时前
Redis Cluster 原理与跨槽(CROSSSLOT)踩坑总结
后端
花间相见2 小时前
【计算基础|MySQL17】三大日志:undo / redo / binlog
数据库·后端·面试
用户5947614425952 小时前
Gitea Push Mirror 镜像同步卡住:磁盘写满导致 LevelDB 队列异常的排查与恢复
后端
shark_chili2 小时前
从需求到架构:我如何用 AI 写好design.md
后端
夕颜1112 小时前
一次跨 Mac 的单站点登录态迁移记录:从「能打开」到「重启后仍可用」
后端
武子康3 小时前
LingBot-VLA 2.0 怎么从 6B 权重走到真实机器人?55 维动作只是起点
人工智能·后端·agent
学长毕设4 小时前
基于springboot的社会志愿者管理系统-附源码
java·spring boot·后端
喵了几个咪4 小时前
GoWind CMS 拆解:给 NL2SQL 上四把锁——智能问数的安全边界
开发语言·后端·安全·微服务·golang·gowind
用户8356290780514 小时前
Python 操作 Excel:创建数组公式与 R1C1 公式
后端·python