说下本人现在的基本情况,毕业一年,双非本科,之前是项目外包,坐标上海,目标中大型互联网公司

蚂蚁金服

电话面

主要问做过的项目的一些解决方案(是否有分库分表、是否使用缓存、服务拆分的原则、遇到的有挑战性的技术问题,如何解决)

Docker是如何实现虚拟化的、JVM运行时数据区、JVM垃圾回收算法、用过Dubbo吗、SpringCloud相关原理

蚂蚁金服电话面卒,我们公司目前做的主要是企业内部的报销相关的系统,数据量、并发量都比较小,用的技术栈也没那么多,所以项目解决方案的话相对来说比较吃亏,而且投的岗位应该是个P7左右,与实际情况也不太相符。投阿里还是要慎重,如果真的想去阿里系的公司一定要找靠谱的人帮忙投简历,而且BOSS直聘上很多阿里的人找我要简历,很多也是为了他们自己的kpi

1、Docker是如何实现虚拟化的

详细可以看下这两篇博客:

https://www.jianshu.com/p/ab423c3db59d

https://blog.csdn.net/qq_40378034/article/details/88906329

拍拍贷

电话面

static关键字、final关键字、JVM运行时数据区、JVM垃圾回收算法、计算机网络5层 哪些常见协议、Eureka是CP还是AP、Eureka底层原理、Eureka Server复制底层原理(复制死循环问题怎么解决的)

一面

Eureka底层原理(画架构图)、如何优化Eureka的注册信息同步延迟问题(推模式,HTTP长连接)、计算机网络分层、TCP慢开始、HTTPS SSL/TSL属于哪一层的协议(SSL/TSL工作在应用层与传输层之间)、DNS使用了哪种运输层协议(为什么)、线程切换的时候操作系统做了什么、MySQL宕机事务怎么保存的(redo log)、MySQL事务隔离级别、MySQL事务的特性、Spring事务的传播行为、Redis分布式锁(Redisson、setnx、Lua、Watch dog)、线程池7个参数、线程池底层原理、让你设计一个数据库连接池(当时是对比线程池说的)、工作中用到的设计模式(日常科普@Autowired注入Map)、手写模板方法

二面

两个白板算法 有点拉闸 手写没搞过(一个数组里只有一个元素是奇数个其他都是偶数个,奇数个的找出来;子集 LeetCode78)、消息队列(RabbitMQ或者Kafka)弄过吗 以前学过 最近没复习了

三面

让你设计一个网关、为什么Full GC这么慢、线上OOM问题怎么排查

网络协议相关博客

TCP:https://blog.csdn.net/qq_40378034/article/details/100893573

HTTPS:https://blog.csdn.net/qq_40378034/article/details/100828017

DNS:https://blog.csdn.net/qq_40378034/article/details/100770570

1、白板算法题:LeetCode78:子集

回溯算法的模板

    private void backtrack(路径, 选择列表, result) {
        if (满足结束条件) {
            result.add(路径);
            return;
        }
        for(选择 选择列表){
            做选择
            backtrack(路径, 选择列表);
            撤销选择
        }
    }

题解1

    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        helper(0, new ArrayList<>(), nums, result);
        return result;
    }

    private void helper(int start, List<Integer> tmpList, int[] nums, List<List<Integer>> result) {
        result.add(new ArrayList<>(tmpList));
        for (int i = start; i < nums.length; ++i) {
            tmpList.add(nums[i]);
            helper(i + 1, tmpList, nums, result);
            tmpList.remove(tmpList.size() - 1);
        }
    }

在这里插入图片描述

题解2

    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        int n = nums.length;
        helper(0, n, nums, new ArrayList<>(), result);
        return result;
    }

    private void helper(int depth, int n, int[] nums, List<Integer> tmpList, List<List<Integer>> result) {
      	//递归终止条件
        if (depth == n) {
            result.add(new ArrayList<>(tmpList));
            return;
        }
      	//不包含当前元素
        helper(depth + 1, n, nums, tmpList, result);
      	//做选择
        tmpList.add(nums[depth]);
      	//包含当前元素
        helper(depth + 1, n, nums, tmpList, result);
      	//撤销选择
        tmpList.remove(tmpList.size() - 1);
    }

类似的几个题目

1)LeetCode46:全排列

