douya0808

douya0808 查看完整档案

深圳编辑沈阳航空航天大学  |  软件工程 编辑  |  填写所在公司/组织填写个人主网站
编辑

有没有大佬给个Java开发岗的工作?

个人动态

douya0808 赞了回答 · 10月30日

解决Java:请教这段代码中 lock 的究竟是哪个对象

请教在线程 t1 中的 lock.lock() 究竟是锁的哪个对象?

Lock lock = new ReentrantLock();

这个不是吗?

lambda表达式可以访问调用者的局部变量。

关注 2 回答 1

douya0808 赞了文章 · 10月30日

1000个并发线程,10台机器,每台机器4核,设计线程池大小

这是why哥的第 71 篇原创文章

一道面试题

兄弟们,怎么说?

我觉得如果你工作了两年左右的时间,或者是突击准备了面试,这题回答个八成上来,应该是手到擒来的事情。这题中规中矩,考点清晰,可以说的东西不是很多。

但是这都上血书了,那不得分析一波?

先把这个面试题拿出来一下:

1000 多个并发线程,10 台机器,每台机器 4 核,设计线程池大小。

这题给的信息非常的简陋,但是简陋的好处就是想象空间足够大。

第一眼看到这题的时候,我直观的感受到了两个考点:

  1. 线程池设计。
  2. 负载均衡策略。

我就开门见山的给你说了,这两个考点,刚好都在我之前的文章的射程范围之内:

《如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答》

《吐血输出:2万字长文带你细细盘点五种负载均衡策略》

下面我会针对我感受到的这两个考点去进行分析。

线程池设计

我们先想简单一点:1000 个并发线程交给 10 台机器去处理,那么 1 台机器就是承担 100 个并发请求。

100 个并发请求而已,确实不多。

而且他也没有说是每 1 秒都有 1000 个并发线程过来,还是偶尔会有一次 1000 个并发线程过来。

先从线程池设计的角度去回答这个题。

要回答好这个题目,你必须有两个最基本的知识贮备:

  1. 自定义线程池的 7 个参数。
  2. JDK 线程池的执行流程。

先说第一个,自定义线程池的 7 个参数。

java.util.concurrent.ThreadPoolExecutor#ThreadPoolExecutor

害,这 7 个参数我真的都不想说了,你去翻翻历史文章,我都写过多少次了。你要是再说不出个头头是道的,你都对不起我写的这些文章。

而且这个类上的 javadoc 已经写的非常的明白了。这个 javadoc 是 Doug Lea 老爷子亲自写的,你都不拜读拜读?

为了防止你偷懒,我把老爷子写的粘下来,我们一句句的看。

关于这几个参数,我通过这篇文章再说最后一次。

如果以后的文章我要是再讲这几个参数,我就不叫 why 哥,以后你们就叫我小王吧。

写着写着,怎么还有一种生气的感觉呢。似乎突然明白了当年在讲台上越讲越生气的数学老师说的:这题我都讲了多少遍了!还有人错?

好了,不生气了,说参数:

  1. corePoolSize:the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set (核心线程数大小:不管它们创建以后是不是空闲的。线程池需要保持 corePoolSize 数量的线程,除非设置了 allowCoreThreadTimeOut。)
  2. maximumPoolSize:the maximum number of threads to allow in the pool。 (最大线程数:线程池中最多允许创建 maximumPoolSize 个线程。)
  3. keepAliveTime:when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating。 (存活时间:如果经过 keepAliveTime 时间后,超过核心线程数的线程还没有接受到新的任务,那就回收。)
  4. unit:the time unit for the {@code keepAliveTime} argument (keepAliveTime 的时间单位。)
  5. workQueue:the queue to use for holding tasks before they are executed. This queue will hold only the {@code Runnable} tasks submitted by the {@code execute} method。 (存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在这里。它仅仅用来存放被 execute 方法提交的 Runnable 任务。所以这里就不要翻译为工作队列了,好吗?不要自己给自己挖坑。)
  6. threadFactory:the factory to use when the executor creates a new thread。 (线程工程:用来创建线程工厂。比如这里面可以自定义线程名称,当进行虚拟机栈分析时,看着名字就知道这个线程是哪里来的,不会懵逼。)
  7. handler :the handler to use when execution is blocked because the thread bounds and queue capacities are reached。 (拒绝策略:当队列里面放满了任务、最大线程数的线程都在工作时,这时继续提交的任务线程池就处理不了,应该执行怎么样的拒绝策略。)

第一个知识贮备就讲完了,你先别开始背,这玩意你背下来有啥用,你得结合着执行流程去理解。

接下来我们看第二个:JDK 线程池的执行流程。

一图胜千言:

关于 JDK 线程池的 7 个参数和执行流程。

虽然我很久没有参加面试了,但是我觉得这题属于必考题吧。

所以如果你真的还不会,麻烦你写个 Demo ,换几个参数调试一下。把它给掌握了。

而且还得多注意由这些知识点引申出来的面试题。

比如从图片也可以看出来,JDK 线程池中如果核心线程数已经满了的话,那么后面再来的请求都是放到阻塞队列里面去,阻塞队列再满了,才会启用最大线程数。

但是你得知道,假如我们是 web 服务,请求是通过 Tomcat 进来的话,那么 Tomcat 线程池的执行流程可不是这样的。

Tomcat 里面的线程池的运行过程是:如果核心线程数用完了,接着用最大线程数,最后才提交任务到队列里面去的。这样是为了保证响应时间优先。

所以,Tomcat 的执行流程是这样的:

其技术细节就是自己重写了队列的 offer 方法。在这篇文章里面说的很清楚了,大家可以看看:

《每天都在用,但你知道 Tomcat 的线程池有多努力吗?》

好的,前面两个知识点铺垫完成了。

这个题,从线程池设计的角度,我会这样去回答:

前面我们说了,10 个机器,1000 个请求并发,平均每个服务承担 100 个请求。服务器是 4 核的配置。

那么如果是 CPU 密集型的任务,我们应该尽量的减少上下文切换,所以核心线程数可以设置为 5,队列的长度可以设置为 100,最大线程数保持和核心线程数一致。

如果是 IO 密集型的任务,我们可以适当的多分配一点核心线程数,更好的利用 CPU,所以核心线程数可以设置为 8,队列长度还是 100,最大线程池设置为 10。

当然,上面都是理论上的值。

我们也可以从核心线程数等于 5 开始进行系统压测,通过压测结果的对比,从而确定最合适的设置。

同时,我觉得线程池的参数应该是随着系统流量的变化而变化的。

所以,对于核心服务中的线程池,我们应该是通过线程池监控,做到提前预警。同时可以通过手段对线程池响应参数,比如核心线程数、队列长度进行动态修改。

上面的回答总结起来就是四点:

  1. CPU密集型的情况。
  2. IO密集型的情况。
  3. 通过压测得到合理的参数配置。
  4. 线程池动态调整。

前两个是教科书上的回答,记下来就行,面试官想听到这两个答案。

后两个是更具有实际意义的回答,让面试官眼前一亮。

基于这道面试题有限的信息,设计出来的线程池队列长度其实只要大于 100 就可以。

甚至还可以设置的极限一点,比如核心线程数和最大线程数都是 4,队列长度为 96,刚好可以承担这 100 个请求,多一个都不行了。

所以这题我觉得从这个角度来说,并不是要让你给出一个完美的解决方案,而是考察你对于线程池参数的理解和技术的运用。

面试的时候我觉得这个题答到这里就差不多了。

接下来,我们再发散一下。

比如面试官问:如果我们的系统里面没有运用线程池,那么会是怎么样的呢?

首先假设我们开发的系统是一个运行在 Tomcat 容器里面的,对外提供 http 接口的 web 服务。

系统中没有运用线程池相关技术。那么我们可以直接抗住这 100 个并发请求吗?

答案是可以的。

Tomcat 里面有一个线程池。其 maxThreads 默认值是 200(假定 BIO 模式):

maxThreads 用完了之后,进队列。队列长度(acceptCount)默认是 100:

在 BIO 的模式下,Tomcat 的默认配置,最多可以接受到 300 (200+100)个请求。再多就是连接拒绝,connection refused。

所以,你要说处理这 100 个并发请求,那不是绰绰有余吗?

但是,如果是每秒 100 个并发请求,源源不断的过来,那就肯定是吃不消了。

这里就涉及到两个层面的修改:

  1. Tomcat 参数配置的调优。
  2. 系统代码的优化。

针对 Tomcat 参数配置的调优,我们可以适当调大其 maxThreads 等参数的值。

针对系统代码的优化,我们就可以引入线程池技术,或者引入消息队列。总之其目的是增加系统吞吐量。

同理,假设我们是一个 Dubbo 服务,对外提供的是 RPC 接口。

默认情况下,服务端使用的是 fixed 线程池,核心线程池数和最大线程数都是 200。队列长度默认为 0:

那么处理这个 100 个并发请求也是绰绰有余的。

同样,如果是每秒 100 个并发请求源源不断的过来,那么很快就会抛出线程池满的异常:

解决套路其实是和 Tomcat 的情况差不多的,调参数,改系统,加异步。

这个情况下的并发,大多数系统还是抗住的。

面试官还可以接着追问:如果这时由于搞促销活动,系统流量翻了好倍,那你说这种情况下最先出现性能瓶颈的地方是什么?

最先出问题的地方肯定是数据库嘛,对吧。

那么怎么办?

分散压力。分库分表、读写分离这些东西往上套就完事了。

然后在系统入口的地方削峰填谷,引入缓存,如果可以,把绝大部分流量拦截在入口处。

对于拦不住的大批流量,关键服务节点还需要支持服务熔断、服务降级。

实在不行,加钱,堆机器。没有问题是不能通过堆机器解决的,如果有,那么就是你堆的机器不够多。

面试反正也就是这样的套路。看似一个发散性的题目,其实都是有套路可寻的。

好了,第一个角度我觉得我能想到的就是这么多了。

首先正面回答了面试官线程池设计的问题。

然后分情况聊了一下如果我们项目中没有用线程池,能不能直接抗住这 1000 的并发。

最后简单讲了一下突发流量的情况。

接下来,我们聊聊负载均衡。

负载均衡策略

我觉得这个考点虽然稍微隐藏了一下,但还是很容易就挖掘到的。

毕竟题目中已经说了:10 台机器。

而且我们也假设了平均 1 台处理 100 个情况。

这个假设的背后其实就是一个负载均衡策略:轮询负载均衡。

如果负载均衡策略不是轮询的话,那么我们前面的线程池队列长度设计也是有可能不成立的。

还是前面的场景,如果我们是运行在 Tomcat 容器中,假设前面是 nginx,那么 nginx 的负载均衡策略有如下几种:

  1. (加权)轮询负载均衡
  2. 随机负载均衡
  3. 最少连接数负载均衡
  4. 最小响应时间负载均衡
  5. ip_hash负载均衡
  6. url_hash负载均衡

如果是 RPC 服务,以 Dubbo 为例,有下面几种负载均衡策略:

  1. (加权)轮询负载均衡
  2. 随机负载均衡
  3. 最少活跃数负载均衡
  4. 最小响应时间负载均衡
  5. 一致性哈希负载均衡

哦,对了。记得之前还有一个小伙伴问我,在 Dubbo + zookeeper 的场景下,负载均衡是 Dubbo 做的还是 zk 做的?

肯定是 Dubbo 啊,朋友。源码都写在 Dubbo 里面的,zk 只是一个注册中心,关心的是自己管理着几个服务,和这几个服务的上下线。

你要用的时候,我把所有能用的都给你,至于你到底要用那个服务,也就是所谓的负载均衡策略,这不是 zk 关心的事情。

不扯远了,说回来。

假设我们用的是随机负载均衡,我们就不能保证每台机器各自承担 100 个请求了。