题解

    public List<List<Integer>> permute(int[] nums) {
        int n = nums.length;
        int[] visited = new int[n];
        List<List<Integer>> result = new ArrayList<>();
        helper(visited, new ArrayList<>(), nums, n, result);
        return result;
    }

    private void helper(int[] visited, List<Integer> tmpList, int[] nums, int n, List<List<Integer>> result) {
        if (tmpList.size() == n) {
            result.add(new ArrayList<>(tmpList));
            return;
        }
        for (int i = 0; i < n; ++i) {
            if (visited[i] == 1) {
                continue;
            }
            visited[i] = 1;
            tmpList.add(nums[i]);
            helper(visited, tmpList, nums, n, result);
            visited[i] = 0;
            tmpList.remove(tmpList.size() - 1);
        }
    }

解决一个回溯问题,实际上就是一个决策树的遍历过程,只需要思考3个问题:

  • 路径:也就是已经做出的选择

  • 选择列表:也就是当前可以做的选择

  • 结束条件:也就是到达决策树底层,无法再做选择的条件

在这里插入图片描述

在这里插入图片描述

上图中2就是路径,记录已经做过的选择,[1,3]就是选择列表,表示当前可以做出的选择;结束条件就是遍历到树的底层,在这里就是选择列表为空的时候

本题中没有显式记录选择列表,而是通过numsvisited推导出当前的选择列表

2)LeetCode47:全排列 II
    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums);
        int n = nums.length;
        int[] visited = new int[n];
        List<List<Integer>> result = new ArrayList<>();
        helper(visited, new ArrayList<>(), nums, n, result);
        return result;
    }

    private void helper(int[] visited, List<Integer> tmpList, int[] nums, int n, List<List<Integer>> result) {
        if (tmpList.size() == n) {
            result.add(new ArrayList<>(tmpList));
            return;
        }
        for (int i = 0; i < n; ++i) {
            if (visited[i] == 1) {
                continue;
            }
            if (i > 0 && nums[i] == nums[i - 1] && visited[i - 1] == 0) {
                continue;
            }
            visited[i] = 1;
            tmpList.add(nums[i]);
            helper(visited, tmpList, nums, n, result);
            visited[i] = 0;
            tmpList.remove(tmpList.size() - 1);
        }
    }
3)LeetCode77:组合
    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> result = new ArrayList<>();
        helper(1, new ArrayList<>(), result, k, n);
        return result;
    }

    private void helper(int start, List<Integer> tmpList, List<List<Integer>> result, int k, int n) {
        if (tmpList.size() == k) {
            result.add(new ArrayList<>(tmpList));
            return;
        }
        for (int i = start; i <= n; ++i) {
            tmpList.add(i);
            helper(i + 1, tmpList, result, k, n);
            tmpList.remove(tmpList.size() - 1);
        }
    }

强烈建议看下LeetCode上这两篇题解

https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-xiang-jie-by-labuladong-2/

https://leetcode-cn.com/problems/subsets/solution/hui-su-si-xiang-tuan-mie-pai-lie-zu-he-zi-ji-wen-t/

我看完之后只想说一句:东哥,永远滴神!

2、为什么Full GC这么慢?

1)、相关定义

1)部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:

  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集
  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集(目前只有CMS收集器会有单独收集老年代的行为)
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集(目前只有G1收集器会有这种行为)

2)整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集

2)、Full GC慢的原因分析

回收算法不一样决定的

1)Minor GC采取的是新生代复制算法

将现有的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收

2)Major GC的老年代采取的是标记整理算法

先需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高

3)区别

两者最大的区别在于前者是用空间换时间后者则是用时间换空间

前者在工作的时候是没有独立的标记与复制阶段的,而是合在一起做一个动作,就叫scavenge。也就是说,每发现一个这次收集中尚未访问过的活对象就直接复制到新地方。这样的工作方式就需要多一份空间

后者在工作的时候则需要分别的标记与压缩阶段,标记阶段用来发现并标记所有活的对象,然后压缩阶段才移动对象来达到压缩的目的(在标记之后就可以按顺序一个个对象滑动到空间的某一侧)。因为已经先遍历了整个空间里的对象图,知道所有的活对象了,所以移动的时候就可以在同一个空间内而不需要多一份空间

所以新生代的回收会更快一点,老年代的回收则会需要更长时间,同时压缩阶段是会暂停应用的,所以给我们应该尽量避免Full GC

参考

https://www.zhihu.com/question/35172533

3、Eureka Server复制死循环问题怎么解决的

com.netflix.eureka.resources.ApplicationResource中的addInstance()方法负责接收注册信息

    @POST
    @Consumes({"application/json", "application/xml"})
    public Response addInstance(InstanceInfo info,
                                @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
        logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
        // validate that the instanceinfo contains all the necessary required fields
        if (isBlank(info.getId())) {
            return Response.status(400).entity("Missing instanceId").build();
        } else if (isBlank(info.getHostName())) {
            return Response.status(400).entity("Missing hostname").build();
        } else if (isBlank(info.getIPAddr())) {
            return Response.status(400).entity("Missing ip address").build();
        } else if (isBlank(info.getAppName())) {
            return Response.status(400).entity("Missing appName").build();
        } else if (!appName.equals(info.getAppName())) {
            return Response.status(400).entity("Mismatched appName, expecting " + appName + " but was " + info.getAppName()).build();
        } else if (info.getDataCenterInfo() == null) {
            return Response.status(400).entity("Missing dataCenterInfo").build();
        } else if (info.getDataCenterInfo().getName() == null) {
            return Response.status(400).entity("Missing dataCenterInfo Name").build();
        }

        // handle cases where clients may be registering with bad DataCenterInfo with missing data
        DataCenterInfo dataCenterInfo = info.getDataCenterInfo();
        if (dataCenterInfo instanceof UniqueIdentifier) {
            String dataCenterInfoId = ((UniqueIdentifier) dataCenterInfo).getId();
            if (isBlank(dataCenterInfoId)) {
                boolean experimental = "true".equalsIgnoreCase(serverConfig.getExperimental("registration.validation.dataCenterInfoId"));
                if (experimental) {
                    String entity = "DataCenterInfo of type " + dataCenterInfo.getClass() + " must contain a valid id";
                    return Response.status(400).entity(entity).build();
                } else if (dataCenterInfo instanceof AmazonInfo) {
                    AmazonInfo amazonInfo = (AmazonInfo) dataCenterInfo;
                    String effectiveId = amazonInfo.get(AmazonInfo.MetaDataKey.instanceId);
                    if (effectiveId == null) {
                        amazonInfo.getMetadata().put(AmazonInfo.MetaDataKey.instanceId.getName(), info.getId());
                    }
                } else {
                    logger.warn("Registering DataCenterInfo of type {} without an appropriate id", dataCenterInfo.getClass());
                }
            }
        }

      	//注册服务信息
        registry.register(info, "true".equals(isReplication));
        return Response.status(204).build();  // 204 to be backwards compatible
    }

上述的代码中从请求头的PeerEurekaNode.HEADER_REPLICATION(实际值为x-netflix-discovery-replication)里取了一个变量isReplication,后面在进行Eureka Server之间信息同步的时候会用到,接着我们来看下注册服务信息的方法

com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl

    @Override
    public void register(final InstanceInfo info, final boolean isReplication) {
        int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
        if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
            leaseDuration = info.getLeaseInfo().getDurationInSecs();
        }
      	//注册服务信息
        super.register(info, leaseDuration, isReplication);
      	//同步注册信息到Eureka Server其他节点
        replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
    }
    private void replicateToPeers(Action action, String appName, String id,
                                  InstanceInfo info /* optional */,
                                  InstanceStatus newStatus /* optional */, boolean isReplication) {
        Stopwatch tracer = action.getTimer().start();
        try {
            if (isReplication) {
                numberOfReplicationsLastMin.increment();
            }
            // If it is a replication already, do not replicate again as this will create a poison replication
          	//如果isReplication为true直接返回
            if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
                return;
            }

            for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
                // If the url represents this host, do not replicate to yourself.
                if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
                    continue;
                }
              	//同步注册信息到Eureka Server其他节点
                replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
            }
        } finally {
            tracer.stop();
        }
    }

最初收到注册信息的Eureka Server的isReplication为false,因此会把节点信息往其他的Eureka Server结点传递

当传递到下一个结点的时候isReplication已经为true,表示该结点信息由其他Eureka Server节点复制过来,这时候下一个节点就不会继续往下传递。这主要是为了避免造成死循环

参考

https://www.jianshu.com/p/985384308677

4、如何优化Eureka的注册信息同步延迟问题

1)、为什么会出现注册信息同步延迟