这时候我们前面给出的线程池设置就是不合理的。

常见的负载均衡策略对应的优缺点、适用场景可以看这个表格:

关于负载均衡策略,我的《吐血输出:2万字长文带你细细盘点五种负载均衡策略》这篇文章,写了 2 万多字,算是写的很清楚了,这里就不赘述了。

说起负载均衡,我还想起了之前阿里举办的一个程序设计大赛。赛题是《自适应负载均衡的设计实现》。

赛题的背景是这样的:

负载均衡是大规模计算机系统中的一个基础问题。灵活的负载均衡算法可以将请求合理地分配到负载较少的服务器上。

理想状态下,一个负载均衡算法应该能够最小化服务响应时间(RTT),使系统吞吐量最高,保持高性能服务能力。

自适应负载均衡是指无论处在空闲、稳定还是繁忙状态,负载均衡算法都会自动评估系统的服务能力,更好的进行流量分配,使整个系统始终保持较好的性能,不产生饥饿或者过载、宕机。

具体题目和获奖团队答辩可以看这里:

题目:https://tianchi.aliyun.com/competition/entrance/231714/information?spm=a2c22.12849246.1359729.1.6b0d372cO8oYGK
 答辩:https://tianchi.aliyun.com/course/video?spm=5176.12586971.1001.1.32de8188ivjLZj&liveId=41090 

推荐大家有兴趣的去看一下,还是很有意思的,可以学到很多的东西。

扩展阅读

这一小节,我截取自《分布式系统架构》这本书里面,我觉得这个示例写的还不错,分享给大家:

这是一个购物商场的例子:

系统部署在一台 4C/8G 的应用服务器上、数据在一台 8C/16G 的数据库上,都是虚拟机。

假设系统总用户量是 20 万,日均活跃用户根据不同系统场景稍有区别,此处取 20%,就是 4 万。

按照系统划分二八法则,系统每天高峰算 4 小时,高峰期活跃用户占比 80%,高峰 4 小时内有 3.2 万活跃用户。

每个用户对系统发送请求,如每个用户发送 30 次,高峰期间 3.2 万用户发起的请求是 96 万次,QPS=960 000/(4x60x60)≈67 次请求,每秒处理 67 次请求,处理流程如下图有所示:

一次应用操作数据库增删改查(CRUD)次数平均是操作应用的三倍,具体频率根据系统的操作算平均值即可。一台应用、数据库能处理多少请求呢?

具体分析如下。

  1. 首先应用、数据库都分别部署在服务器,所以和服务器的性能有直接关系,如 CPU、内存、磁盘存储等。
  2. 应用需要部署在容器里面,如 Tomcat、Jetty、JBoss 等,所以和容器有关系,容器的系统参数、配置能增加或减少处理请求的数目。
  3. Tomcat 部署应用。Tomcat 里面需要分配内存,服务器共 8GB 内存,服务器主要用来部署应用,无其他用途,所以设计 Tomcat 的可用内存为8/2=4GB (物理内存的1/2),同时设置一个线程需要 128KB 的内存。由于应用服务器默认的最大线程数是 1000(可以参考系统配置文件),考虑到系统自身处理能力,调整 Tomcat 的默认线程数至 600,达到系统的最大处理线程能力。到此一台应用最大可以处理 1000 次请求,当超过 1000 次请求时,暂存到队列中,等待线程完成后进行处理。
  4. 数据库用 MySQL。MySQL 中有连接数这个概念,默认是 100 个,1 个请求连接一次数据库就占用 1 个连接,如果 100 个请求同时连接数据库,数据库的连接数将被占满,后续的连接需要等待,等待之前的连接释放掉。根据数据库的配置及性能,可适当调整默认的连接数,本次调整到 500,即可以处理 500 次请求。

显然当前的用户数以及请求量达不到高并发的条件,如果活跃用户从 3.2 万扩大到 32 万,每秒处理 670 次请求,已经超过默认最大的 600 ,此时会出现高并发的情况,高并发分为高并发读操作和高并发写操作。

好了,书上分享的案例就是这样的。

荒腔走板

上周五晚上去看了《金刚川》。

据说拍摄周期只有 2 个月,但是电影整体来说还是挺好看的。前半段比较的平缓,但是后半段高潮迭起。对我而言,是有好几个泪点的。

第一个泪点是“喀秋莎”火箭炮出来的时候,像烟花一样,战士们说:这就是我们的喀秋莎吧?

第二个泪点是张译扮演的张飞,一个人对抗美军侦察机,在高炮上高呼:“千古流芳莽撞人”的时候。

用老张的话说:不要以为这是神剧套路,历史现场比这还要惨烈。

张译的演技,没的说。一个人撑起了这个片段的一半。影帝预定一个。

第三个泪点就是燃烧弹落到江面,然后随之响起的《我的祖国》 BGM 了:

一条大河波浪宽

风吹稻花香两岸

我家就在岸上住

听惯了艄公的号子

看惯了船上的白帆

这是美丽的祖国

是我生长的地方

配合着整个修桥的故事,简直就是泪点暴击。

实话实话,《金刚川》这个电影不是特别的完美。但是我还是力荐大家去电影院支持这部电影。

因为这部电影,拍在抗美援朝 70 周年纪念的这个特殊节点。能让有幸生活在和平时代的我们知道,现在的和平长安,国富民强不是从天上掉下来的,是 70 年前,那一群只有十七八岁的“最可爱的人”用生命打下来的。

之前看《人民日报》里面的一个短视频,一位老兵说的话特别的感动,他说:

什么是祖国?当我们跨过鸭绿江,看到战火的时候,我身后就是祖国。

和平来之不易,吾辈自当珍惜。

向最可爱的人致敬。

最后说一句(求关注)

好了,看到了这里安排个 “一键三连”吧,周更很累的,不要白嫖我,需要一点正反馈。

才疏学浅,难免会有纰漏,如果你发现了错误的地方,可以在留言区提出来,我对其加以修改。 感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。