原因1

为了性能考虑,Eureka Client会从Eureka Server拉取服务注册的信息缓存在本地,每次获取从本地获取,定时更新缓存,配置参数如下:

#缓存清单的更新时间,默认30秒
eureka.client.registry-fetch-interval-seconds=30

原因2

Eureka Client启动后,不是立即向Eureka Server注册的,而是有一个延迟向服务端注册的时间,配置参数如下:

#将实例信息变更同步到Eureka Server的初始延迟时间,默认为40秒
eureka.client.initial-instance-info-replication-interval-seconds=40

源码解析:

com.netflix.discovery.DiscoveryClient中的initScheduledTasks()方法用来初始化所有调度任务,主要有三个:

  • 获取并更新注册信息的缓存清单
  • 心跳续约
  • 实例信息变更同步(注册)
    /**
     * Initializes all scheduled tasks.
     */
    private void initScheduledTasks() {
        if (clientConfig.shouldFetchRegistry()) {
            // registry cache refresh timer
            int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
            int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
          	//获取并更新注册信息的缓存清单的任务
            cacheRefreshTask = new TimedSupervisorTask(
                    "cacheRefresh",
                    scheduler,
                    cacheRefreshExecutor,
                    registryFetchIntervalSeconds,
                    TimeUnit.SECONDS,
                    expBackOffBound,
                    new CacheRefreshThread()
            );
            scheduler.schedule(
                    cacheRefreshTask,
                    registryFetchIntervalSeconds, TimeUnit.SECONDS);
        }

        if (clientConfig.shouldRegisterWithEureka()) {
            int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
            int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
            logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);

            // Heartbeat timer
          	//心跳续约的任务
            heartbeatTask = new TimedSupervisorTask(
                    "heartbeat",
                    scheduler,
                    heartbeatExecutor,
                    renewalIntervalInSecs,
                    TimeUnit.SECONDS,
                    expBackOffBound,
                    new HeartbeatThread()
            );
            scheduler.schedule(
                    heartbeatTask,
                    renewalIntervalInSecs, TimeUnit.SECONDS);

            // InstanceInfo replicator
            instanceInfoReplicator = new InstanceInfoReplicator(
                    this,
                    instanceInfo,
                    clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                    2); // burstSize

            statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
                @Override
                public String getId() {
                    return "statusChangeListener";
                }

                @Override
                public void notify(StatusChangeEvent statusChangeEvent) {
                    if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
                            InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
                        // log at warn level if DOWN was involved
                        logger.warn("Saw local status change event {}", statusChangeEvent);
                    } else {
                        logger.info("Saw local status change event {}", statusChangeEvent);
                    }
                    instanceInfoReplicator.onDemandUpdate();
                }
            };

            if (clientConfig.shouldOnDemandUpdateStatusChange()) {
                applicationInfoManager.registerStatusChangeListener(statusChangeListener);
            }

          	//实例信息变更同步(注册)的任务
            instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
        } else {
            logger.info("Not registering with Eureka server per configuration");
        }
    }
2)、如何优化Eureka的注册信息同步延迟问题

方法1:调整参数

  • 加快缓存清单的更新频率
  • 合理调整启动后向Eureka Server的延迟注册的时间(时间太短了,服务还未完全启动,可能会出现调用失败;时间太长了,服务都启动了好久了还没注册)

方法2:如果我们要优化Eureka的代码,可以采用推拉结合的方式,定时拉取注册信息并缓存的这个策略不变,每当新的实例注册到Eureka Server之后向Eureka Client推送更新的服务清单(推模式,HTTP长连接)

这里具体的实现思路可以借鉴Apollo的Config Service,Config Service提供配置的读取、推送等功能,服务对象是Apollo客户端,主要实现原理如下:

  • 客户端会发起一个Http请求到Config Service的/notifications/v2接口,也就是NotificationControllerV2
  • NotificationControllerV2不会立即返回结果,而是通过Spring的DeferredResult把请求挂起
  • 如果在60秒内没有该客户端关心的配置发布,那么会返回Http状态码304给客户端
  • 如果有该客户端关心的配置发布,NotificationControllerV2会调用DeferredResult的setResult()方法,传入有配置变化的namespace信息,同时该请求会立即返回。客户端从返回的结果中获取到配置变化的namespace后,会立即请求Config Service获取该namespace的最新配置
3)、DeferredResult的使用

当一个请求到达API接口,如果该API接口的return返回值是DeferredResult,在没有超时或者DeferredResult对象设置setResult时,接口不会返回,但是Servlet容器线程会结束,DeferredResult另起线程来进行结果处理,如此以来这个请求不会占用服务连接池太久,如果超时或设置setResult,接口会立即返回

使用DeferredResult的流程

1)浏览器发起异步请求

2)请求到达服务端被挂起

3)向浏览器进行响应,分为两种情况:调用DeferredResult.setResult(),请求被唤醒,返回结果;超时,返回一个你设定的结果

4)浏览得到响应,处理此次响应结果

优化Eureka,采用推拉结合的方式

使用DeferredResult就可以实现:客户端(Eureka Client)请求服务端(Eureka Server)接口,服务端请求被挂起,直到通过监听事件触发(监听注册中心中的服务注册信息是否有改动)调用DeferredResult.setResult(),唤醒请求并返回结果(Eureka Client判断如果有服务注册信息的改动则立即请求拉取最新的服务注册信息)

@Data
public class DeferredResultResponse {
    private Integer code;
    private String msg;

    public enum Msg {
        TIMEOUT("超时"),
        FAILED("失败"),
        SUCCESS("成功");

        @Getter
        private String desc;

        Msg(String desc) {
            this.desc = desc;
        }
    }
}
@RestController
@RequestMapping(value = "/api/deferred-result")
public class DeferredResultController {
    @Autowired
    private DeferredResultService deferredResultService;

    @GetMapping(value = "/get")
    public DeferredResult<DeferredResultResponse> get(@RequestParam String taskId,
                                                      @RequestParam(required = false, defaultValue = "10000") Long timeout) {
        return deferredResultService.process(taskId, timeout);
    }

    @GetMapping(value = "/result")
    public String setResult(@RequestParam String taskId,
                            @RequestParam(required = false, defaultValue = "成功") String desired) {
        deferredResultService.setResult(taskId, desired);
        return "Done";
    }
}
@Service
public class DeferredResultService {

    private static final Map<String, Consumer<DeferredResultResponse>> TASK_MAP = new ConcurrentHashMap<>();

    /**
     * 客户端和服务端保持长连接,请求被挂起
     *
     * @param taskId
     * @param timeout
     * @return
     */
    public DeferredResult<DeferredResultResponse> process(String taskId, Long timeout) {
        DeferredResult<DeferredResultResponse> deferredResult = new DeferredResult<>(timeout);

        //请求超时的回调函数
        deferredResult.onTimeout(() -> {
            TASK_MAP.remove(taskId);
            DeferredResultResponse deferredResultResponse = new DeferredResultResponse();
            deferredResultResponse.setCode(HttpStatus.REQUEST_TIMEOUT.value());
            deferredResultResponse.setMsg(DeferredResultResponse.Msg.TIMEOUT.getDesc());
            deferredResult.setResult(deferredResultResponse);
        });

        Optional.ofNullable(TASK_MAP)
                .filter(t -> !t.containsKey(taskId))
                .orElseThrow(() -> new IllegalArgumentException(String.format("taskId=%s is existing", taskId)));

        //这里的Consumer相当于是传入的DeferredResult对象的地址
        //所以下面setResult方法中"TASK_MAP.get(taskId)"就是上面创建的deferredResult
        TASK_MAP.putIfAbsent(taskId, deferredResult::setResult);

        return deferredResult;
    }

    /**
     * 当调用setResult方法时,上面挂起的请求被唤醒,返回结果
     *
     * @param taskId
     * @param desired
     */
    public void setResult(String taskId, String desired) {
        DeferredResultResponse deferredResultResponse = new DeferredResultResponse();
        if (DeferredResultResponse.Msg.SUCCESS.getDesc().equals(desired)) {
            deferredResultResponse.setCode(HttpStatus.OK.value());
            deferredResultResponse.setMsg(desired);
        } else {
            deferredResultResponse.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
            deferredResultResponse.setMsg(DeferredResultResponse.Msg.FAILED.getDesc());
        }
        if (TASK_MAP.containsKey(taskId)) {
            Consumer<DeferredResultResponse> deferredResultResponseConsumer = TASK_MAP.get(taskId);
            //这里相当于DeferredResult对象的setResult方法
            deferredResultResponseConsumer.accept(deferredResultResponse);
            TASK_MAP.remove(taskId);
        }
    }
}