我是 why,一个被代码耽误的文学创作者,不是大佬,但是喜欢分享,是一个又暖又有料的四川好男人。

还有,欢迎关注我呀。

查看原文

赞 32 收藏 24 评论 5

douya0808 提出了问题 · 10月30日

解决Java:请教这段代码中 lock 的究竟是哪个对象

public class Solution {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        Thread t1 = new Thread(()->{
            try {
                lock.lock();
                System.out.println("线程一启动");
                TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
                System.out.println("线程一结束");
            } catch (InterruptedException e) {
                System.out.println("线程一中断");
            } finally {
                lock.unlock();
            }
        }); 
        Thread t2 = new Thread(()->{
            try {
                lock.lock();
                System.out.println("线程二启动");
                TimeUnit.SECONDS.sleep(5);
                System.out.println("线程二启动");
            } catch (InterruptedException e) {
                System.out.println("线程二中断");
            } finally {
                lock.unlock();
            }
        }); 
        t1.start();
        t2.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.interrupt();
    }
}

程序输出结果仅有【线程一启动】,线程二不会被执行
请教在线程 t1 中的 lock.lock() 究竟是锁的哪个对象?没看到程序里有显式声明对象

关注 2 回答 1

douya0808 赞了回答 · 10月24日

解决SpringMVC与Servlet的比较

SpringMVC不是为了简单情况下编写代码更简单,而是在复杂需求情况下编写代码更简单。

锤子用起来是简单,如果仅是钉个钉子的话。但造不出复杂的机械。

关注 2 回答 1

douya0808 提出了问题 · 10月24日

解决SpringMVC与Servlet的比较

说是使用Servlet时需要为【用户模块】和【订单模块】分别创建分别创建名为【UserServlet】和【OrderServlet】的Servlet,而使用SpringMVC则可通过【DispatcherController】统一转发请求
image
我的疑问是:使用SpringMVC后确实可以不用再创建两个Servlet了,但不是还得创建两个Controller么?没看出来这里的优势在哪里,望赐教

关注 2 回答 1

douya0808 提出了问题 · 10月24日

解决SpringMVC与Servlet的比较

说是使用Servlet时需要为【用户模块】和【订单模块】分别创建分别创建名为【UserServlet】和【OrderServlet】的Servlet,而使用SpringMVC则可通过【DispatcherController】统一转发请求
image
我的疑问是:使用SpringMVC后确实可以不用再创建两个Servlet了,但不是还得创建两个Controller么?没看出来这里的优势在哪里,望赐教

关注 2 回答 1

douya0808 收藏了文章 · 10月18日

MySQL 死锁产生原因及解决方法

image

一、Mysql 锁类型和加锁分析

1、锁类型介绍:

MySQL有三种锁的级别:页级、表级、行级。

  • 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
  • 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
  • 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

算法:

  • next KeyLocks锁,同时锁住记录(数据),并且锁住记录前面的Gap
  • Gap锁,不锁记录,仅仅记录前面的Gap
  • Recordlock锁(锁数据,不锁Gap)

所以其实 Next-KeyLocks=Gap锁+ Recordlock锁

二、死锁产生原因和示例

1、产生原因:

所谓死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。表级锁不会产生死锁.所以解决死锁主要还是针对于最常用的InnoDB。

  • 死锁的关键在于:两个(或以上)的Session加锁的顺序不一致。
  • 那么对应的解决死锁问题的关键就是:让不同的session加锁有次序
2、产生示例:

案例一

需求:将投资的钱拆成几份随机分配给借款人。

起初业务程序思路是这样的:

投资人投资后,将金额随机分为几份,然后随机从借款人表里面选几个,然后通过一条条select for update 去更新借款人表里面的余额等。

例如两个用户同时投资,A用户金额随机分为2份,分给借款人1,2

B用户金额随机分为2份,分给借款人2,1

由于加锁的顺序不一样,死锁当然很快就出现了。

对于这个问题的改进很简单,直接把所有分配到的借款人直接一次锁住就行了。

Select * from xxx where id in (xx,xx,xx) for update

在in里面的列表值mysql是会自动从小到大排序,加锁也是一条条从小到大加的锁

例如(以下会话id为主键):

Session1:

mysql> select * from t3 where id in (8,9) for update;
+----+--------+------+---------------------+
| id | course | name | ctime               |
+----+--------+------+---------------------+
|  8 | WA     | f    | 2016-03-02 11:36:30 |
|  9 | JX     | f    | 2016-03-01 11:36:30 |
+----+--------+------+---------------------+
rows in set (0.04 sec)

Session2:

select * from t3 where id in (10,8,5) for update;

锁等待中……

其实这个时候id=10这条记录没有被锁住的,但id=5的记录已经被锁住了,锁的等待在id=8的这里,不信请看。

Session3:

mysql> select * from t3 where id=5 for update;

锁等待中

Session4:

mysql> select * from t3 where id=10 for update;
+----+--------+------+---------------------+
| id | course | name | ctime               |
+----+--------+------+---------------------+
| 10 | JB     | g    | 2016-03-10 11:45:05 |
+----+--------+------+---------------------+
row in set (0.00 sec)

在其它session中id=5是加不了锁的,但是id=10是可以加上锁的。

案例二

在开发中,经常会做这类的判断需求:根据字段值查询(有索引),如果不存在,则插入;否则更新。

以id为主键为例,目前还没有id=22的行

Session1:

select * from t3 where id=22 for update;
Empty set (0.00 sec)

session2:

select * from t3 where id=23  for update;
Empty set (0.00 sec)

Session1:

insert into t3 values(22,'ac','a',now());

锁等待中……

Session2:

insert into t3 values(23,'bc','b',now());
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

当对存在的行进行锁的时候(主键),mysql就只有行锁。

当对未存在的行进行锁的时候(即使条件为主键),mysql是会锁住一段范围(有gap锁)

锁住的范围为:

(无穷小或小于表中锁住id的最大值,无穷大或大于表中锁住id的最小值)