参考

https://blog.csdn.net/weixin_41922349/article/details/99655326

https://blog.csdn.net/F_Hello_World/article/details/107882187

https://www.cnblogs.com/lewis09/p/10822020.html

https://www.cnblogs.com/stateis0/p/9393871.html

https://blog.csdn.net/qq_26418435/article/details/102601514

https://www.cnblogs.com/theRhyme/p/10846349.html

5、让你设计一个数据库连接池,怎么设计?

其实这种设计题,就是说下现在市面上类似的组件具体是怎么实现的即可

首先,数据库连接池有两个最重要的配置:最小连接数最大连接数,它们控制着从连接池中获取连接的流程:

  • 如果当前连接数小于最小连接数,则创建新的连接处理数据库请求
  • 如果连接池中有空闲连接则复用空闲连接
  • 如果空闲池中没有连接并且当前连接数小于最大连接数,则创建新的连接处理请求
  • 如果当前连接数已经大于等于最大连接数,则按照配置中设定的时间(C3P0 的连接池配置是checkoutTimeout)等待旧的连接可用
  • 如果等待超过了这个设定时间则向用户抛出错误

MySQL有个参数是wait_timeout,控制着当数据库连接闲置多长时间后,数据库会主动地关闭这条连接。这个机制对于数据库使用方是无感知的,所以当我们使用这个被关闭的连接时就会发生错误,如何判断这个连接是否可用:

  • 启动一个线程来定期检测连接池中的连接是否可用,比如使用连接发送select 1的命令给数据库看是否会抛出异常,如果抛出异常则将这个连接从连接池中移除,并且尝试关闭。目前C3P0连接池可以采用这种方式来检测连接是否可用(推荐使用
  • 在获取到连接之后,先校验连接是否可用,如果可用才会执行SQL语句。比如DBCP连接池的testOnBorrow配置项,就是控制是否开启这个验证。这种方式在获取连接时会引入多余的开销,在线上系统中还是尽量不要开启,在测试服务上可以使用

对比线程池:

当提交一个新任务到线程池时,线程池的处理流程如下

  • 线程池判断核心线程池里的线程是否已满且线程都在执行任务。如果不是,则创建一个新的工作线程来执行任务。否则进入下个流程
  • 线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。否则进入下个流程
  • 线程池判断线程池的线程数是否达到最大线程数且线程都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。否则执行拒绝策略

数据库连接池:先创建连接到最小连接数 -> 没有空闲连接,当前连接数小于最大连接数,则创建新的连接 -> 连接数大于等于最大连接数,等待旧的连接可用,等待超时了抛出错误

线程池:先创建线程至核心线程数 -> 工作队列进行排队 -> 工作队列满了,创建线程至最大线程数 -> 达到最大线程数,执行拒绝策略

参考

高并发系统设计40问:https://time.geekbang.org/column/article/144796

7、让你设计一个网关,怎么设计?

1)、API网关的基本功能

API网关的基本功能:单点入口、路由转发、限流熔断、日志监控、安全认证

2)、Zuul1.x网关架构

在这里插入图片描述

在这里插入图片描述

当外部HTTP请求到达API网关服务的时候,首先它会进入第一个阶段pre,在这里它会被pre类型的过滤器进行处理,该类型过滤器的主要目的是在进行请求路由之前做一些前置加工,比如请求的校验等。在完成了pre类型的过滤器处理后,请求进入第二个阶段routing,也就是说之前说的路由请求转发阶段,请求将会被routing类型过滤器处理。这里的具体处理内容就是将外部请求转发到具体服务实例上去的过程,当服务实例将请求结果都返回之后,routing阶段完成,请求进入第三个阶段post。此时请求将会被post类型的过滤器处理,这些过滤器在处理的时候不仅可以获取到请求信息,还能获取到服务实例的返回信息,所以在post类型的过滤器中,可以对处理结果进行一些加工或转换等内容。另外,还有一个特殊的阶段error,该阶段只有在上述三个阶段中发生异常的时候才会触发,但是它的最后流向还是post类型的过滤器,因为它需要通过post过滤器将最终结果返回给请求客户端

参考

微服务架构实战160讲(Zuul架构相关介绍):https://time.geekbang.org/course/detail/100007001-10580

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