如:如果表中目前有已有的id为(11 , 12)

那么就锁住(12,无穷大)

如果表中目前已有的id为(11 , 30)

那么就锁住(11,30)

对于这种死锁的解决办法是:

insert into t3(xx,xx) on duplicate key update `xx`='XX';

用mysql特有的语法来解决此问题。因为insert语句对于主键来说,插入的行不管有没有存在,都会只有行锁

案例三

mysql> select * from t3 where id=9 for update;
+----+--------+------+---------------------+
| id | course | name | ctime               |
+----+--------+------+---------------------+
|  9 | JX     | f    | 2016-03-01 11:36:30 |
+----+--------+------+---------------------+
 
row in set (0.00 sec)

Session2:

mysql> select * from t3 where id<20 for update;

锁等待中

Session1:

mysql> insert into t3 values(7,'ae','a',now());
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

这个跟案例一其它是差不多的情况,只是session1不按常理出牌了,

Session2在等待Session1的id=9的锁,session2又持了1到8的锁(注意9到19的范围并没有被session2锁住),最后,session1在插入新行时又得等待session2,故死锁发生了。

这种一般是在业务需求中基本不会出现,因为你锁住了id=9,却又想插入id=7的行,这就有点跳了,当然肯定也有解决的方法,那就是重理业务需求,避免这样的写法。

案例四

一般的情况,两个session分别通过一个sql持有一把锁,然后互相访问对方加锁的数据产生死锁。

案例五

两个单条的sql语句涉及到的加锁数据相同,但是加锁顺序不同,导致了死锁。

死锁场景如下:

表结构:

CREATE TABLE dltask (
    id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT ‘auto id’,
    a varchar(30) NOT NULL COMMENT ‘uniq.a’,
    b varchar(30) NOT NULL COMMENT ‘uniq.b’,
    c varchar(30) NOT NULL COMMENT ‘uniq.c’,
    x varchar(30) NOT NULL COMMENT ‘data’,   
    PRIMARY KEY (id),
    UNIQUE KEY uniq_a_b_c (a, b, c)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=’deadlock test’;
#a,b,c三列,组合成一个唯一索引,主键索引为id列。

事务隔离级别:

RR (Repeatable Read)

每个事务只有一条SQL:

delete from dltask where a=? and b=? and c=?;

SQL的执行计划:

死锁日志:

众所周知,InnoDB上删除一条记录,并不是真正意义上的物理删除,而是将记录标识为删除状态。(注:这些标识为删除状态的记录,后续会由后台的Purge操作进行回收,物理删除。但是,删除状态的记录会在索引中存放一段时间。) 在RR隔离级别下,唯一索引上满足查询条件,但是却是删除记录,如何加锁?InnoDB在此处的处理策略与前两种策略均不相同,或者说是前两种策略的组合:对于满足条件的删除记录,InnoDB会在记录上加next key lock X(对记录本身加X锁,同时锁住记录前的GAP,防止新的满足条件的记录插入。) Unique查询,三种情况,对应三种加锁策略,总结如下:

此处,我们看到了next key锁,是否很眼熟?对了,前面死锁中事务1,事务2处于等待状态的锁,均为next key锁。明白了这三个加锁策略,其实构造一定的并发场景,死锁的原因已经呼之欲出。但是,还有一个前提策略需要介绍,那就是InnoDB内部采用的死锁预防策略。

  • 找到满足条件的记录,并且记录有效,则对记录加X锁,No Gap锁(lock_mode X locks rec but not gap);
  • 找到满足条件的记录,但是记录无效(标识为删除的记录),则对记录加next key锁(同时锁住记录本身,以及记录之前的Gap:lock_mode X);
  • 未找到满足条件的记录,则对第一个不满足条件的记录加Gap锁,保证没有满足条件的记录插入(locks gap before rec);

死锁预防策略

InnoDB引擎内部(或者说是所有的数据库内部),有多种锁类型:事务锁(行锁、表锁),Mutex(保护内部的共享变量操作)、RWLock(又称之为Latch,保护内部的页面读取与修改)。

InnoDB每个页面为16K,读取一个页面时,需要对页面加S锁,更新一个页面时,需要对页面加上X锁。任何情况下,操作一个页面,都会对页面加锁,页面锁加上之后,页面内存储的索引记录才不会被并发修改。

因此,为了修改一条记录,InnoDB内部如何处理:

  • 根据给定的查询条件,找到对应的记录所在页面;
  • 对页面加上X锁(RWLock),然后在页面内寻找满足条件的记录;
  • 在持有页面锁的情况下,对满足条件的记录加事务锁(行锁:根据记录是否满足查询条件,记- 录是否已经被删除,分别对应于上面提到的3种加锁策略之一);

死锁预防策略:相对于事务锁,页面锁是一个短期持有的锁,而事务锁(行锁、表锁)是长期持有的锁。因此,为了防止页面锁与事务锁之间产生死锁。InnoDB做了死锁预防的策略:持有事务锁(行锁、表锁),可以等待获取页面锁;但反之,持有页面锁,不能等待持有事务锁。

根据死锁预防策略,在持有页面锁,加行锁的时候,如果行锁需要等待。则释放页面锁,然后等待行锁。此时,行锁获取没有任何锁保护,因此加上行锁之后,记录可能已经被并发修改。因此,此时要重新加回页面锁,重新判断记录的状态,重新在页面锁的保护下,对记录加锁。如果此时记录未被并发修改,那么第二次加锁能够很快完成,因为已经持有了相同模式的锁。但是,如果记录已经被并发修改,那么,就有可能导致本文前面提到的死锁问题。

以上的InnoDB死锁预防处理逻辑,对应的函数,是row0sel.c::row_search_for_mysql()。感兴趣的朋友,可以跟踪调试下这个函数的处理流程,很复杂,但是集中了InnoDB的精髓。

剖析死锁的成因

做了这么多铺垫,有了Delete操作的3种加锁逻辑、InnoDB的死锁预防策略等准备知识之后,再回过头来分析本文最初提到的死锁问题,就会手到拈来,事半而功倍。

首先,假设dltask中只有一条记录:(1, ‘a’, ‘b’, ‘c’, ‘data’)。三个并发事务,同时执行以下的这条SQL:

delete from dltask where a=’a’ and b=’b’ and c=’c’;

并且产生了以下的并发执行逻辑,就会产生死锁:

上面分析的这个并发流程,完整展现了死锁日志中的死锁产生的原因。其实,根据事务1步骤6,与事务0步骤3/4之间的顺序不同,死锁日志中还有可能产生另外一种情况,那就是事务1等待的锁模式为记录上的X锁 + No Gap锁(lock_mode X locks rec but not gap waiting)。这第二种情况,也是”润洁”同学给出的死锁用例中,使用MySQL 5.6.15版本测试出来的死锁产生的原因。

此类死锁,产生的几个前提:

  • Delete操作,针对的是唯一索引上的等值查询的删除;(范围下的删除,也会产生死锁,但是死锁的场景,跟本文分析的场景,有所不同)
  • 至少有3个(或以上)的并发删除操作;
  • 并发删除操作,有可能删除到同一条记录,并且保证删除的记录一定存在;
  • 事务的隔离级别设置为Repeatable Read,同时未设置innodb_locks_unsafe_for_binlog参数(此参数默认为FALSE);(Read Committed隔离级别,由于不会加Gap锁,不会有next key,因此也不会产生死锁) -使用的是InnoDB存储引擎;(废话!MyISAM引擎根本就没有行锁)

https://blog.csdn.net/tr1912/...

image

查看原文

douya0808 收藏了文章 · 10月18日

索引失效底层原理分析,这么多年终于有人讲清楚了

前言

吊打面试官又来啦,今天我们讲讲MySQL索引为什么会失效,很多文章和培训机构的教程,都只会告诉你,在什么情况下索引会失效。

比如:没遵循最佳左前缀法则、范围查询的右边会失效、like查询用不到索引等等

但是没有一个人告诉你,索引失效的原理是什么,老哥今天就告诉大家,让你们知其然,还要知其所以然

image.png

单值索引B+树图

单值索引在B+树的结构里,一个节点只存一个键值对

image.png

联合索引

开局一张图,由数据库的a字段和b字段组成一个联合索引
image.png

从本质上来说,联合索引也是一个B+树,和单值索引不同的是,联合索引的键值对不是1,而是大于1个。

a, b 排序分析

a顺序:1,1,2,2,3,3

b顺序:1,2,1,4,1,2

大家可以发现a字段是有序排列,b字段是无序排列(因为B+树只能选一个字段来构建有序的树)

一不小心又会发现,在a相等的情况下,b字段是有序的。

大家想想平时编程中我们要对两个字段排序,是不是先按照第一个字段排序,如果第一个字段出现相等的情况,就用第二个字段排序。这个排序方式同样被用到了B+树里。

分析最佳左前缀原理

先举一个遵循最佳左前缀法则的例子

select * from testTable where a=1 and b=2

分析如下:

首先a字段在B+树上是有序的,所以我们可以通过二分查找法来定位到a=1的位置。

其次在a确定的情况下,b是相对有序的,因为有序,所以同样可以通过二分查找法找到b=2的位置。

再来看看不遵循最佳左前缀的例子

select * from testTable where b=2

分析如下:

我们来回想一下b有顺序的前提:在a确定的情况下。

现在你的a都飞了,那b肯定是不能确定顺序的,在一个无序的B+树上是无法用二分查找来定位到b字段的。

所以这个时候,是用不上索引的。大家懂了吗?
image.png

范围查询右边失效原理

举例

select * from testTable where a>1 and b=2

分析如下:

首先a字段在B+树上是有序的,所以可以用二分查找法定位到1,然后将所有大于1的数据取出来,a可以用到索引。

b有序的前提是a是确定的值,那么现在a的值是取大于1的,可能有10个大于1的a,也可能有一百个a。

大于1的a那部分的B+树里,b字段是无序的(开局一张图),所以b不能在无序的B+树里用二分查找来查询,b用不到索引。

like索引失效原理

where name like "a%"

where name like "%a%"

where name like "%a"

我们先来了解一下%的用途

  • %放在右边,代表查询以"a"开头的数据,如:abc
  • 两个%%,代表查询数据中包含"a"的数据,如:cab、cba、abc
  • %放在左边,代表查询以"a"为结尾的数据,如cba

为什么%放在右边有时候能用到索引

  • %放右边叫做:前缀
  • %%叫做:中缀
  • %放在左边叫做:后缀

没错,这里依然是最佳左前缀法则这个概念

image.png

大家可以看到,上面的B+树是由字符串组成的。

字符串的排序方式:先按照第一个字母排序,如果第一个字母相同,就按照第二个字母排序。。。以此类推

开始分析

一、%号放右边(前缀)

由于B+树的索引顺序,是按照首字母的大小进行排序,前缀匹配又是匹配首字母。所以可以在B+树上进行有序的查找,查找首字母符合要求的数据。所以有些时候可以用到索引。

二、%号放左边

是匹配字符串尾部的数据,我们上面说了排序规则,尾部的字母是没有顺序的,所以不能按照索引顺序查询,就用不到索引。

三、两个%%号

这个是查询任意位置的字母满足条件即可,只有首字母是进行索引排序的,其他位置的字母都是相对无序的,所以查找任意位置的字母是用不上索引的。

总结

这里把一些经典的索引失效案例给大家分析了,希望能引发大家的思考,能够通过这些案例,明白其他情况索引失效的原理。

之后我们在讲讲,如何通过索引查询到数据整个流程,InnoDBMyISAM两个引擎底层索引的实现区别。

授人以鱼不如授人以渔,这一瞬间,老哥感觉自己特别的shuai

image.png

关注我,一个自学进入大厂的高级Java开发工程师

查看原文

douya0808 赞了文章 · 10月13日

spring解决循环依赖为什么要用三级缓存?

关注“苏三说技术”,回复:开发手册、时间管理 有惊喜。

也许有些朋友对spring的循环依赖问题并不了解,让我们先一起看看这个例子。

@Service
public class AService {

    private BService bService;

    public AService(BService bService) {
        this.bService = bService;
    }

    public void doA() {
        System.out.println("call doA");
    }
}

@Service
public class BService {

    private AService aService;

    public BService(AService aService) {
        this.aService = aService;
    }

    public void doB() {
        System.out.println("call doB");
    }
}
@RequestMapping("/test")
@RestController
public class TestController {

    @Autowired
    private AService aService;

    @RequestMapping("/doSameThing")
    public String doSameThing() {
        aService.doA();
        return "success";
    }
}

@SpringBootApplication
public class Application {

    /**
     * 程序入口
     * @param args 程序输入参数
     */
    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
    }
}

我们在运行Application类的main方法启动服务时,报了如下异常:

Requested bean is currently in creation: Is there an unresolvable circular reference?

这里提示得很明显,出现了循环依赖。

什么是循环依赖?

循环依赖是实例a依赖于实例b,实例b又依赖于实例a。

或者实例a依赖于实例b,实例b依赖于实例c,实例c又依赖于实例a。

像这种多个实例之间的相互依赖关系构成一个环形,就是循环依赖。

为什么会形成循环依赖?

上面的例子中AService实例化时会调用构造方法 public AService(BService bService),该构造方法依赖于BService的实例。此时BService还没有实例化,需要调用构造方法public BService(AService aService)才能完成实例化,该构造方法巧合又需要AService的实例作为参数。由于AService和BService都没有提前实例化,在实例化过程中又相互依赖对方的实例作为参数,这样构成了一个死循环,所以最终都无法再实例化了。

spring要如何解决循环依赖?

只需要将上面的例子稍微调整一下,不用构造函数注入,直接使用Autowired注入。

@Service
public class AService {

    @Autowired
    private BService bService;

    public AService() {
    }

    public void doA() {
        System.out.println("call doA");
    }
}

@Service
public class BService {

    @Autowired
    private AService aService;

    public BService() {
    }

    public void doB() {
        System.out.println("call doB");
    }
}

我们看到可以正常启动了,说明循环依赖被自己解决了

spring为什么能循环依赖?

调用applicationContext.getBean(xx)方法,最终会调到AbstractBeanFactory类的doGetBean方法。由于该方法很长,我把部分不相干的代码省略掉了。

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { final String beanName = transformedBeanName(name);    Object bean;

    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance != null && args == null) {
       省略........
      bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
    } else {
       省略........

        if (mbd.isSingleton()) {
          sharedInstance = getSingleton(beanName, () -> {
            try {
              return createBean(beanName, mbd, args);
            }
            catch (BeansException ex) {
              destroySingleton(beanName);
              throw ex;
            }
          });
          bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
        }

        else if (mbd.isPrototype()) {
          // It's a prototype -> create a new instance.
          Object prototypeInstance = null;
          try {
            beforePrototypeCreation(beanName);
            prototypeInstance = createBean(beanName, mbd, args);
          }
          finally {
            afterPrototypeCreation(beanName);
          }
          bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
        }
        else {
          String scopeName = mbd.getScope();
          final Scope scope = this.scopes.get(scopeName);
          if (scope == null) {
            throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
          }
          try {
            Object scopedInstance = scope.get(beanName, () -> {
              beforePrototypeCreation(beanName);
              try {
                return createBean(beanName, mbd, args);
              }
              finally {
                afterPrototypeCreation(beanName);
              }
            });
            bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
          }
          catch (IllegalStateException ex) {
            throw new BeanCreationException(beanName,
                "Scope '" + scopeName + "' is not active for the current thread; consider " +
                "defining a scoped proxy for this bean if you intend to refer to it from a singleton",
                ex);
          }
        }
      }
      catch (BeansException ex) {
        cleanupAfterBeanCreationFailure(beanName);
        throw ex;
      }
    }
    省略........
    return (T) bean;
  }

我们可以看到,该方法一进来会调用getSingleton方法从缓存获取实例,如果获取不到。会判断作用域是否为:单例,多列 或者 都不是,不同的作用域创建实例的规则不一样。接下来,我们重点看一下getSingleton方法。

 public Object getSingleton(String beanName) {
    return getSingleton(beanName, true);
  }
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
      synchronized (this.singletonObjects) {
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
          ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
          if (singletonFactory != null) {
            singletonObject = singletonFactory.getObject();
            this.earlySingletonObjects.put(beanName, singletonObject);
            this.singletonFactories.remove(beanName);
          }
        }
      }
    }
    return singletonObject;
  }

我们发现有三个Map集合:

/** Cache of singleton objects: bean name --> bean instance */
  private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

  /** Cache of singleton factories: bean name --> ObjectFactory */
  private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

  /** Cache of early singleton objects: bean name --> bean instance */
  private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

singletonObjects对应一级缓存,earlySingletonObjects对应二级缓存,singletonFactories对应三级缓存。

上面getSingleton方法的逻辑是:

  1. 先从singletonObjects(一级缓存)中获取实例,如果可以获取到则直接返回singletonObject实例。
  2. 如果从singletonObjects(一级缓存)中获取不对实例,再从earlySingletonObjects(二级缓存)中获取实例,如果可以获取到则直接返回singletonObject实例。
  3. 如果从earlySingletonObjects(二级缓存)中获取不对实例,则从singletonFactories(三级缓存)中获取singletonFactory,如果获取到则调用getObject方法创建实例,把创建好的实例放到earlySingletonObjects(二级缓存)中,并且从singletonFactories(三级缓存)删除singletonFactory实例,然后返回singletonObject实例。
  4. 如果从singletonObjects、earlySingletonObjects和singletonFactories中都获取不到实例,则singletonObject对象为空。

获取实例需要调用applicationContext.getBean("xxx")方法,第一次调用getBean方法,代码走到getSingleton方法时返回的singletonObject对象是空的。然后接着往下执行,默认情况下bean的作用域是单例的,接下来我们重点看看这段代码:

createBean方法会调用doCreateBean方法,该方法同样比较长,我们把不相干的代码省略掉。

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
      throws BeanCreationException {

    BeanWrapper instanceWrapper = null;
    省略......
    
    if (instanceWrapper == null) {
      instanceWrapper = createBeanInstance(beanName, mbd, args);
    }
    final Object bean = instanceWrapper.getWrappedInstance();
    省略........

    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
        isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
      addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }

    Object exposedObject = bean;
    try {
      populateBean(beanName, mbd, instanceWrapper);
      exposedObject = initializeBean(beanName, exposedObject, mbd);
    }
    catch (Throwable ex) {
      省略 .....
    }
    省略 .......
    return exposedObject;
  }

该方法的主要流程是:

  1. 创建bean实例
  2. 判断作用域是否为单例,允许循环依赖,并且当前bean正在创建,还没有创建完成。如果都满足条件,则调用addSingletonFactory将bean实例放入缓存中。
  3. 调用populateBean方法进行依赖注入
  4. 调用initializeBean方法完成对象初始化和AOP增强

我们关注的重点可以先放到addSingletonFactory方法上。

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
      if (!this.singletonObjects.containsKey(beanName)) {
        this.singletonFactories.put(beanName, singletonFactory);
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
      }
    }
  }

该方法的逻辑是判断如果singletonObjects(一级缓存)中找不到实例,则将singletonFactory实例放到singletonFactories(三级缓存)中,并且移除earlySingletonObjects(二级缓存)中的实例。

createBean方法执行完之后,会调用外层的getSingleton方法

我们重点看看这个getSingleton方法

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(beanName, "Bean name must not be null");
    synchronized (this.singletonObjects) {
      Object singletonObject = this.singletonObjects.get(beanName);
      if (singletonObject == null) {
        if (this.singletonsCurrentlyInDestruction) {
          throw new BeanCreationNotAllowedException(beanName,
              "Singleton bean creation not allowed while singletons of this factory are in destruction " +
              "(Do not request a bean from a BeanFactory in a destroy method implementation!)");
        }
        beforeSingletonCreation(beanName);
        boolean newSingleton = false;
        boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
        if (recordSuppressedExceptions) {
          this.suppressedExceptions = new LinkedHashSet<>();
        }
        try {
          singletonObject = singletonFactory.getObject();
          newSingleton = true;
        }
        catch (IllegalStateException ex) {

          singletonObject = this.singletonObjects.get(beanName);
          if (singletonObject == null) {
            throw ex;
          }
        }
        catch (BeanCreationException ex) {
          if (recordSuppressedExceptions) {
            for (Exception suppressedException : this.suppressedExceptions) {
              ex.addRelatedCause(suppressedException);
            }
          }
          throw ex;
        }
        finally {
          if (recordSuppressedExceptions) {
            this.suppressedExceptions = null;
          }
          afterSingletonCreation(beanName);
        }
        if (newSingleton) {
          addSingleton(beanName, singletonObject);
        }
      }
      return singletonObject;
    }
  }

该方法逻辑很简单,就是先从singletonObjects(一级缓存)中获取实例,如果获取不到,则调用singletonFactory.getObject()方法创建一个实例,然后调用addSingleton方法放入singletonObjects缓存中。

 protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
      this.singletonObjects.put(beanName, singletonObject);
      this.singletonFactories.remove(beanName);
      this.earlySingletonObjects.remove(beanName);
      this.registeredSingletons.add(beanName);
    }
  }

该方法会将实例放入singletonObjects(一级缓存),并且删除singletonFactories(二级缓存),这样以后再调用getBean时,都能从singletonObjects(一级缓存)中获取到实例了。

说了这么多,再回到示例中的场景。

spring为什么要用三级缓存,而不是二级缓存?

像示例的这种情况只用二级缓存是没有问题的。

但是假如有这种情况:a实例同时依赖于b实例和c实例,b实例又依赖于a实例,c实例也依赖于a实例。

a实例化时,先提前暴露objectFactorya到三级缓存,调用getBean(b)依赖注入b实例。b实例化之后,提前暴露objectFactoryb到三级缓存,调用getBean(a)依赖注入a实例,由于提前暴露了objectFactorya,此时可以从三级缓存中获取到a实例, b实例完成了依赖注入,升级为一级缓存。a实例化再getBean(c)依赖注入c实例,c实例化之后,提前暴露objectFactoryc到三级缓存,调用getBean(a)依赖注入a实例,由于提前暴露了objectFactorya,此时可以从三级缓存中获取到a实例。注意这里又要从三级缓存中获取a实例,我们知道三级缓存中的实例是通过调用singletonFactory.getObject()方法获取的,返回结果每次都可能不一样。如果不用二级缓存,这里会有问题,两次获取的a实例不一样。

总结:

    只有单例的情况下才能解决循环依赖问题,并且allowCircularReferences要设置成true。

以下情况还是会出现循环依赖:

  1. 构造器注入
  2. 作用域非单例的情况,当然在自定义作用域,自己可以实现避免循环依赖的逻辑
  3. allowCircularReferences参数设置为false

大家喜欢这篇文章的话,烦请关注一下 :苏三说技术

查看原文

赞 12 收藏 8 评论 2

douya0808 提出了问题 · 9月30日

解决计算 [1, n] 区间范围内完全平方数的数量

计算 [1, n] 区间范围内完全平方数的数量,经常看到大佬直接开放,即:
`
sqrt(n)
`
请问这样做的原理是什么呢?为什么 sqrt(n) 等于 [1, n] 区间内完全平方数的数量?

关注 1 回答 1

认证与成就

  • 获得 10 次点赞
  • 获得 134 枚徽章 获得 2 枚金徽章, 获得 39 枚银徽章, 获得 93 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-06-03
个人主页被 1k 人浏览