暂时未有相关云产品技术能力~
五、主从复制工作原理1. 主从复制的三个阶段主从复制完整的工作流程分为以下三个阶段。每一段都有自己的内部工作流程,那么我们会对这三个过程进行谈论。建立连接过程:这个过程就是slave跟master连接的过程数据同步过程:是master给slave同步数据的过程命令传播过程:是反复同步数据2. 第一阶段:建立连接过程上图是一个完整主从复制建立连接工作流程。然后使用简短的话语来描述上边的工作流程。设置master的地址和端口,保存master的信息建立socket连接(这个连接做的事情下文会说)持续发送ping命令身份验证发送slave端口信息在建立连接的过程中,从节点会保存master的地址和端口、主节点master保存从节点slave的端口。3. 第二阶段:数据同步阶段过程这张图是详细描述第一次从节点连接主节点时的数据同步过程。当从节点第一次连接主节点时,先会执行一次全量复制这次的全量复制是无法避免的。全量复制执行完成后,主节点就会发送复制积压缓冲区的数据,然后从节点就会执行bgrewriteaof恢复数据,这也就是部分复制。在这个阶段提到了三个新点,全量复制、部分复制、复制缓冲积压区。会在下文的常见问题里详细说明这几个点。4. 第三阶段:命令传播阶段当master数据库被修改后,主从服务器的数据不一致后,此时就会让主从数据同步到一致,这个过程称之为命令传播。master会将接收到的数据变更命令发送给slave,slave接收命令后执行命令,让主从数据达到一致。命令传播阶段的部分复制在命令传播阶段出现断网的情况,或者网络抖动时会导致连接断开(connection lost)这个时候主节点master还是会继续往replbackbuffer(复制缓冲积压区)写数据从节点会继续尝试连接主机(connect to master)当从节点把自己的runid和复制偏移量发送给主节点,并且执行pysnc命令同步如果master判断偏移量是在复制缓冲区范围内,就会返回continue命令。并且发送复制缓冲区的数据给从节点。从节点接收数据执行bgrewriteaof,恢复数据六. 详细介绍主从复制原理(全量复制+部分复制)这个过程就是主从复制最齐全的流程讲解。那么下来我们对每一步进程简单的介绍从节点发送指令psync ? 1 psync runid offset 找对应的runid索取数据。但是这里可以考虑一下,当从节点第一次连接的时候根本就不知道主节点的runid 和 offset 。所以第一次发送的指令是psync ? 1意思就是主节点的数据我全要。主节点开始执行bgsave生成RDB文件,记录当前的复制偏移量offset主节点这个时候会把自己的runid 和 offset 通过 +FULLRESYNC runid offset 指令 通过socket发送RDB文件给从节点。从节点接收到+FULLRESYNC 保存主节点的runid和offset 然后清空当前所有数据,通过socket接收RDB文件,开始恢复RDB数据。在全量复制后,从节点已经获取到了主节点的runid和offset,开始发送指令 psync runid offset主节点接收指令,判断runid是否匹配,判断offset是否在复制缓冲区中。主节点判断runid和offset有一个不满足,就会在返回到步骤2继续执行全量复制。这里的runid不匹配只有的可能是从节点重启了这个问题后边会解决,offset(偏移量)不匹配就是复制积压缓冲区溢出了。 如果runid或offset校验通过,从节点的offset和主节点的offset相同时则忽略。 如果runid或offset检验通过,从节点的offset与offset不相同,则会发送 +CONTINUE offset(这个offset为主节点的),通过socket发送复制缓冲区中从节点offset到主节点offset的数据。从节点收到+CONTINUE 保存master的offset 通过socket接收到信息后,执行bgrewriteaof,恢复数据。1-4是全量复制 5-8是部分复制在主节点的第3步下面 主节点在主从复制的期间是一直在接收客户端的数据,主节点的offset是一直变化的。只有有变化就会给每个slave进行发送,这个发送的过程称之为心跳机制七. 心跳机制在命令传播阶段是,主节点与从节点之间一直都需要进行信息互换,使用心跳机制进行维护,实现主节点和从节点连接保持在线。master心跳指令:ping默认10秒进行一次,是由参数repl-ping-slave-period决定的主要做的事情就是判断从节点是否在线可以使用info replication 来查看从节点租后一次连接时间的间隔,lag为0或者为1就是正常状态。slave心跳任务指令:replconf ack {offset}每秒执行一次主要做的事情是给主节点发送自己的复制偏移量,从主节点获取到最新的数据变更命令,还做一件事情就是判断主节点是否在线。心跳阶段的注意事项主节点为保障数据稳定性,当从节点挂掉的数量或者延迟过高时。将会拒绝所有信息同步。这里有俩个参数可以进行配置调整:min-slaves-to-write 2min-slaves-max-lag 8这俩个参数表示从节点的数量就剩余2个,或者从节点的延迟大于8秒时,主节点就会强制关闭maste功能,停止数据同步。那么主节点是如何知道从节点挂掉的数量和延迟时间呢! 在心跳机制里边slave 会每隔一秒发送perlconf ack 这个指令,这个指令可携带偏移量,也可以携带从节点的延迟时间和从节点的数量。八、部分复制的三个核心要素1. 服务器的运行id (run id)我们先看一下这个run id是什么,执行info命令即可看到。在上文中我们查看启动日志信息也可以看到。redis在启动时会自动生成一个随机的id(这里需要注意的是每次启动的id都会不一样),是由40个随机的十六进制字符串组成,用来唯一识别一个redis节点。在主从复制初次启动时,master会把自己的runid发送给slave,slave会保存master的这个id,我们可以使用info命令查看当断线重连时,slave把这个id发送给master,如果slave保存的runid与master现在的runid相同,master会尝试使用部分复制(这块能否复制成功还有一个因素就是偏移量)。如果slave保存的runid与master现在的runid不同,则会直接进行全量复制。2. 复制积压缓冲区复制缓冲积压区是一个先进先出的队列,用户存储master收集数据的命令记录。复制缓冲区的默认存储空间是1M。可以在配置文件修改repl-backlog-size 1mb来控制缓冲区大小,这个比例可以根据自己的服务器内存来修改,咔咔这边是预留出了30%左右。复制缓冲区到底存储的是什么?当执行一个命令为set name kaka时,我们可以查看持久化文件查看那么复制积压缓冲区就是存储的aof持久化的数据,并且以字节分开,并且每个字节都有自己的偏移量。这个偏移量也就是复制偏移量(offset)那为什么会说复制缓冲积压区有可能会导致全量复制呢在命令传播阶段,主节点会把收集的数据存储到复制缓冲区中,然后在发送给从节点。就是这里出现了问题,当主节点数据量在一瞬间特别大的时候,超出了复制缓冲区的内存,就会有一部分数据会被挤出去,从而导致主节点和从节点的数据不一致。从而进行全量复制。如果这个缓冲区大小设置不合理那么很大可能会造成死循环,从节点就会一直全量复制,清空数据,全量复制。3. 复制偏移量(offset)主节点复制偏移量是给从节点发送一次记录一次,从节点是接收一次记录一次。用于同步信息,对比主节点和从节点的差异,当slave断联时恢复数据使用。这个值也就是来自己于复制缓冲积压区里边的那个偏移量。九. 主从复制常见的问题1. 主节点重启问题(内部优化)当主节点重启后,runid的值将发生变化,会导致所有的从节点进行全量复制。这个问题我们无需考虑,知道系统是怎么优化的即可。在建立完主从复制后主节点会创建master-replid变量,这个生成的策略跟runid一样,长度是41位,runid长度是40位,然后发送给从节点。在主节点执行shutdown save命令时,进行了一次RDB持久化会把runid 和 offset保存到RDB文件中。可以使用命令redis-check-rdb查看该信息。主节点重启后加载RDB文件,将文件中的repl-id 和repl-offset加载到内存中。纵使让所有从节点认为还是之前的主节点。2. 从节点网络中断偏移量越界导致全量复制由于网络环境不佳,从节点网络中断。复制积压缓冲区内存过小导致数据溢出,伴随着从节点偏移量越界,导致全量复制。有可能会导致反复的全量复制。解决方案:修改复制积压缓冲区的大小:repl-backlog-size设置建议:测试主节点连接从节点的时间,获取主节点每秒平均产生的命令总量write_size_per_second复制缓冲区空间设置 = 2 * 主从连接时间 * 主节点每秒产生的数据总量3. 频繁的网路中断由于主节点的cpu占用过高,或者从节点频繁连接。出现这种情况造成的结果就是主节点各种资源被严重占用,其中包括但不限于缓冲区,宽带,连接等。为什么会出现主节点资源被严重占用?在心跳机制中,从节点每秒会发送一个指令replconf ack指令到主节点。从节点执行了慢查询,占用大量的cpu主节点每秒调用复制定时函数replicationCron,然后从节点长时间没有相应。解决方案:设置从节点超时释放设置参数:repl-timeout这个参数默认为60秒。超过60秒,释放slave。4. 数据不一致问题由于网络因素,多个从节点的数据会不一致。这个因素是没有办法避免的。关于这个问题给出俩个解决方案:第一个数据需要高度一致配置一台redis服务器,读写都用一台服务器,这种方式仅限于少量数据,并且数据需高度一直。第二个监控主从节点的偏移量,如果从节点的延迟过大,暂时屏蔽客户端对该从节点的访问。设置参数为slave-serve-stale-data yes|no。 这个参数一但设置就只能响应info slaveof等少数命令。5. 从节点故障这个问题直接在客户端维护一个可用节点列表,当从节点故障时,切换到其他节点进行工作,这个问题在后边集群会说到。十. 总结本文主要讲解了什么是主从复制、主从复制工作的三大阶段以及工作流程、部分复制的三大核心。命令传播阶段的心跳机制。最后说明了主从复制常见问题。耗时俩天写的文章,这也是咔咔最近耗时最长的一篇文章,以后咔咔发的文章估计都是这样的,不会在把一问题单独出多篇文章来讲解,会一篇文章全部说完。不完善知识点或者错误知识点,随着咔咔的知识点增多在回来改善。文章主要是为了咔咔回顾方便。有什么问题评论区见。咔咔希望是大家共同交流学习,不对的可以指出来,不喜勿喷。
相信很多小伙伴都已经配置过主从复制,但是对于redis主从复制的工作流程和常见问题很多都没有深入的了解。咔咔这次用时俩天时间给大家整理一份redis主从复制的全部知识点。本文实现所需环境centos7.0redis4.0一、什么是Redis主从复制?主从复制就是现在有俩台redis服务器,把一台redis的数据同步到另一台redis数据库上。前者称之为主节点(master),后者为从节点(slave)。数据是只能master往slave同步单向。但是在实际过程中是不可能只有俩台redis服务器来做主从复制的,这也就意味这每台redis服务器都有可能会称为主节点(master)下图案例中,我们的slave3既是master的从节点,也是slave的主节点。先知道这么个概念,更多详解继续查看下文。二、为什么需要Redis主从复制?假设我们现在就一台redis服务器,也就是单机状态。在这种情况下会出现的第一个问题就是服务器宕机,直接导致数据丢失。如果项目是跟¥占关系的,那造成的后果就可想而知。第二个情况就是内存问题了,当只有一台服务器时内存肯定会到达峰值的,不可能对一台服务器进行无限升级的。所以针对以上俩个问题,我们就多准备几台服务器,配置主从复制。将数据保存在多个服务器上。并且保证每个服务器的数据是同步的。即使有一个服务器宕机了,也不会影响用户的使用。redis可以继续实现高可用、同时实现数据的冗余备份。这会应该会有很多疑问,master跟slave怎么连接呢? 如何同步数据呢? 假如master服务器宕机了呢?别着急,一点一点解决你的问题。三、Redis主从复制的作用在上边我们说了为什么使用redis的主从复制,那么主从复制的作用就是针对为什么使用它来讲了。我们继续使用这个图来谈论第一点是数据冗余了,实现了数据的热备份,是持久化之外的另一种方式。第二点是针对单机故障问题。当主节点也就是master出现问题时,可以由从节点来提供服务也就是slave,实现了快速恢复故障,也就是服务冗余。第三点是读写分离,master服务器主要是写,slave主要用来读数据,可以提高服务器的负载能力。同时可以根据需求的变化,添加从节点的数量。第四点是负载均衡,配合读写分离,有主节点提供写服务,从节点提供读服务,分担服务器负载,尤其在写少读多的情况下,通过多个从节点分担读负载,可以大大提高redis服务器的并发量和负载。第五点是高可用的基石,主从复制是哨兵和集群能够实施的基础,因此我们可以说主从复制是高可用的基石。四、配置Redis主从复制说了这么多,我们先简单的配置一个主从复制案例,然后在谈实现的原理。redis存储路径为:usr/local/redis日志跟配置文件存储在:usr/local/redis/data首先我们先配置俩个配置文件,分别为redis6379.conf 和 redis6380.conf修改配置文件,主要就是修改端口。为了查看方便在把日志文件和持久化文件的名字都用各自的端口来做标识。然后分别开启俩个redis服务,一个端口为6379,一个端口为6380。执行命令redis-server redis6380.conf,然后使用redis-cli -p 6380连接,因为redis的默认端口就是6379所以我们启动另外一台redis服务器直接使用redis-server redis6379.conf 然后直接使用redis-cli直接连接就可以。这个时候我们就成功的配置了俩个redis服务,一台为6380,一台为6379,这里只是为了演示。实际工作中是需要配置在俩台不同的服务器的。1. 使用客户端命令行启动我们先得有一个概念,就是在配置主从复制时,所有的操作都是在从节点来操作,也就是slave。那么我们在从节点执行一个命令为 slaveof 127.0.0.1 6379,执行完就代表我们连接上了。我们先测试一下看是否实现主从复制。在master这台服务器上执行俩个set kaka 123 和 set master 127.0.0.1,然后在slave6380端口是可以成功获取到的,也就说明我们的主从复制就已经配置完成了。但是在实现生产环境可不是就这样完事了,后边会在进一步对主从复制进行优化,直到实现高可用。2. 使用配置文件启用在使用配置文件启动主从复制之前呢!先需要把之前使用客户端命令行连接的断开,在从主机执行slaveof no one即可断开主从复制。在哪可以查看从节点已经断开了主节点呢!在主节点的客户端输入命令行info查看这张图是使用从节点使用客户端命令行连接主节点后,在主节点的客户端输入info打印的信息,可以看到有一个slave0的一个信息。这个图是在从节点执行完slaveof no one 后,在主节点打印的info,说明从节点已经跟主节点断开连接了。在根据配置文件启动redis服务,redis-server redis6380.conf当在从节点重新启动后就可以在主节点直接查看到从节点的连接信息。测试数据,主节点写的东西,从节点还是会自动同步的。3. 启动redis服务器时启动这种方式配置也是很简单,在启动redis服务器时直接就启动主从复制,执行命令:redis-server --slaveof host port 即可。4. 主从复制启动后的日志信息查看这个是主节点的日志信息这个是从节点的信息,其中有连接主节点信息,还有RDB快照保存。
大家好,我是咔咔 不期速成,日拱一卒之前ElasticSearch系列文章中提到了如何处理空值,若为Null则会直接报错,因为在ElasticSearch中当字段值为null时、空数组、null值数组时,会将其视为该字段没有值,最终还是需要使用exists或者null_value来处理空值大多数ElasticSearch的数据都来自于各类数据库,这里暂且只针对于MySQL,各个开源软件中都默认兼容各种Null值,空数组等等若从根源上截断就可以省很多事,直到现在很多开发小伙伴还是坚韧不拔的给字段的默认值还是Null本期就来聊一聊为什么不建议给字段的默认值设置为Null本期环境为:MySQL8.0.26一、案例数据创建表userCREATE TABLE `user` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, `age` tinyint(4) unsigned NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci添加数据,共计10条数据,有两条数据的name值为NullINSERT INTO `user` (`name`, `age`) VALUES ('kaka', 26); INSERT INTO `user` (`name`, `age`) VALUES ('niuniu', 26); INSERT INTO `user` (`name`, `age`) VALUES ('yangyang', 26); INSERT INTO `user` (`name`, `age`) VALUES ('dandan', 26); INSERT INTO `user` (`name`, `age`) VALUES ('liuliu', 26); INSERT INTO `user` (`name`, `age`) VALUES ('yanyan', 26); INSERT INTO `user` (`name`, `age`) VALUES ('leilie', 26); INSERT INTO `user` (`name`, `age`) VALUES ('yao', 26); INSERT INTO `user` (`name`, `age`) VALUES (NULL, 26); INSERT INTO `user` (`name`, `age`) VALUES (NULL, 26);一、count数据丢失在这期 MySQL统计总数就用count,别花里胡哨的《死磕MySQL系列 十》 文章中,已经对count的使用说的非常明白了。那借着这个案例,来分析一下为什么数据会丢失,先看结果select count(*) as num1 ,count(name) as num2 from user;使用count字段名时出现了数据丢失,很明显是因为主键ID9、10这两条记录的name值为空造成的。为什么会出现这种情况?当count除了主键字段外,会有两种情况:一种是字段为null,执行时,判断到有可能是null,但还要把值取出来再判断下,不是null的进行累加另一种是字段为not null,执行时,逐行从记录里边读出这个字段,判断不是null,才进行累加此时,咱们遇到的问题是name字段的值存在了null值,所以会走第一种情况,不进行统计null值为什么建议大家都使用count(*)?MySQL对于count做了专门的优化,跟字段不同的是并不是把所有带了*的值取出来,而是指定了count(*)肯定不是null,只需要按行累加即可MySQL团队对count(*)做了什么优化?MySQL系列文章至今已经更新了第十八期了,你有没有猜到原因呢?现在你应该知道主键索引结构中叶子节点存储的是整行数据,而普通索引叶子节点存储的是主键ID那对于普通索引来说肯定会比主键索引小,因为对于MySQL来说,不管遍历哪个索引结果都一样,所以优化器会主动去找到那颗最小的树进行遍历。在逻辑正确的前提下,尽量减少访问数据量,是数据库系统设计通用法则之一。最后给大家留一个问题,为什么Innodb存储引擎不跟Myisam存储一样存储一个count值呢?如果不知道的话,可以看上文提到的count文章二、为distinct打抱不平在开发工作中使用Distinct进行去重的场景十分的少,大多数情况都是使用group by完成的select distinct name from user;可以看到此时的数据依然是正确的,对Null值做了去重的操作为什么要说这个,因为咔咔在其它的平台上看到过有人这么使用count(distinct name,mobile),然后说是统计出来的数据不准确。这种用法依然是count(字段)的用法,distinct本身是会对Null进行去重,去重后依然是需要判断name的值不为null时,才会进行累计。所以,不要把锅甩给distinct三、使用表达式数据丢失在一些值为null时,使用表达式会造成数据的不一致,接下来一起看下select * from user where name != 'kaka';这跟我们的预期结果不大一致,预期是想返回id2~10的数据当然,这个问题也不是无解,MySQL同样也提供了方法要解决这个问题,只能再加一个条件就是把字段值为null的再单独处理一下四、空指针问题如果一个列存在null值,使用MySQL的聚合函数后返回结果是null,而并非是0,就会造成程序执行时的指针异常CREATE TABLE user_order ( id INT PRIMARY KEY auto_increment, num int ) ENGINE='innodb';insert into user_order(num) values(3),(6),(6),(NULL);创建用户订单数量表,并插入4条数据,接下来演示一下产生的问题select sum(num) from goods where id>4;可以看到当字段为null时,使用聚合函数返回值就是null,并非是0,那么这个问题要怎么处理呢?同样MySQL也给大家提供了对应函数,就是ifnullselect ifnull(sum(num), 0) from goods where id>4;五、这是在难为谁?当一个字段的值存在null值,若要进行null值查询时,必须要使用isnull或者ifnull进行匹配查询,又或者使用is null,is not null。而常用的表达式就不能再进行使用了,有工作经验的还好的,要是新人的话会很难受。接下来看几个新人经常犯的错误错误一对存在null值的字段使用表达式进行过滤,正确用法应该是is null 或者 is not nullselect * from user where name<>null;错误二依然是使用表达式,同样可以使用isnull六、总结说了这么多也都感觉到了字段设置为null的麻烦之处,不过幸好的是MySQL对使用is null、isnull()等依然可以使用上索引。咔咔目前所在的公司存在大量字段默认值就是null,于是代码中就大量存储ifnull、is null、is not null等代码。一般字段数值类型的默认值就给成0,字符串的给个空也行,千万不要给null了哈!
大家好,我是咔咔 不期速成,日拱一卒ElasticSearch致力于搜索的同时,也提供了聚合实时分析数据的功能,聚合可以实现把复杂的数据进行一系列计算后得出我们想要的数据。虽然聚合的功能与搜索完全不同,但使用的数据结构是完全相同的,因此聚合的执行速度很快,也就是说在一次请求中对相同数据可以同时进行搜索+过滤、分析。在ElasticSearch中聚合共分为四大类:Bucket Aggregation:分桶类型,一些列满足特定条件的文档集合Metric Aggregation:指标分析类型,对数据进行数学运算,例如求最大、小值Pipeline Aggregation:管道分析类型,已经聚合的结果进行二次聚合Matix Aggregation:矩阵分析类型,支持对多个字段操作并提供一个结果矩阵先从简开始,看一下Bucket、Metric这两种类型,Bucket实现的结果就是MySQL中group关键字的使用,Metric则是MySQL中max、min函数的使用。一、Buckert Aggregation介绍通过上图可得知将数据分为了三个桶,第一个桶统计的是身高小于300,第二个桶统计的是身高大于600,第三个桶统计的是身高在300到600之间的,在这个案例中就是根据不同的身高分到不同的桶中。使用聚合分析机制还可以按照年龄、地理位置、性别、薪资范围、订单增长情况、工作岗位分布等。只要有一定共同点的数据都可使用聚合进行归档处理。常见的Bucket分桶策略terms:按照term来分桶,如果是text类型则会按照分词后的结果进行分桶range:指定数值的范围来设定分桶规则data range:指定日期的范围来设定分桶规则histogram:固定的间隔来来设定分桶规则data histogram:针对日期的直方图或柱状图Terms根据目的地进行分桶post /kibana_sample_data_flights/_search { "size":0, "aggs":{ "destcountry_term":{ "terms": { "field": "DestCountry" } } }, "profile":"true" } 从返回结果中看到根据目的地将航班信息进行了归类处理,同时也会发现在ElasticSearch中如果不手动定义size值都会默认只返回10条结果"aggregations" : { "destcountry_term" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 3187, "buckets" : [ { "key" : "IT", "doc_count" : 2371 }, { "key" : "US", "doc_count" : 1987 }, { "key" : "CN", "doc_count" : 1096 }, { "key" : "CA", "doc_count" : 944 }, { "key" : "JP", "doc_count" : 774 }, { "key" : "RU", "doc_count" : 739 }, { "key" : "CH", "doc_count" : 691 }, { "key" : "GB", "doc_count" : 449 }, { "key" : "AU", "doc_count" : 416 }, { "key" : "PL", "doc_count" : 405 } ] } } Range想要查询平均价格在300以下、300~600之间、大于600的案例post /kibana_sample_data_flights/_search { "size":0, "aggs":{ "avgticketprice_range":{ "range": { "field": "AvgTicketPrice", "ranges": [ {"to":300}, {"from":300,"to":600}, {"from":600} ] } } } } 返回结果如下,可以三条结果都根据不同的区间设置了key值"aggregations" : { "avgticketprice_range" : { "buckets" : [ { "key" : "*-300.0", "to" : 300.0, "doc_count" : 1816 }, { "key" : "300.0-600.0", "from" : 300.0, "to" : 600.0, "doc_count" : 4115 }, { "key" : "600.0-*", "from" : 600.0, "doc_count" : 7128 } ] } } 可以通过设置keyed:true,使每个区间都返回一个特定的名字post /kibana_sample_data_flights/_search { "size":0, "aggs":{ "avgticketprice_range":{ "range": { "field": "AvgTicketPrice", "keyed":"true", "ranges": [ {"to":300}, {"from":300,"to":600}, {"from":600} ] } } } } 可以好好的跟上一个案例对比一下区别"aggregations" : { "avgticketprice_range" : { "buckets" : { "*-300.0" : { "to" : 300.0, "doc_count" : 1816 }, "300.0-600.0" : { "from" : 300.0, "to" : 600.0, "doc_count" : 4115 }, "600.0-*" : { "from" : 600.0, "doc_count" : 7128 } } } } 当然也可以指定区间的名字post /kibana_sample_data_flights/_search { "size":0, "aggs":{ "avgticketprice_range":{ "range": { "field": "AvgTicketPrice", "keyed":"true", "ranges": [ {"key":"小于300","to":300}, {"key":"300到600之间","from":300,"to":600}, {"key":"大于600","from":600} ] } } } } 返回结果"aggregations" : { "avgticketprice_range" : { "buckets" : { "小于300" : { "to" : 300.0, "doc_count" : 1816 }, "300到600之间" : { "from" : 300.0, "to" : 600.0, "doc_count" : 4115 }, "大于600" : { "from" : 600.0, "doc_count" : 7128 } } } }
大家好,我是咔咔 不期速成,日拱一卒在MySQL中,十分不建议大家给表的默认值设置为Null,这个后期咔咔也会单独出一期文章来说明这个事情。但你进入一家新公司之前的业务中存在大量的字段默认值为Null,把这些值导入ElasticSearch中还是需要处理,接下来就看看ElasticSearch如何应对空值。一、ElasticSearch如何处理Null值的先看一个案例,当值为null时会发生什么POST /kaka/_bulk { "index": { "_id": "1"}} { "tags" : ["search"]} { "index": { "_id": "2"}} { "tags" : ["search", "open_source"] } { "index": { "_id": "3"}} { "tags" : null } { "index": { "_id": "4"}} { "tags" :"null"}在这个案例中可以发现,第3、4都存在一个null值,不同的是一个为字符串nullpost /kaka/_search { "query":{ "term": { "tags": null } }, "profile":"true" }当你执行上面这搜索时会出现下面这个错误{ "error": { "root_cause": [ { "type": "illegal_argument_exception", "reason": "field name is null or empty" } ], "type": "illegal_argument_exception", "reason": "field name is null or empty" }, "status": 400 }然后咔咔就跑到ElasticSearch文档找了一下原因,是因为在ElasticSearch中空值不能被索引或搜索,当字段值为null时、空数组、null值数组时,会将其视为该字段没有值若你执行的语句为如下,则会返回最后一条数据post /kaka/_search { "query":{ "term": { "tags": "null" } }, "profile":"true" }二、使用exists解决ElasticSearch中Null值搜索问题同样在文档中咔咔也找到了答案,案例如下post /kaka/_search { "query":{ "constant_score": { "filter": { "missing": { "field": "tags" } } } } } 执行结果返回no [query] registered for [missing],这就让人有点百思不得其解,再通过啃文档后发现这个接口在ElasticSearch7.1已经被取消了,根据文档的意思是exists可以同时满足存在和不存在两种情况先看使用exists如何查询不为nullpost /kaka/_search { "query":{ "constant_score":{ "filter":{ "exists":{ "field":"tags" } } } } } 再看使用exists查询为null的post /kaka/_search { "query":{ "bool":{ "must_not":{ "exists":{ "field":"tags" } } } } }三、使用null_value替换显示的空值删除上边定义的索引delete kaka,然后自定义mapping,给tags设置"null_value" : "null",用指定的值替换显示的空值,"null"可以自定义为任意值使用了null_value这样做的好处就是空字段也可以被索引,同时也不会在查询时报field name is null or empty的错误put kaka { "mappings":{ "properties":{ "tags" :{ "type":"keyword", "null_value":"null" } } } } 再插入上边的数据POST /kaka/_bulk { "index": { "_id": "1"}} { "tags" : ["search"]} { "index": { "_id": "2"}} { "tags" : ["search", "open_source"] } { "index": { "_id": "3"}} { "tags" : null } { "index": { "_id": "4"}} { "tags" :"null"}再次执行查询为null的数据,就会出现第3、4条数据post /kaka/_search { "query":{ "term": { "tags": "null" } }, "profile":"true" }四、使用null_value注意点null_value必须和定义的数据类型匹配,例如long类型的不能定义字符串类型的value_null值看一下long类型设置了字符串类型value_null会出现什么错误# 错误演示,long类型使用了字符串类型的null_value值 put kaka { "mappings":{ "properties":{ "tags" :{ "type":"long", "null_value":"null" } } } } 返回错误如下{ "error": { "root_cause": [ { "type": "mapper_parsing_exception", "reason": "Failed to parse mapping [_doc]: For input string: \"null\"" } ], "type": "mapper_parsing_exception", "reason": "Failed to parse mapping [_doc]: For input string: \"null\"", "caused_by": { "type": "number_format_exception", "reason": "For input string: \"null\"" } }, "status": 400 } 注意了数据类型外,你还需要知道value_null不是任何类型都可以使用的,以下列举的类型都可使用null_valueArrayBooleanDategeo_pointipkeywordNumericpoint
三、安装Kibana下载方式跟ElasticSearch一样,学会怎么下载这些软件,这里需要注意一点下载的版本需要跟ElasticSearch版本一致。https://artifacts.elastic.co/downloads/kibana/kibana-7.1.0-linux-x86_64.tar.gz所以接下来需要安装Kibana7.1.0版本配置Kibana参数复制到文件最后即可i18n.locale: "zh-CN" server.host: "0.0.0.0" elasticsearch.hosts: ["http://localhost:9200"]启动Kibana进到Kibana目录执行./bin/kibana即可无法启动报错如下[WARN ][o.e.c.c.ClusterFormationFailureHelper] [node-1] master not discovered or elected yet, an election requires a node with id [rEq_ExihQ927BnwBy3Iz7A], have discovered [] which is not a quorum; discovery will continue using [127.0.0.1:9300, 127.0.0.1:9301, 127.0.0.1:9302, 127.0.0.1:9303, 127.0.0.1:9304, [::1]:9300, [::1]:9301, [::1]:9302, [::1]:9303, [::1]:9304] from hosts providers and [{node-1}{DtZPMDK4S3qaSQF6mRhRqw}{lBAhaMvDTKmGkysihkwAqA}{192.168.122.130}{192.168.122.130:9300}{ml.machine_memory=1907744768, xpack.installed=true, ml.max_open_jobs=20}] from last-known cluster state; node term 412, last-accepted version 16 in term 1解决方案:查看ElasticSearch的data目录下有没有之前的节点数据Kibana实例启动如何关闭查看当前端口号执行netstat -anp | grep 5601,最后边就有进程ID,执行kill -9 进程ID即可再重新启动即可。如果把Kibana配置为中文参数已经给到大家了,就是i18n.locale: "zh-CN"这个配置进来后你会看到这个页面点击工具手,就可以操作ElasticSearch数据了,还是非常nice的四、Kibana界面快速浏览进入软件后,可以看到加载数据集进来之后可以看到三个样例数据,分别为日志、电商订单、航班数据接着点击Dashboards就会看到刚添加的三组样本数据接着看一个非常重要的工具dev Tools,这个工具再后期会大量的使用,用处就是帮助你在Kibana中可以很方便的执行一些ElasticSearch的命令五、守护进程启动Kibana、ElasticSearch在启动完ElasticSearch和Kibana后你会发现是直接在当前端口启动的,如果把这个终端关闭就会关闭。接下来借助nohup来实现守护进程启动,注意不是nohup在网上很多的资料写的都是这个,不要被混淆了安装nohup当你执行nohup命令发现没有时,执行yum install -y coreutils一般情况下会安装在/usr/bin/nohup执行which nohup确认安装位置将nohup命令配置为全局,使用最简单的方式,执行#vi ~/.bash_profile # 添加这行代码 PATH=$PATH:$HOME/bin:/use/bin export PATH然后,保存,刷新生效source ~/.bash_profile最后,进行验证,出现版本信息则安装成功nohup --version启动Kibana估计在网看了很多都是让直接执行nohup ./bin/kibana,虽然这样可以让Kibana启动起来,但会出现下图的错误,同时当你ctrl+c时Kibana也会关闭掉需要把错误信息重定向到linux的空洞即可nohup ./bin/kibana > /dev/null 2>&1 &你会发现执行完后出来了一行数字,这个就是Kibana的进程ID,如果你忘记了可以执行ps -ef | grep node来查看,因为Kibana是node起的启动ElasticSearch跟启动Kibana同理,执行nohup ./bin/elasticsearch > /dev/null 2>&1 &
项目中准备使用ElasticSearch,之前只是对ElasticSearch有过简单的了解没有系统的学习,本系列文章将从基础的学习再到深入的使用。咔咔之前写了一份死磕MySQL文章,如今再入一个系列玩转ElasticSearch。本期文章会带给大家安装ElasticSearch、Kibana、Logstash、配置ElasticSearch外网可访问、配置守护进程启动Kibana、ElasticSearch、使用Logstash导入演示数据到ElasticSearch中。一、安装ElasticSearch从0开始搭建一个ElasticSearch环境,接下来先安装。ElasticSearch官网在一直改版,有很多小伙伴找不到下载位置。进去之后点击圈起来的位置,不要点击左边直接下载了进去之后可以看到发布的历史版本,根据自己的需求下载对应的版本即可,这里咔咔下载的是7.1.0版本如果你是Linux,可以打开开发者模式,把地址复制出来,使用wget + 地址 直接下载即可。https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.1.0-linux-x86_64.tar.gz可以看到此时已将ElasticSearch下载下来了,这是已经解压好的到了这一步不要冲动直接就去启动,ElasticSearch从5.x版本开始为了安全起见,不能直接使用root用户启用。添加用户执行useradd es,添加es用户在root用户下把ElasticSearch用户权限给es用户即可chown -R es ElasticSearch启动ElasticSearch切到es用户后,执行执行./bin/elasticsearch即可启动ElasticSearch启动出现初始化密钥库问题Exception in thread "main" org.elasticsearch.bootstrap.BootstrapException: java.nio.file.AccessDeniedException: /usr/local/elasticsearch/elasticsearch-6.6.0/config/elasticsearch.keystore Likely root cause: java.nio.file.AccessDeniedException: /usr/local/elasticsearch/elasticsearch-7.1.0/config/elasticsearch.keystore at sun.nio.fs.UnixException.translateToIOException(UnixException.java:84) at sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:102) at sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:107) at sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:214) at java.nio.file.Files.newByteChannel(Files.java:361) at java.nio.file.Files.newByteChannel(Files.java:407) at org.apache.lucene.store.SimpleFSDirectory.openInput(SimpleFSDirectory.java:77) at org.elasticsearch.common.settings.KeyStoreWrapper.load(KeyStoreWrapper.java:206) at org.elasticsearch.bootstrap.Bootstrap.loadSecureSettings(Bootstrap.java:224) at org.elasticsearch.bootstrap.Bootstrap.init(Bootstrap.java:289) at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:159) at org.elasticsearch.bootstrap.Elasticsearch.execute(Elasticsearch.java:150) at org.elasticsearch.cli.EnvironmentAwareCommand.execute(EnvironmentAwareCommand.java:86) at org.elasticsearch.cli.Command.mainWithoutErrorHandling(Command.java:124) at org.elasticsearch.cli.Command.main(Command.java:90) at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:115) at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:92) Refer to the log for complete error details. 这个版本需要进行安全认证功能需要创建elasticsearch.keystore这个文件所以输入下面的命令./bin/elasticsearch-keystore create查看是否启动成功在浏览器访问127.0.0.1::9200看到如下界面就说明已经安装成功了这里咔咔是安装在centos虚拟机上的,那么如果用外网可以访问到ElasticSearch呢!二、配置外网访问配置外网访问的步骤也很简单,跟着步骤走三分钟搭建好问题一max file descriptors [4096] for elasticsearch process is too low, increase to at least [65535]编辑 /etc/security/limits.conf,追加以下内容es soft nofile 65536es hard nofile 65536问题二max number of threads [3782] for user [es] is too low, increase to at least [4096]意思是elasticsearch最大线程数目太低修改 /etc/security/limits.conf在文件末尾增加以下两行:es soft nproc 4096es hard nproc 4096问题三max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]在/etc/sysctl.conf文件最后添加一行vm.max_map_count=262144问题四the default discovery settings are unsuitable for production use; at least one of [discovery.seed_hosts, discovery.seed_providers, cluster.initial_master_nodes] must be configured意思是配置以下三者,最少其一#[discovery.seed_hosts, discovery.seed_providers, cluster.initial_master_nodes]在elasticsearch的config目录下,修改elasticsearch.yml配置文件node.name: node-1 cluster.initial_master_nodes: ["node-1"] network.host: 0.0.0.0问题五以上操作都执行完了,但外网还访问不了,需要看一下防火墙是否关闭# 关闭防火强systemctl stop firewalld.service# 设置永久关闭systemctl disable firewalld.service这里需要注意一点,问题一和二修改完成后需要重启机器,切记、切记、切记外网访问ElasticSearch可以看到虚拟机的ip是http://192.168.253.129/接下来试着在宿主机上访问虚拟机ip+端口9200看是否可以访问
大家好,我是咔咔 不期速成,日拱一卒本期来聊聊MySQL的加锁规则,知道这些规则后可以判断SQL语句的加锁范围,同时也可以写出更好的SQL语句,防止幻读问题的产生,在能力范围内最大程度的提升MySQL并发处理事务能力。现在你应该知道了MVCC解决了快照读下的幻读问题,但当前读的幻读问题还是基于锁解决的,也就是next-key lock。一、了解next-key lock在文章幻读:听说有人认为我是被MVCC干掉的这期文章中,详细说明了幻读在当前读、快照读下的解决方式。快照读简单来说就是简单的select操作,没有加任何锁,在Innodb存储引擎下执行简单的select操作时,会记录下当前的快照读数据,之后的select会沿用第一次快照读的数据,即使有其它事务提交也不会影响当前的select结果,因此通过快照读查询的数据虽然是一致的,但有可能不是最新的数据,而是历史数据。这个是从官方文档中获取的资料,解释在当前读下Innodb使用next-key lock锁来解决幻读问题。To prevent phantoms, InnoDB uses an algorithm called next-key locking that combines index-row locking with gap locking. InnoDB performs row-level locking in such a way that when it searches or scans a table index, it sets shared or exclusive locks on the index records it encounters. Thus, the row-level locks are actually index-record locks. In addition, a next-key lock on an index record also affects the “gap” before that index record. That is, a next-key lock is an index-record lock plus a gap lock on the gap preceding the index record. If one session has a shared or exclusive lock on record R in an index, another session cannot insert a new index record in the gap immediately before R in the index order.大致意思,为了防止幻读,Innodb使用next-key lock算法,将行锁(record lock)和间隙锁(gap lock)结合在一起。Innodb行锁在搜索或者扫描表索引时,会在遇到的索引记录上设置共享锁或者排它锁,因此行锁实际是索引记录锁。另外, 在索引记录上设置的锁同样会影响索引记录之前的“间隙(gap)”。即next-key lock是索引记录行加上索引记录之前的“gap”上的间隙锁定。二、next-key lock 加锁规则加锁规则总结为以下几点,不同MySQL版本会有微小的差异查询过程中只要访问的数据都会加锁,加锁的基本单位是next-key lock,左开右闭唯一索引等值查询,next-key lock退化为行锁索引等值查询,需要访问到第一个不满足条件的值,此时的next-key lock会退化为间隙锁索引范围查询需要访问到不满足条件的第一个值为止之前看过丁老师的文章说是在唯一索引下,范围查询会访问到不满足条件的第一个值为止,这个问题在MySQL8.0.18已经修复了目前咔咔使用的MySQL版本是 8.0.26 ,接下来根据这几条规则设计几条SQL,一起来看看都锁了那些数据。创建next_key_lock表,建表的初始化语句如下。CREATE TABLE `next_key_lock` ( `id` int(11) NOT NULL AUTO_INCREMENT, `class` tinyint(4) NOT NULL, `name` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, KEY `idx_class` (`class`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; INSERT INTO next_key_lock (`class`,`name`) VALUES (1,'咔咔'),(3,'小刘'),(8,'小张'), (15,'小李'),(20,'张但'),(25,'王五'),(25,'李四');三、唯一索引等值查询下图是SQL的执行流程,分为了三个终端,按照终端顺序来执行SQL分析这条SQL满足那些规则规则一:查询过程中只要访问到的数据都会加锁,加锁的基本单位是next-key lock,左开右闭状态。规则二:唯一索引等值查询,next-key lock退化为行锁。规则三:索引等值查询,需要访问到第一个不满足条件的值,此时的next-key lock会退化为间隙锁根据规则一,加锁范围为(7,∞]根据规则二,退化为行锁,但明显此条SQL不满足条件,因为表里边就不存在id=9的这条记录,所以此条规则不生效根据规则三,next-key lock退化为间隙锁,加锁范围为(7,∞)结论得知唯一索引等值查询时,行数据存在的时候是行锁,行数据不存在,那就是间隙锁。因此终端2的语句会一直处于等待状态,直到终端1执行完成。四、普通索引等值查询分析这条SQL满足那些规则规则一:查询过程中只要访问到的数据都会加锁,加锁的基本单位是next-key lock,左开右闭状态。规则二:索引等值查询,需要访问到第一个不满足条件的值,此时的next-key lock会退化为间隙锁根据规则一,加锁范围是(3,8]根据规则二,需要访问到第一个不满足的值,加锁范围(8,15],又因为会退化为间隙锁,加锁范围变为(8,15)结论三条SQL执行后,你看到的现象是MySQL2执行成功,MySQL3SQL等待MySQL3要加入的值是9,在锁范围内所以需要等MySQL1提交事务后才可执行成功。为什么MySQL2为什么会执行成功总结的加锁规则中,查询过程中访问到的数据都会加锁,但MySQL2使用的覆盖索引,所以并不需要回表查询主键索引,所以主键索引上是没有加任何锁的。你要理解这块就需要知道主键索引、普通索引的索引结构,在B+tree中主键索引叶子节点存储的是整行数据,而普通索引叶子节点存储的是主键的值。扩展现在你知道了在这个例子中,lock in share mode值锁覆盖索引,但是如果是for update就会给主键索引上满足条件的行加上行锁。所以你也知道了使用了覆盖索引是避免不了数据被更新的,若想实现数据避免更新就需要绕过覆盖索引的优化。现在你应该知道使用for update会给主键索引加锁,如果查询条件为普通索引但值是存在多个相同数据的,此时的加锁就会根据主键索引加锁。五、主键索引范围锁从上图得知MySQL2和MySQL3都处于等待MySQL1中分析这条SQL满足那些规则规则一:访问到的数据都会加锁规则二:唯一索引等值查询,next_key_lock退化为行锁规则三:索引范围查询需要访问到不满足条件的第一个值为止根据规则一,加锁范围(7,8]根据规则二,退化为行锁,加锁范围只是id=8这一行(后边解释)根据规则三,范围查询就往后继续找,加锁范围(8,∞]结论此条SQL加锁范围,行锁id=8,next_key lock(8,∞]问题:为什么从next-key lock退化为行锁首先你需要明白所谓的等值判断和范围判断,指的是这一行数据被查询选中的时候走的判断条件是通过 a=b 还是 a>b或a<b 来确定的,直白点就是这行数据是通过等值来的还是范围查询来的。从SQL返回结果可得知数据是根据id=8来的,因此next-key lock会退化为行锁。六、普通索引范围锁执行SQL为select * from next_key_lock where class >= 8 and class<10 for update;可以看到这个SQL跟第五案例的MySQL1的唯一区别是普通索引没有退化行锁的规则。分析这条SQL满足那些规则规则一:索引等值查询需要访问到第一个不满足的值,next_key lock 退化为间隙锁规则二:索引范围查询需要访问到不满足条件的第一个值为止根据规则一,加锁范围(7,8]根据规则二,加锁范围(8,15]结论加锁范围为(7,8]、(8,15]问题:为什么没有退化为间隙锁仔细看规则,索引等值查询需要访问到不满足的值才会退化为间隙锁,此时是可以访问到8这个数据的,因此不会退化为间隙锁。七、普通索引倒叙范围锁在以上的所有案例中都是默认正序规则,接下来看下倒叙时的加锁规则是怎么样的执行SQL为select * from next_key_lock where class >= 15 and class<=20 order by desc lock in share mode;由于SQL加上了order by ,因此第一个要定位class索引最右边的值,也就是class=20,因为class是普通索引等值查询,因此会加上next-key lock 左开右闭(15,20],普通索引等值查询会访问到不满足条件的值为止,所以还会继续扫描,直到遇到25,又会加上一个next-key lock (20,25],又因为25不满足查询条件,因此会退化为间隙锁(20,25)还有一个条件是class >= 15,向左扫描到class = 8才会停下来知道了是小于15了,加锁单位是next-key loc ,左开右闭范围是(3,8]又因为查询是*,绕过了覆盖索引,需要回表查询,因此给主键ID也会加锁,加锁为id=4,id=5两个行锁。结论因此这条SQL加锁范围在索引class是(3,25),主键索引上id=4,5两个行锁。八、总结本期文章带大家了解next_key lock的加锁范围,并且给大家总结了四条加锁规则,经过五个实战案例给再给大家说几个注意点。唯一索引等值查询时next-key lock退化为行锁,这里指查询到数据,若没有查到数据则依然是间隙锁普通索引等值查询next-key lock退化为间隙锁最后一点当SQL加上排序时加锁规则会有一定的变化,在后期文章中咔咔也会不断的提供很多案例供大家查看。
在开发中有遇到很简单的SQL却执行的非常慢,甚至只查询一行数据。咔咔遇到的只有两种情况,一种是MySQL服务器CPU占用率很高,所有的SQL都执行的很慢直到超时,程序也直接502,另一种情况是行锁造成的锁等待。接下来咔咔带领大家看看各种为难SQL执行的场景,本期文章带大家再熟悉一下MySQL中的锁一、MDL锁现在你应该知道要聊的是MDL,这个锁很少有开发人员去关注,在开发中并没有实际的语法来开启或关闭锁。这个特性是在MySQL5.5引入的,目的是为了解决一张表同时在做查询和修改表结构,这种情况必定会造成查询结果跟表结构无法对应。所以,当你访问一个表时会默认加上MDL锁,MDL锁的互斥关系跟共享锁、排它锁是一样的,读写互斥,写写互斥。MDl锁是在事务提交后才会释放,执行期间一直持有。同时你需要知道MDL锁的操作会形成一个队列,队列中写锁获取优先级高于读锁,一旦出现MDL写锁等待,会阻塞后续该表的所有CURL操作。也就说,一旦你在一个未提交事务之后执行了DDL操作,那么等到的结果就是MySQL挂掉,客户端会有重试机制,DDL后所有CURD会在超时后重新发起请求,这个库的线程会很快爆满。当线程A通过DDL时手里握着表的MDL写锁,而线程B的查询需要获取MDL读锁,所以线程B就一直处于锁等待状态。在生产环境是坚决不可以直接修改表结构的,如果你的表非常大的话会很容易造成业务所有的CURD处于堵塞。解决方案大表DDL可以使用pt-online-schema-change这个工具来处理,具体怎么用后续文章会跟大家分享出来。若不小心在线上执行了修改表结构,可以通过show processlist命令来查找,不过这个命令在查找上很不方便,可以使用performance_schema和sys系统库来进行查询。前提是你的MySQL参数performance_schema=on,在MySQL8.0.26版本中,这个参数是默认开启的,若你所在的版本没有开启时可以打开。然后就可以执行select blocking_pid from sys.schema_table_lock_waits,就可以看到当前持有MDL锁的线程ID,直接使用kill命令即可。二、全局锁在MySQL强人“锁”难《死磕MySQL系列 三》的文章中给大家聊到了全局锁,使用语法flush table t with read lock 或者 flush table with read lock指定表名时就锁定指定表,未指定时表示锁定所有表。这两个语句执行是非常快的,一般不会造成SQL堵塞,但防火、防盗你也防不住有其它线程的语句把flush语句堵塞住。线程A执行大事务,需要执行10s线程B执行flush table t with read lock线程C执行select * from evt_sms where id = 1所以线程C哪怕是只查询一条数据在10s内也是返回不了结果的,线程B的flush 命令需要等线程A的事务执行完毕,而线程C此时却被未执行的线程B堵塞着。解决方案一般出现这种情况只需要执行show processlist就可以看到堵塞线程C的线程是那个,同样直接使用kill掉对应的线程即可。三、行锁这个场景是非常好模拟的,接下来让我们一起看看线程A正常修改大批量数据执行语句为update evt_sms set code = 123 where id > 11089线程B执行select * from evt_sms where id = 120365 lock in share mode在文章开头就跟大家简单的说了一句,MySQL中读锁与写锁、写锁与写锁互斥,所以线程B会一直等待线程A的事务提交之后才能返回结果。解决方案分析一下,线程B执行的语句添加的是读锁,能被堵住的只有是写锁,所以可以直接在sys.innodb_lock_waits表中查到占着这个写锁的是谁。执行语句select * from evt_sms sys.innodb_lock_waits where lock_table='kaka.evt_sms'\G这个试验就不演示了,复现过程也十分简答可以自己看一下哈!输出结果的最后一行就是解决方案,带着你的答案来到评论区四、快照读引发的问题了解过MVCC实现原理的大概率都会看到过当前读、快照读这两个词,如果你还不知道它们是什么就好好记一下。当前读执行select语句时加上共享锁、排它锁的操作就是当前读。例:select * from evt_sms where id = 1 lock in share mode这里的共享锁、排它锁也就是常说的读锁、写锁在MySQL的Innodb存储引擎中进行DML操作时会默认添加排它锁上边这个例子,select语句一旦加上了共享锁其它线程是不能修改当前记录的,因此当前读读取的数据库就是最新的数据快照读快照读的前提是隔离级别不是串行级别,串行级别的快照读会退化为当前读,快照读的出现是为了提高事务并发性,其实现也是基于MVCC的MVCC在某种情况下可以认为是行锁的一个变种,但要知道的是在很多情况是不会有加锁行为的这时你应该记住快照读获取的数据不是最新的,有可能是之前版本的数据实现MVCC的三大因素隐式字段、undo log、read-view,read-view就是通过快照读产生的,它是由查询的那一时间所有未提交事务ID组成的数组,和已经创建的最大事务ID组成的。然后通过本线程的事务ID在read-view中进行对比为什么说快照读会引发查询迟迟不返回结果上文给大家提了一个东西undo log,都知道undo log是回滚日志,查询慢的原因也在这里线程A先开启一个事务线程B开启对id为1的数据行进行更新由于id = 1的数据很多所以会产生很多的版本链,这里就认为是5万个线程A执行了select * from evt_sms where id = 1就会迟迟返回不了结果此时线程B并没有提交事务,所以线程A的查询需要根据版本链一直回退到5W个undo log之前,也就是这里导致查询非常慢下图是一个咔咔之前做的undo log版本链图线程A的查询是快照读,执行查询时会产生read-view,read-view会把线程A、线程B的事务存放在一个数组中,然后用一定的规则进行判断线程A能看到的数据是什么。比对规则是什么trx_id为当前的事务ID,min_id、max_id为当前启动事务的最大事务ID和最小事务ID如果落在trx_id<min_id,表示此版本是已经提交的事务生成的,由于事务已经提交所以数据是可见的如果落在trx_id>max_id,表示此版本是由将来启动的事务生成的,是肯定不可见的若在min_id<=trx_id<=max_id时如果row的trx_id在数组中,表示此版本是由还没提交的事务生成的,不可见,但是当前自己的事务是可见的如果row的trx_id不在数组中,表明是提交的事务生成了该版本,可见在这里还有一个特殊情况那就是对于已经删除的数据,在之前的undo log日志讲述时说了update和delete是同一种类型的undo log,同样也可以认为delete就是update的特殊情况。当删除一条数据时会将版本链上最新的数据复制一份,然后将trx_id修改为删除时的trx_id,同时在该记录的头信息中存在一个delete flag标记,将这个标记写上true,用来表示当前记录已经删除。在查询时按照版本链的规则查询到对应的记录,如果delete flag标记位为true,意味着数据已经被删除,则不返回数据。五、总结本期文章通过MDL锁、全局锁、行锁、undo log说明查询一条数据页迟迟不返回的问题,可以看到大多数都是一些理论知识,有些东西看着看着也就理解其中的含义了。这里需要注意的是不要把MDL和DML搞混淆了,这可是两个东西,MDL指的是锁、而DML指的是数据库的增删改查。
群里一个小伙伴在问为什么MySQL字符串不加单引号会导致索引失效,这个问题估计很多人都知道答案。没错,是因为MySQL内部进行了隐式转换。本期文章就聊聊什么是隐式转换,为什么会发生隐式转换。一、几大索引失效原因你肯定在网上看到过非常多关于索引失效原因的文章,但是一定要自己亲手尝试一下,因为版本不同引发的结果不会一致。1.带头大哥不能死这局经典语句是说创建索引要符合最左侧原则。例如表结构为u_id,u_name,u_age,u_sex,u_phone,u_time创建索引为idx_user_name_age_sex。查询条件必须带上u_name这一列。2.不在索引列上做任何操作不在索引列上做任何计算、函数、自动或者手动的类型转换,否则会进行全表扫描。简而言之不要在索引列上做任何操作。3.俩边类型不等例如建立了索引idx_user_name,name字段类型为varchar在查询时使用where name = kaka,这样的查询方式会直接造成索引失效。正确的用法为where name = “kaka”。4.不适当的like查询会导致索引失效创建索引为idx_user_name执行语句为select * from user where name like “kaka%”;可以命中索引。执行语句为select name from user where name like “%kaka”;可以使用到索引(仅在8.0以上版本)。执行语句为select * from user where name like ‘’%kaka";会直接导致索引失效5.范围条件之后的索引会失效创建索引为idx_user_name_age_sex执行语句select * from user where name = ‘kaka’ and age > 11 and sex = 1;上面这条sql语句只会命中name和age索引,sex索引会失效。复合索引失效需要查看key_len的长度即可。总结:%在后边会命令索引,当使用了覆盖索引时任何查询方式都可命中索引。以上就是咔咔关于索引失效会出现的原因总结,在很多文章中没有标注MySQL版本,所以你有可能会看到is null 、or索引会失效的结论。二、从规则方面说明索引失效的原因问题的答案就是第3点,两边类型不一致导致索引失效。下图是表结构,目前这个表存在两个索引,一个主键索引,一个普通索引phone。分别执行以下两条SQL语句explain select * from evt_sms where phone = 13020733815;explain select * from evt_sms where phone = '13020733815';从上图可看出,执行第一条SQL没有使用到索引,第二条SQL却使用到了索引。不错,你也发现了两条SQL的不同,第二条SQL跟第一条SQL逻辑一致,不同的是一个查询条件有引号,一个没有。问题:为什么逻辑相同的SQL却是用不了索引选择索引是优化器大哥的工作,大哥做事肯定轮不到咱们去教,因为大哥有自己的一套规则。对于优化器来说,如果等号两边的数据类型不一致,则会发生隐式转换。例如,explain select * from evt_sms where phone = 13020733815;这条SQL语句就会变为explain select * from evt_sms where cast(phone as signed int) = 13020733815;由于对索引列进行了函数操作,从而导致索引失效。问题:为什么会把左侧的列转为int类型呢?优化器大哥就是根据这个规则进行判断,是把字符串转为数字,还是把数字转为字符串。若返回1,则把字符串转为数字。若返回0,则把数字转为字符串。问题:select * from evt_sms where id = "193014410456945216"这条SQL语句能用上索引吗?如果你忘记了表结构,可以翻到文章开头再看下表evt_sms的索引。可以知道列id添加了主键索引,类型为int类型。根据规则得到,MySQL8.0以上的版本是将字符串转为数字。所以说,函数操作的是等号右边的数据,跟索引列没有关系,所以可以用上索引。那么来到数据库验证一下结论,你答对了吗?三、从索引结构说明索引失效原因有这样一个需求,要统计每年双11注册用户数量。可以看到在evt_sms表中是没有给create_time创建索引的,于是你会执行alter table evt_sms add index idx_ctime(create_time),给create_time添加上索引。接着你就执行了下面的SQL语句。explain select count(*) from evt_sms where month(create_time) = 11;上线没一会数据库出现了大量的慢查询,导致非常多的SQL返回失败。此时公司大牛肯定会直接指出问题,索引列进行函数操作。问题:为什么索引列使用函数就用不上索引了呢?你现在看到的create_time索引结构图。若此时执行的是where create_time = ‘2021-11-16’,那么MySQL就会非常快的等位到对应位置,并返回结果。但是,做了函数操作,例如month(2021-11-16)得到的值是11。当MySQL拿到返回的这个11时,在索引结构中根据就不知道怎么办。MySQL之所以能使用快速定位,是因为B+树的有序性。而使用了函数对索引列进行操作后就会破坏索引的有序性,因此优化器大哥会选择执行代价最低的索引来继续执行。四、结论本期文章给大家介绍了两个案例,一个隐式转换,一个对索引列进行函数操作。两种情况的本质是一样的,都是在索引列上进行了函数操作,导致全表扫描。类似于这两种情况的还是字符集问题,不过一般这个问题会会很少发生,如有新业务需要新创建表,都会设置为之前的字符集。两张表的字符集不同在进行join时也会导致隐式字符集转换,导致索引失效。
有一个问题是这样的统计数据总数用count(*)、count(主键ID)、count(字段)、count(1)那个效率高。先说结论,不用那么花里胡哨遇到统计总数全部使用count(*).但是有很多小伙伴就会问为什么呢?本期文章就解决大家的为什么。一、不同存储引擎的做法你需要知道的是在不同的存储引擎下,MySQL对于使用count(*)返回结果的流程是不一样的。在Myisam中,每张表的总行数都会存储在磁盘上,因此执行count(*)时,是直接从磁盘拿到这个值返回,效率是非常高的。但你也要知道如果加了条件的统计总数返回也不会那么快的。在Innodb引擎中,执行count(*),需要把数据一行一行的读出来,然后再统计总数返回。问题:为什么Innodb不跟Myisam一样把表总数存起来呢?这个问题就需要追溯的我们之前的MVCC文章,就是因为要实现多版本并发控制,才会导致Innodb不能直接存储表总数。因为每个事务获取到的一致性视图都是不一样的,所以返回的数据总数也是不一致的。如果你无法理解,再回到MVCC文章好好看看,意思就跟不同事务看到的数据不一致一回事。实战案例假设这三个用户是并行的,你会看到三个用户看到最终的数据总数都不一致。每个用户会根据read view存储的数据来判断那些数据是自己可以看见的,那些是看不见的。read view当执行SQL语句查询时会产生一致性视图,也就是read-view,它是由查询的那一时间所有未提交事务ID组成的数组,和已经创建的最大事务ID组成的。在这个数组中最小的事务ID被称之为min_id,最大事务ID被称之为max_id,查询的数据结果要根据read-view做对比从而得到快照结果。于是就产生了以下的对比规则,这个规则就是使用当前的记录的trx_id跟read-view进行对比,对比规则如下。如果落在trx_id<min_id,表示此版本是已经提交的事务生成的,由于事务已经提交所以数据是可见的如果落在trx_id>max_id,表示此版本是由将来启动的事务生成的,是肯定不可见的若在min_id<=trx_id<=max_id时如果row的trx_id在数组中,表示此版本是由还没提交的事务生成的,不可见,但是当前自己的事务是可见的如果row的trx_id不在数组中,表明是提交的事务生成了该版本,可见二、MySQL对count(*)做了什么优化先来看两个索引结构,一个是主键索引、另一个是普通索引。现在你应该知道了,主键索引的叶子节点存储的是整行数据,而普通索引叶子节点存储的是主键值。得出结论就是普通索引的比主键索引会小很多。所以,MySQL对于count(*)这样的操作,不管遍历那个索引树得到的结果在逻辑上都一样。因此,优化器会找到最小的那棵树来遍历,在保证正确的逻辑前提下,尽量减少扫描数据量,是数据库系统设计的通用法则之一。问题:为什么存储的有数据怎么不用?这个图的数据怎么得到的,我想你应该知道了,没错,就是执行show table status \G;得来的。那为什么innodb存储引擎不直接使用Rows这个值呢?还记不记得在第六期文章中,五分钟,让你明白MySQL是怎么选择索引《死磕MySQL系列 六》先不要返回去看这篇文章,看下上文图中最后查到的数据总条数是多少。你会发现这两个统计的数据是不一致的,因此这个值肯定是不可以用的。具体原因因为Rows这个值跟索引基数Cardinality一样,都是通过采样统计的。采样规则首先,会选出N个数据页,然后统计每个数据页上不同的值,最后得到一个平均值。再用这个平均值乘索引的数据页总数得到的就是索引基数。并且这个索引基数也不是一成不变的,会随着数据持续增删改,当变更的数据超过1/M时才会触发,M值是根据MySQL参数innodb_stats_persistent得到的,设置为on是10,off是16。在MySQL8.0这个默认值为on,也就是说当这张表的数据变更超过总数据的1/10就会重新触发采样统计。三、不同count的用法以下所有的结论都基于MySQL的Innodb存储引擎。count(主键ID)innodb引擎会遍历整张表,把每一行的ID值都那出来,然后返回给server层,server层拿到ID后,判断不可能为空,进行累加。count(1)同样遍历整张表,但不取值,server层对返回的每一行,放一个数字1进去,判断是不可能为空的,按行累加。count(字段)分为两种情况,字段定义为not null和null为not null时:逐行从记录里面读出这个字段,判断不能为null,累加为 null时:执行时,判断到有可能是null,还要把值取出来再判断一下,不是null才累加。count(*)这个哥们就厉害了,不是带了*就把所有值取出来,而是MySQL做了专门的优化,count ( * )肯定不是null,按行累加。结论按照效率的话,字段 < 主键ID < 1 ~ ,最好都使用count(),别花里胡哨的。五、总结本期文章就一句话,统计总数就用count(*),别花里胡哨的。
如果你对索引的知识点还不太清楚,可以直接通过传送门查看咔咔总结的索引知识点。揭开MySQL索引神秘面纱索引是为加速查询速度,创建的索引也符合所有规则,但MySQL就是不使用理想的索引,导致查询速度变慢并产生大量慢查询记录。今天就从这个问题来聊聊MySQL选择索引时都做一些什么事情。一、如何选择索引影响优化器的几大因素一条查询SQL执行需要经过连接器、分析器、优化器、执行器,而选择索引的重任就交给了优化器。优化器在多个索引中选择目的是为了找出执行代价最低的方案。影响优化器选择无非就这几个因素,扫描行数、是否使用了临时表、是否使用文件排序。临时表、文件排序这个两个点会在后期文章给大家慢慢引出,今天只聊扫描行数。扫描行数越少则访问磁盘数据的次数就越少,消耗的CPU资源越少。那么这个扫描行数是从哪里取的呢?扫描行数从何而来?创建索引一直提倡大家给区分度高的列建立索引,在一个索引上不同值的个数称之为基数(cardinality)。使用show index from table_name可以查看每个索引的基数是多少。索引基数怎么计算MySQL使用采样统计的方法,会选出N个数据页,每个数据页大小16kb,接着统计选出来的数据页上的不同值就会得到一个平均值,用平均值在乘以索引的页面数得到的结果就是这个索引的基数。表数据是持续增加或删减的,统计的这个数据也不是时时变化的,当变更的数据超过1/M时会自动触发重新计算。这个M是根据参数innodb_stats_persistent的值选则的,设置为on值为10,设置为off值为16。索引基数通过这种方式计算不是精准的但也差不了多少为什么优化器选择了扫描行数多的索引?第一种情况表增删十分频繁,导致扫描行数不准确第二种情况假设你主键索引扫描行数是10W行,而普通索引需要扫描5W行,这种情况就会遇到优化器选择了扫描行数多的。在索引那一期文章中知道主键索引是不需要回表的,找到值直接就返回对应的数据了。而普通索引是需要先拿到主键值,再根据主键值获取对应的数据,这个过程优化器选择索引时需要计算的一个成本。如何解决这种情况扫描行数不准确时可以执行analyze table table_name命令,重新统计索引信息,达到预期优化器选择的索引。二、索引选择异常如何处理方案一在MySQL中提供了force index来强制优化器使用这个索引。使用方法:select * from table_name force index (idx_a) where a = 100;但别误解force index的使用方法,之前在代码中看到这样一个案例,给查询列使用了函数操作导致使用不上索引,然后这哥们就直接使用force index,肯定不行的哈!当优化器没有正确选择索引时是可以使用这种方案来解决。缺点使用force index的缺点相信大家也知道就是太死板,一旦索引名字改动就会失效。方案二删掉误选的索引,简单粗暴,很多索引建立其实也是给优化器的一个误导,直接删掉即可。方案三修改SQL语句,主动引导MySQL使用期望的索引,一般情况这种做法使用的很少除非你对系统十分熟悉,否则尽量少操作。三、总结优化器选择索引首先会根据扫描行数再由执行成本决定。当索引统计信息不准确时,使用analyze table 解决。优化器选择了错误的索引,只用force index来快速矫正,再通过优化SQL语句来引导优化器选择正确的索引,最暴力的手法是直接删除误选的索引。
前言最近数据库老是出现下面死锁情况,借着这俩种情况出发详细的理解一下MySQL中的锁。Lock wait timeout exceeded; try restarting transactionDeadlock found when trying to get lock; try restarting transaction一、MySQL中有那些锁全局锁根据全局两个字,就可以肯定的是给一个整体加上锁。全局锁就是对整个数据库实例加锁。对于flush tables with read lock,执行完成后整库就处于只读状态,所有语句将被堵塞,包括增删改查、创建表、修改表结构等语句。表锁表锁大家都非常熟悉了,执行命令lock tables kaka read ,kaka2 write直到unlock tables之前,其它线程是无法对kaka写kaka2读的。执行命令的这个线程也只可以对kaka读,kaka2写。行锁行锁是在引擎层由各个引擎自己实现的。在MySQL中Innodb存储引擎支持行锁,若不支持行锁意味着并发控制只能使用表锁,对于这种引擎的表,同一张表上任何时刻只能有一个更新在执行,这就会影响到业务并发度。(由于篇幅的原因,下期细谈)二、全局锁演示执行flush tables with read lock命令后数据库处于什么状态。终端1执行全局锁命令端口2执行删除操作,它不会直接执行成功,而是在端口1解锁后返回。这个SQL需要3分钟的执行时间,这3分钟就是咔咔打开终端2并连接数据库的时间。现在见证了开篇所说的全局锁直接让整个库处于只读状态,这里只演示了删除操作其它的几个操作自己尝试一下。在蒋老师的文章中看到全局锁最典型的场景是用于逻辑备份,即是将整个库的每一个表都select存储成文本。现在,你想想这种场景是在什么需要下出现的。假如只有一个主库,执行了全局锁整库处于只读状态,那么业务基本停摆,产品无法使用。此时你会有疑问我在从库上备份啊!备份期间,不能执行主库同步过来的binlog的,数据量如果非常大,将引发主从延迟过大,必须进行全量备份。以上是全局锁引发的负面情况,但再看备份不加全局锁会出现什么问题。相信大多数小伙伴都开发过支付类项目,接下来就用支付案例让大家很清晰的理解备份不加全局锁引发的问题。发起一个逻辑备份。如果一个用户在备份期间购买了你公司的服务,在业务逻辑先扣除用户余额,然后给用户添加你公司对应的产品。显然,这个逻辑没有问题的,但在特殊案例下执行备份操作就会引发问题。若在时间顺序上先备份用户余额,然后用户发起购买,接着备份用户购买的产品表。一个非常清晰的问题出现了,用户余额没减成功但用户却获得了对应的产品。从用户的角度出发那是赚大发了,但这种执行顺序如果反过来的话就会产生不一样的结果。先备份用户产品表,然后备份用户余额表,就会出现用户钱花了东西没得着,这还得了,用户都是衣食父母这不是再割父母的韭菜。也就是说,在备份不加锁的话,不同表之间的执行备份的顺序不同,如果某个表在备份的过程中进行了更新并且成功备份而关联的表已经备份完成无法再进行跟新,此时就会出现数据不一致。在MVCC那篇文章中提到了一个非常重要的概念一致性视图(read view),一致性视图是根据快照读那一刻所有未提交事务的集合,前提是隔离级别为可重复。这时你应该知道要说什么了,没错就是官方大大给提供的逻辑备份工具mysqldump。mysqldump的备份原理是通过协议连接到 MySQL 数据库,将需要备份的数据查询出来,将查询出的数据转换成对应的insert 语句,当我们需要还原这些数据时,只要执行这些 insert 语句,即可将对应的数据还原。例如备份test库的命令为mysqldump -uroot -p test > /backup/mysqldump/test.db。当mysqldump使用参数–single-transaction时,备份数据之前会启动一个事物,拿到一致性视图(read view),所以在整个备份的过程中是支持更新的。既然有了官方大大提供的mysqldump工具为何还要使用flush tables with read lock来将整表锁住呢?别忘记了刚提到的可以在备份过程中进行更新,可以更新的前提是可以得到一致性视图,获取一致性视图的前提是开启事务。这里你应该清楚,不是所有存储引擎都支持事物。如果有的表使用了别的存储引擎不支持事物,那么就只能使用flush tables with read lock方法,说到这里希望大家尽量在创建表时都选择Innodb存储引擎。看着好一会了,还能记得咱们要干什么吗?需求是全库处于只读状态。如果你搭建过MySQL的主从架构,就会知道主库用来写数据,从库用来读数据并且从库不支持写入操作,可以实现这样的效果都是来自于参数readonly。同样执行set global readonly=true也可以达到整库只读状态,那么为什么从一开始没有给大家说这个方案,那是有原因的。一是,刚刚提到的搭建主从架构需要使用readonly来判断主库与从库。二是,在异常处理的方式不同。如果使用flush talbes with read lock命令客户端异常后MySQL会自动释放全局锁,让整个库回到正常状态。而整库设置为readonly后,一旦发生异常就会一直处于只读状态,导致整库长时间处于不可写状态。所以说数据库一旦加上全局锁后数据的增删改、修改表结构、修改字段等操作都会被锁住。三、表锁表锁跟全局锁释放的命令一致unlock tables,同样客户端断开的时候也会自动释放。在老一辈的革命前辈处理并发都是用的表锁,应该都知道锁表的影响虽不及锁库影响大,但在今天锁的粒度已经支持到行锁了(前提是使用Innodb存储引擎,就没必要再使用行锁来处理并发了。再来看表锁中的另一位哥们“元数据锁”(metalock)简称“MDL”,这个锁估计很少人知道,因为在实际开发过程中是不会有实际的语法来开启或关闭。这个特性是在MySQL5.5版本后引入的,就是为了解决A线程正在查询一个表的数据,在这期间B线程修改了表的数据结构,那么就会造成查询的结果跟表结构对不上,这肯定是不行的。当你访问一个表时会默认加上MDL写锁,不管在任何时候记住读锁与读锁之间不互斥,读锁与写锁,写锁与写锁之间互斥,知道行锁的共享锁、排它锁也是这么个理。那么MDL 不需要显示调用,那它是在什么时候释放的?回答是:“MDL 是在事务提交后才会释放,这意味着事务执行期间,MDL 是一直持有的。”那么看一个场景。首先,线程A开启事务并执行查询语句时,对表加上了MDL锁。然后,线程B执行的是查询,并不会堵塞住,因为读与读并不冲突。接着,线程C修改表结构,此时的线程A还未提交事务,MDL还未释放,这时的线程因无法获取到MDl写锁,就会被阻塞。最后线程D执行查询会发生什么呢?答案是堵塞。到这里按照正常的逻辑,线程C没有获取到MDL的写锁,线程D是可以申请到MDL读锁的,那为什么还会堵塞呢!这是因为申请MDL锁的操作会形成一个队列,队列中写锁获取优先级高于读锁,一旦出现MDL写锁等待,会阻塞后续该表的所有CURL操作。到这里你有没有后背发凉,一旦你在一个未提交事务之后执行了DDL操作,那么等到的结果就是MySQL挂掉,客户端会有重试机制,DDL后所有CURD会在超时后重新发起请求,这个库的线程会很快爆满。既然这样如何给表安全的执行DDL操作呢?首先,必须解决到长事务,事务不提交MDL锁就无法释放。然后,在MySQL系统表里找到infomation_schema库中的innodb_trx,可以查看当前正在执行中的事务ID,这个表在事务那期文章中也没少提。接着,你是不是想kill掉这些长事务然后执行DDL不就得了。试想一下,当你kill掉的下一刻一个新的事务又进来了,同时你又执行了DDL操作,后果是什么应该清楚了哈!这种操作肯定是不行的。官方大大怎么会允许这种情况发生呢!于是当你执行DDL操作时alter table kaka wait 30 add name可以加一个等待时间,如果在这个等待时间拿到MDL写锁最好,拿不到也不能堵塞后边的业务逻辑,先放弃。再重试执行这个命令。四、总结
前言项目准备上ElasticSearch,为了后期开发不卡壳只能笨鸟先飞,在整个安装过程中遇到以下三个问题。Docker安装非常慢ElasticSearch-Head连接出现跨域ElasticSearch-Head操作报出406错误码一、安装Docker目前咔咔对Docker的理解还只是个皮毛,对于不了解的东西就要多多使用,使用的多了自然而然也就会了。安装依赖包,执行命令yum install -y yum-utils device-mapper-persistent-data lvm2此时若直接执行安装docker命令就会发现是十分慢的,这个等待过程是漫长的。配置国内的源就可以解决问题yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo这里使用的是阿里云的源。接着再执行命令yum install docker-ce docker-ce-cli containerd.io安装docker即可。配置docker开机自启systemctl enable docker执行systemctl start docker命令启动docke查看docker版本看是否安装成功如若之前安装docker有问题,那么执行yum remove docker-ce来删除并且将/var/lib/docker下的所有东西全部删除干净。docker安装完成后咔咔就进行了创建容器,但遇到了一个问题WARNING: IPv4 forwarding is disabled. Networking will not work.拿着这个错误直接找度娘才知道,没有开启转发,网络桥接配置后,是需要开启转发的。若不开启转发就会出现上边的错误,显示没有网络。解决方案修改配置文件/etc/sysctl.conf,在里边加上net.ipv4.ip_forward=1,然后重启服务systemctl restart network,让配置生效。二、安装ElasticSearch使用docker直接获取es镜像,执行命令docker pull elasticsearch:7.7.0执行完成后,执行docker images即可看到上一步拉取的镜像。有了镜像,就可以开始创建容器了,接下来创建一个es的容器。执行docker run --name elasticsearch -d -e ES_JAVA_OPTS="-Xms512m -Xmx512m" -e "discovery.type=single-node" -p 9200:9200 -p 9300:9300 elasticsearch:7.7.0--name 表示容器名称 -d: 后台运行容器,并返回容器ID; -e: 指定容器内的环境变量 -p: 指定端口映射,格式为:主机(宿主)端口:容器端口命令执行完成会返回容器id,此时再执行docker ps -a列出所有的容器。es默认端口为9200,只用ip地址+端口号直接访问,就会返回如下图。出现这个界面就表示你安装成功了。到这里我们就非常快速的使用Docker安装完成了ElasticSearch,接下来再一起安装针对于ElasticSearch的客户端工具。三、安装ElasticSearch-Head同样也采用Docker进行快速安装,跟上边一样同样先拉取镜像,执行命令docker pull mobz/elasticsearch-head:5接着创建容器,执行docker create --name elasticsearch-head -p 9100:9100 mobz/elasticsearch-head:5为了保证图片的清晰度,图片就没有截取完全,同时也是咔咔接下来将要跟你讲的,注意俩次创建容器时的不同。安装ElasticSearch时是容器创建成功后直接在后台就运行了,但在安装ElasticSearch-Head时并没有保持一致。而是指定容器名,端口号就直接执行了,这样执行完成后是创建了一个容器,并没有运行。也就是咔咔在上图右下角的地方有一个框,这里就可以看到状态,会发现是create。所以还需要一个操作,那就是启动容器docker start 容器id。安装完成直接使用域名加端口9100即可访问。处理跨域在连接ElasticSearch会发现无法连接,由于时前后端分离开发,所以会存在跨域问题,需要在服务端做跨域处理。执行命令docker exec -it elasticsearch /bin/bash 进入到第一步创建的ElasticSearch容器中,修改配置文件vi config/elasticsearch.yml即可。http.cors.enabled: true http.cors.allow-origin: "*"将上边俩行写进配置文件中,注意这里是用yml的配置文件,简单普及一下此类配置文件的几点语法。冒号后边必须有一个空格使用空格的缩进标识层级关系,空格数据不重要,只要是左边对其的一列键即可。对大小写十分敏感缩进时不允许使用tab,只允许使用空格。配置修改完后需执行命令exit退出容器,接着执行docker restart 容器ID重启容器即可。处理报406错误此时通过ElasticSearch-Head可以成功连接ElasticSearch了,但进行数据操作时会报406错误。只需要修改ElasticSearch-Head容器中的配置即可,将配置文件复制到宿主机进行修改。执行docker cp 容器ID:/usr/src/app/_site/vendor.js /usr/local/ ,此命令会把docker容器中的文件复制到你的宿主机目录。进入到/usr/local即可看到从容器中复制出来的文件vendor.js。修改文件第6886、7574行,将"application/x-www-from-urlencodes"修改为"application/json;charset=UTF-8"即可修改后再将文件复制到容器中,从容器复制文件到宿主机命令已经使用过了,那么现在只不过是把俩个目录反过来即可执行docker cp /usr/local/vendor.js 容器ID:/usr/src/app/_site最后一步重启ElasticSearch-Head容器就结束了。四、安装IK分词器首先问一个问题,ElasticSearch中自带的有分词器为什么还要使用IK分词器?在ElasticSearch中的分词器会把中文分为一个一个的字,例如"今天是周五",会被分成“今”、“天”、“是”,“周”、“五”,这里很明显是不合适的,在大多数场景下需要的是词而不是字。所以就需要安装中文分词器IK来解决这个问题。IK提供了两个分词算法:ik_smart和ik_max_word,其中ik_smart为最少切分,ik_max_word为最细力度。分别都有什么区别会在下期文章中给大家提出来。这里需要注意安装的版本需要跟ElasticSearch版本一致。进入到ElasticSearch容器中docker exec -it 容器ID /bin/bash使用wget来进行安装,执行wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.7.0/elasticsearch-analysis-ik-7.7.0.zip即可。当你使用wget安装出现Unable to establish SSL connection时执行以下俩个命令即可。yum install openssllsyum install openssl-devel执行cd /usr/share/elasticsearch/plugins来到插件目录创建一个IK目录。将压缩包移动到IK目录中,执行解压指令elasticsearch-analysis-ik-7.7.0.zip接着删除压缩包即可,此时你可以看到一个config包和几个jar包最后退出容器,重启重启容器即可。六、总结本期文章将需要使用ElasticSearch所有的东西都已经准备齐全了,接下来的文章会带着你使用PHP的Laravel开始封装ElasticSearch的所有查询方法。后期也会在Go中封装一份,给自己的工具类添加一点内容。
一、从宏观的角度分析MySQL首先看一张经典图片这幅图估计很多人都看到过,也是经典之作高性能MySQL里边的,如果有兴趣可以先看一下电子版的(如需要电子版可联系咔咔),感觉自己能看进去了,再去买书也来的急。闲话少说,进入正题。上图的客户端可以直接理解为PHP、Java等。接下来,你会看到连接、线程处理。这一部分并不是MySQL所特有的,而且大多数客户端、服务器都具有类似的结构。因此,一般而言,MySQL可以分为两层:Server层和存储引擎层。Server层主要包括连接层、查询缓存、分析器、优化器、执行器等重要模块组成,这一层还包含了MySQL核心Api部分,比如常用的格式化时间、加密等。存储引擎大家都很熟悉,因为在面试中不止一次的问过大家Innodb、Myisam存储引擎的不同。所以想过没有,MySQL为什么会有这么多的存储引擎呢?一切技术起源于当下问题,同样在MySQL中也不例外。MySQL在存储引擎这一方面的架构是插件式的,即可以随意切换不固定,而且MySQL5.5版本存储引擎已经默认为Innodb。二、一条SQL执行要经过多少困难?下图是咔咔之前培训时给发的资料图中还有一个熟悉的陌生人查询缓存模块,该模块在MySQL8.0中已不存在。关于该模块为何要被删除,后续的文章也将于大家交流。首先,我们将大致了解当我们执行一条SQL语句时,如何在这个架构图中运行。2-1 连接器mysql -u root -p连接数据库命令,在执行之后,你将需要输入密码。当完成经典的TCP握手之后,连接器就开始发挥作用了。如果码错误时,则返回Access denied for user ‘root‘@‘localhost‘ (using password: YES,错误编码1045。如果连接信息均正确,则此时将根据你输入的用户访问权限表来获取该用户的权限,此处必须清楚,当你登录成功后,即使其他人修改了你的权限,在这个连接未断开之前你的权限是不会发生改变的。当你连接完成之后,如果你一直不做任何事情,执行show processlist将会看到一个sleep,表示空连接。那么你知道在MySQL中,如果连接成功后没有进行任何操作,多久会被自动中断?可以执行show variables like 'wait_timeout';用于查看时间。在MySQL中如果没有特别说明,那么所有的时间都是以秒为单位的,根据时间转换可以得知空连接持续8小时。2-2 查询缓存你需要注意的是,MySQL8.0已经被取消了,这个问题不止说了一次了,特别是那些正在使用MySQL8.0以下版本的小伙伴要注意哈!当你切换到8.0时候,遇到这个问题不知道怎么解决。MySQL8.0为何取消查询缓存模块这个模块的设计,把查询语句作为key ,将结果作为value 进行缓存,一旦这个表有更新,之前所有的缓存都会被清除掉。这就像你辛辛苦苦写的代码提交之后被别人覆盖一样难受。MySQL8.0以下的版本提供了一个参数query_cache_type = enmand来控制是否要使用查询缓存,在设置完成后,默认的select语句将不会被缓存。如果确实可以使用部分场景,那么你可以将sql_cache添加到select关键字之后。如果一条select语句之前被缓存过,那么结果集在这里就会直接返回,而没有缓存过的select语句就比较辛苦了,还要继续自己的漫漫长路。2-3 分析器MySQL8.0之前,它会在进入分析器之前判断是否缓存,在MySQL8.0之后,连接器验证成功后就直接进入分析器。分析器,根据字面意思来理解就是分析要执行的SQL语句是什么,要做什么。比如执行select * from user where id = 1MySQL首先根据select判断这是一个查询语句,然后将user识别为表名,id识别为字段名,这个过程被称为词法分析。下一步,需要知道该SQL的语法是否正确,进行语法分析,如果语法不对你就会看到You have an error in your SQL syntax错误。通常,将在use near中找到该错误。2-4 优化器到了这一步,MySQL知道你要做什么,但是要选择最佳执行方案。优化器都优化些什么?举例来说:多个索引时选择那个索引、多表关联时连接顺序。现在你是否想知道,优化器将优化多表关联的连接顺序,那在写SQL语句时是否就不必考虑连接顺序呢?当然不是,能让MySQL少做事情就少做,还是一个准则用小表驱动大表。2-5 执行器通过要做什么、怎么做后这条SQL语句才会真正的被执行,先进行权限验证,若没有权限则直接返回权限错误,否则根据表定义的存储引擎,去使用对应引擎提供的接口。三、总结上图包含了正文的所有知识点,也是整个MySQL的大体执行流程图,后期文章都将围绕这几个要点展开。
让我们通过例子来学习Phalcon本系列目录前言一、项目架构二、入口文件三、配置Nginx四、控制器跳转五、数据库之增、删、改、查插入数据修改数据删除数据六、代码优化总结本系列目录一、Phalcon在Windows上安装 《Phalcon入坑指南系列 一》二、Phalcon入坑必须知道的功能《Phalcon入坑指南系列 二》前言上一期文章是给大家介绍了Phalcon的安装,并且使用Phalcon开发工具进行创建了项目、控制器、模型。也就做了几个简单的操作。本期给大家继续聊聊Phalcon的实战使用。一、项目架构从上图可以看出这个目录结构跟TP框架极其相似,对应的目录就不一一解释了,这其中的migrations这个目录跟大家说一下。这个目录就跟laravel中的数据库迁移一样,具体怎么使用就不细讲了哈!框架结构也不是固定的,跟ThinkPHP也一样可以注册命名空间来修改目录结构。在Phalcon框架中,咔咔最近负责的项目也是使用的多模块进行开发的。但是目录结构也跟使用Phalcon开发工具生成的目录是不一样的。万变不离其宗,都是一个模样。二、入口文件每个框架必不可少的文件,index.php这个貌似是所有开发者默认的。那么在Phalcon这个框架中也是必不可少的了。关于这里边加载的是什么就不去细细的剖析源码了,没那个必要,想看源码解析的可以去搜索ThinkPHP框架源码解析。大体执行就是先进行依赖注入,使用/config/services.php引入一些文件其中你要着重知道的是在这里进行了数据库的连接。这个文件/config/router.php看名字就知道是什么,路由呗!怎么设置路由,后边再说。在就通过第一步依赖注入后获取配置信息。最后一行代码就是include APP_PATH . '/config/loader.php';注册从配置信息中获取的目录。三、配置Nginx在第一期文章中没有对项目进行配置,接下来进行简单的配置一下。Phalcon提供了三种方式的配置,咱们就先用最简单的第一种即可。server { listen 80; server_name www.kakaweb.com; root "D:/phpstudy_pro/WWW/phalcon/public"; index index.php index.html error/index.html; location / { try_files $uri $uri/ /index.php?_url=$uri&$args; } location ~ \.php(.*)$ { fastcgi_pass 127.0.0.1:9002; fastcgi_index index.php; fastcgi_split_path_info ^((?U).+\.php)(/?.+)$; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info; include fastcgi_params; } location ~ /\.ht { deny all; } }以上是咔咔的配置,如果你也使用的是PhpStudy,那么就可以直接复制过去使用。四、控制器跳转在第一期文章中,使用phalcon开发工具创建了控制器、模型,如果你还没有创建出来项目就需要去看第一期文章哈!先看一下访问如何。代码实现可以看到在index控制器中,还建立了另一个方法kaka。主流的框架都配置的是index控制器为默认访问路径,这个kaka怎么访问跟其它框架也是一样的。访问链接就是http://www.kakaweb.com/index/kaka即可。也就是域名+控制器+方法名,这里的方法名需要注意的是不需要带着Action。演练一下官方给的案例。可以看到输出结果就是一个a链接这个链接会直接跳转到Signup控制器,接下来使用开发者工具来生成一下这个控制器。接着点击刚刚那个按钮,就会跳转到Signup控制器了。关于控制器就先说到这里。
前言这个框架之前就记得在哪看到过,最近入职的公司在用这个框架,就来看看咔咔的入坑指南吧!首先不管框架效率怎么样这个安装过程真是一步一个坑啊!官方说是PHP框架里边速度最快的,比ThinkPHP快15倍,那挺好!但我还是要吐槽一下这个框架,安装工具包的时候我第一次安装的是phalcon3.4.3,然后提示说是需要升级到4.0.0版本。这还没什么,那就升级呗!下载了4.0.0后,说需要升级到4.0.3。那没事在继续下载,这次直接就下载了4.0.6。在听官方的,我估计一晚上都安装不成功。一、安装安装环境:PHP7.2phalcon_x64_vc15_php7.2_4.0.6+5190_nts.zipphpstudy下载地址:https://github.com/phalcon/cphalcon/releases/tag/v4.0.6将下载好的dll文件放到PHP的扩展目录然后在php.ini文件中添加对应的dll信息。重启服务器,在PHP文件里边输出版本信息psr下载地址:https://pecl.php.net/package/psr/0.7.0/windows如果你发现怎么安装都不行的话,在考虑安装吧!我是被整这个框架整的没有一点脾气。我的是出现了找不到ddl文件。如果你也下载了,也是需要把扩展文件放到对应PHP版本的目录下的。二、Phalcon 开发工具安装使用git进行克隆,地址为git clone https://github.com/phalcon/phalcon-devtools.git然后将文件就放到桌面,打开目录找到下图位置鼠标右击修改路径地址接着在命令行里边执行phalcon看到下图这些信息就对了,这里还需要注意一下,会提示对应的信息,按照操作即可。最终结果就是这个样子的,说实话,把这个环境搭起来都很废力气。怪不得国内很少有人用。三、环境变量配置这一个小节本应该不会出现在这里的,但是为了新手还是写出来,希望一篇文章可以直接解决你们在安装这块的所有问题。配置环境变量这个操作是有部分人没有配置过的,因为大多数在本地都使用的是PhpStudy来作为开发环境。右击我的电脑点击高级系统配置点击环境变量然后按照打开的顺序,一个个的点击确定即可。四、Phalcon 开发工具的使用这里需要注意的是需要在path里边配置phalcon-devtools的安装目录。如果记不住命令,可以直接执行`phalcon commands即可生成项目框架这里咔咔使用的是Phpstudy集成环境,直接在www目录下执行创建项目框架命令。执行命令phalcon create-project phalcon返回到www目录下就可以看到生成的这个项目。然后在Phpstudy配置一个虚拟域名即可,配置到public下。先看数据库信息的配置吧!生成控制器 / 模型命令分别为phalcon create-controller --name Userphalcon model test对应的就会生成文件。夸一下phalcon在安装的时候内心对phalcon说了一万句我爱你(草泥马)。但在这里的时候对于phalcon命令不覆盖的功能还是竖个大拇指。上图就是修改后,在执行创建模型文件依然存在的。这个可比fastadmin想的周全。五、总结这一期先到这里,也是不容易啊!框架都一样,不要放太多精力在这个上边,大概了解即可。实现原理都是一个样子。本文需要注意以下几点注意你自己的PHP环境注意下载phalcon的版本跟自己的PHP环境是否兼容注意环境变量的配置下一期将写phalcon的实际应用
七、为什么添加和修改都不管用了?这里的问题首先去看看数据库的表结构,看你的表结构是否有问题。表结构不要设置为field_id结尾,这样的字段是不可以的。直接暴力就是fieldId即可。关于这块后期看一下fastadmin源码,看是不是后缀带id的都会做什么操作。经过看源码得出的结论。fastadmin在一键curd时会将后缀为_id的字段识别为表外键,在新增记录时无论填入任何值都和报错“xxx_id不能为空”,这是因为fastadmin将其识别为了外键在add.html语句中这个字段的input是这样的:<input id="c-xxx_id" data-rule="required" data-source="xxx/index" class="form-control selectpage" data-field="name" name="row[xxx_id]" type="text" value="">八、列表字段显示的为英文这个问题就很简单了,咔咔没有看源码理解就是语言包没有你设置的这个字段名。直接打开到对应的js文件,然后修改初始化表格中的title值即可。修改完之后就可以看到中文名了。九、如何在一个模板上设置按钮,然后显示设置的表格先看实现效果,点击回收站之后弹出一个表格。那么如何给自己需要的模板设置同样的效果呢?接下来就使用官方给的案例进行解读。首先来到test的index模板中可以看到回收站的这个按钮。这里需要注意这个herf的值,如果是自己自定义的按钮直接把title替换成自己设置的名字即可。接着来到test的js模板,在这个js中你能看到出了index初始化表格外,还存在一个recyclebin这个方法。如果你不想写直接把这个方法拿过去即可,但是一定要修改url地址哈!但是此时做的这些修改远远还是不够的,还需要控制器,那么test的recyclebin的这个方法是在application/admin/library/traits/Backend.php这个文件里边。如果你想设置你自己的方法,那么你需要在控制器新增加一个方法,然后把这个文件中的index方法代码复制过来即可。既然有了控制器,有了js创建了表格,那么还缺什么呢!还缺一个模板喽!创建一个跟你的方法名一样的html文件,然后随便复制一个index.html模板进去即可。在复制的时候只留下这些代码,其余的就可以删除了,咔咔测试如果加上上边的代码会出现无法访问的情况。html文件名的设置就跟下图一样做完这些工作那就可以看到你想要的东西了。实现步骤在控制器Test中写需要显示列表的方法,例如display方法创建了控制器就需要创建跟display方法对应的display.html模板文件html模板文件的创建位置是在Test目录下找到Test的js文件,然后添加display方法,这个方法不会写的话就直接使用案例中test.js中的recyclebin然后就大功告成了如果你还不会那么就只能私信咔咔了,哈哈这里有个注意点就是如果你是用的test.js方法中的recyclebin,那么会存在一个问题就是操作的按钮问题。这个按钮就是在recyclebin的初始化表格中设置的,自行设置即可。也就是换换classname和名字即可。修改完后的图,只要细心这个框架使用起来还是非常顺手的。十、关于数据库字段设置为state查询条件没用如果你发现你对state这个字段设置where条件后没有效果时,试着查询一条数据,然后看一下这个state类型string(1) "1"如果类型是上边打印的结果,那么你就要注意了,在查询条件中需要写上"1" ,也就是字符串1,而非整型1。看到这里是不是恍然大悟,赶紧去改你的查询条件吧!十一、设置的enum类型页面怎么是下拉框,不是单选框如果你使用的enum类型,则会出现上的两种情况。一种是下拉框,一种是单元框。那么是什么原因造成的不一致呢?请看上图,如果你想生成单元的形式,那就使用enum类型,字段名结尾使用data即可。十二、发起ajax成功后如何实现刷新数据这个过程你可以理解为用户上传了一个图片。然后后台需要审核。后台点击审核后,这条数据就需要移除,如果你发现数据未移除。需要在对应的js文件上加上refresh:true即可。在来说一下如何发起ajax请求,也是很简单将button的属性设置为btn-ajax即可。本节关于fastadmin的实战问题到这里就结束了,如果你有其它的相关问题,可以私聊咔咔,也可以在评论区,咔咔看到会第一时间进行回复的。
五、请求接口返回403将以下代码添加到api基类即可header("Access-Control-Allow-Methods: *"); header('Access-Control-Allow-Origin: 域名'); header("access-control-allow-credentials: true"); // 响应头设置 // header('Access-Control-Allow-Headers:x-requested-with,Content-Type,X-CSRF-Token'); if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS'){ // 浏览器页面ajax跨域请求会请求2次, // 第一次会发送OPTIONS预请求,不进行处理,直接exit返回, // 但因为下次发送真正的请求头部有带token, // 所以这里设置允许下次请求头带token否者下次请求无法成功 header('Access-Control-Allow-Headers:x-requested-with,content-type,token,userid,laravelsession'); exit("ok"); } 然后将跨域检测注释掉六、解决列表页显示的是分类ID而不是分类名称其实这个问题很好解决,不要想的太过复杂。你可以试想一下在thinkphp框架是怎么解决这个问题的。在thinkphp中解决这个问题就是使用获取器来处理的。只需要在对应的模型中设置获取器即可。页面显示但是真的是这样处理的吗?不要一看数据出来了,就万事大吉了。你可以思考一下,在index页面没有显示出分类的名字,但是在修改时却出现了名字。那么是不是可以得出的结论为首页没有使用获取器,而add、edit页面使用了获取器。若按照上边的方法,直接暴力的在模型中添加获取器,会造成一个非常严重的问题,那就是修改会无法显示数据。不信你可以试一下。就跟下图一样,这不是闹玩呢木。所以说这种方案肯定不行。试想一下,在thinkphp框架中,除了获取器可以表字段处理外,同样使用模型关联也可以。当然用最原始的json方法也是可以的。在fastadmin中,控制器的方法都继承于application/admin/library/traits/Backend.php这个文件中。在这个文件中可以看到index方法那只能做的一件事情就是在需要显示分类名称的控制器中重写这个方法index呗!在重写之前需要做的一件事情就是需要在对应的模型中写上模型关联然后在你需要的控制器上重写index代码源码 /** * 查看 */ public function index() { //设置过滤方法 $this->request->filter(['strip_tags', 'trim']); if ($this->request->isAjax()) { //如果发送的来源是Selectpage,则转发到Selectpage if ($this->request->request('keyField')) { return $this->selectpage(); } list($where, $sort, $order, $offset, $limit) = $this->buildparams(); $list = $this->model ->with('category') ->where($where) ->order($sort, $order) ->paginate($limit); $result = array("total" => $list->total(), "rows" => $list->items()); return json($result); } return $this->view->fetch(); } 到这里可并没有万事大吉啊!还需要修改模板,在对应的js文件中添加即可{field: 'category.name', title: __('分类名称'), formatter:Table.api.formatter.文档中说是还需要设置属性设置属性protected $relationSearch = true;,反正咔咔测试设置不设置都一样,你们看着来,目前还没发现这个参数到底是控制什么的。官方文档说是控制开启关联查询,但是经测试,好像没什么大碍。来看一下最终结果吧!如果只是一对一那解决方案会有很多种。你可以使用json来直接查询,同样你也可以循环查询出来的数据,然后根据每个分类ID进行查询出对应的分类名即可。
目录结构前言一、安装二、配置成可以自己使用的后台模板三、添加自己的后台模块四、关于数据库设计五、请求接口返回403六、解决列表页显示的是分类ID而不是分类名称七、为什么添加和修改都不管用了?八、列表字段显示的为英文九、如何在一个模板上设置按钮,然后显示设置的表格十、关于数据库字段设置为state查询条件没用十一、设置的enum类型页面怎么是下拉框,不是单选框十二、发起ajax成功后如何实现刷新数据前言本文会对fastadmin进行简单安装和配置,并且对fastadmin在使用过程中遇到的问题全程记录。如你也有相关的问题,那么就评论区见一、安装下载地址:https://www.fastadmin.net/download.html?ref=docs咔咔这里使用的使用源码安装方式,比较方便。点击上边的下载地址,然后下载源码包,进行解压。然后将解压的文件放置到PHP环境目录中。配置phpstudy虚拟域名。这里一定要注意你的PHP环境。PHP >= 7.1 且 < 7.3 (推荐PHP7.1版本)Mysql >= 5.5.0 (需支持innodb引擎)Apache 或 NginxPDO PHP ExtensionMBstring PHP ExtensionCURL PHP ExtensionNode.js (可选,用于安装Bower和LESS,同时打包压缩也需要使用到)Composer (可选,用于管理第三方扩展包)Bower (可选,用于管理前端资源)Less (可选,用于编辑less文件,如果你需要增改css样式,最好安装上)这里咔咔配置的地址是www.fast.com。然后直接访问www.fast.com/install.php即可接下来就是填写一些数据库信息即可。安装成功的后台样子。这里你可以看到上边有一个小提示,这个提示在安装成功后的后台访问目录是一串自动生成的目录。就是因为咔咔将目录地址改为了admin然后给的提示。这里将admin改为fastadmin,在来看一下。这样就可以正常的使用了。二、配置成可以自己使用的后台模板可以根据上边的这个图来看,菜单中有很多都是不需要的,接下来就进行删除。举个例子删除插件管理的这一个菜单。删除代码里边所有关于插件的代码,这里的地址可以在浏览器中看到然后在后台的菜单管理中,将插件的这个选项删除掉即可。其余的菜单都是一样的,由于这个插件的是没有表的,所以就没有涉及到表结构删除。三、添加自己的后台模块创建一级文件:php think crud -t picture -u 1创建一个二级文件夹:php think crud -t video -c video/video -u 1创建驼峰的文件:php think crud -t video_subject -c video/videoSubject -u 1删除二级文件目录:php think crud -t picture -d picture/picture -u 1四、关于数据库设计使用图片上传功能image smallimage varchar 识别为图片文件,自动生成可上传图片的组件,单图images smallimages varchar 识别为图片文件,自动生成可上传图片的组件,多图createtime 创建时间 int 记录添加时间字段,不需要手动维护updatetime 更新时间 int 记录更新时间的字段,不需要手动维护text 文本型 自动生成textarea文本框weigh 权重 int 后台的排序字段,如果存在该字段将出现排序按钮,可上下拖动进行排序
前言在上文中解读了PHP8新特性,由于需要代码演示需要安装PHP8源码。为了方便没有在linux上搭建,直接使用了phpstudy继承环境来使用。在那一文中虽然代码可以正常运行,但是里边的扩展是没有打开的。接下来咔咔带你一步一步实现phpstudy集成PHP8一、安装PHP8下载地址https://windows.php.net/download#php-8.0下载完成后将文件解压,存放目录建立为php8即可都知道phpstudy的安装目录在新版本中是直接安装在D盘的,所以只要你没有修改安装目录,那么所有的安装目录就是一致的。这里确实给写文章一组提供了很大的方便,因为每个人的安装位置都是一样的就省了很多麻烦。接下来将下载并解压的PHP8放到phpstudy总PHP的目录此时你就可以尽情的玩耍了。这里一定要修改PHP版本哈!二、运行PHP8报错处理有一部分伙伴在安装PHP8时有可能会出现以下错误。咔咔测试是在win10专业版是不会出现此错误的,但是在win10的家庭版会出现个错误。如果你把第一步执行完成之后,重启了环境之后发现PHP无法运行,或者报错502。那么你直接cmd到D:\phpstudy_pro\Extensions\php\php8.0.2nts这个位置执行php -v如果发现出现以下错误PHP Warning: 'C:\Windows\SYSTEM32\VCRUNTIME140.dll' 14.0 is not compatible with this PHP build linked with 14.28 in Unknown on line 0进入到这个网址https://www.yuque.com/u30882/rx39g7/kns2a2,咔咔已经将软件下载下来了,点击进去直接下载即可。下载完成后需要重启电脑。然后就可以完美的运行PHP8三、完美结合phpstudy和PHP8通过第一步和第二步的操作,环境已经搭建好,也可以运行PHP文件。但也仅此可以运行PHP文件,此时MySQL是连接不了的。那是因为PHP8是我们自己下载的,里边所有的扩展和配置信息没有修改。所以造成的问题就是自己下载的PHP8配置文件里边所有的扩展都是关闭状态。此时就需要去一个一个开启对应的扩展,如果不知道都需要开启什么扩展。打开你的phpstudy然后点击到网站,右边有个管理,看看之前的PHP环境都开启了什么扩展,然后跟着照猫画虎即可这里是咔咔已经全部打开过的。这里的打开方式你可以在下图的位置一个一个开启,也可是直接到php.ini文件中直接修改,只需要将extension前边的注释去掉即可。例如需要打开curl,就把前边的;去掉即可。你会发现你把这些扩展都按照之前的PHP环境打开完了,发现在面板的扩展里还是没有一个打钩的。但是明明我都开启了啊!为什么还是扩展还是没有打开。不要着急,依然来到php.ini文件中,搜索这样的一个词extension_dir,然后修改扩展位置重启你的PHP环境,接下来测试一下,本文测试使用的是tp6测试代码就是查询数据库的数据,前提是你把数据库信息配置好啊!这里就不写了四、总结至此关于phpstudy集成PHP8才算是完善,才可以投入更多的测试。在这里给大家说几个注意点修改完配置文件一定要重启环境不知道开什么扩展的,打开你原本的phpstudy跟着照猫画虎扩展开启结束后一定要修改扩展目录运行php -v报错时看看是不是咔咔一样
三、介绍特性版本的更新迭代会有新功能的出现,伴随着某些旧功能也会舍弃。接下来就聊聊PHP8.0到底带来了哪些新特性呢!1. 联合类型关于联合类型的特性,其实在PHP7的时候已经实现,但是当时是以注解形式。现在看一下俩者区别,下图来源于PHP官网。PHP8在设置了参数类型后,如果传入类型与预设类型不符合会直接报错但是PHP7就不同,虽然预设了类型,但是传入不对应的类型也可执行。此功能可以用来限制参数类型,可以更好的对参数进行过滤。2. 匹配表达式此项功能类似于PHP7的switch语句。1. match匹配单值相对于switch隐藏了break<?php $name = match(2) { 1 => 'kaka', 2 => 'niuiniu', }; echo $name; // niuiniu2. 匹配多个条件<?php $method = $_SERVER['REQUEST_METHOD']; match($method) { 'post' => $this->handlePost(), 'get','put' => $this->handleGet(), };3. 默认值存在跟switch相同的属性default<?php $name = match(3) { 1 => 'kaka', 2 => 'niuniu', default => 'heihei', }; echo $name; // heihei4. 如果不设置默认值会报错如果不设置default则会报错<?php $name = match(3) { 1 => 'kaka', 2 => 'niuniu', }; echo $name; // Uncaught UnhandledMatchError: Unhandled match value of type int5. 强制类型匹配默认强制类型匹配,如下代码匹配值为int,但是搜索值为字符串3,所以会直接走default<?php $name = match(3) { 1 => 'kaka', 2 => 'niuniu', "3" => 'niuniu', default => 'zero', }; echo $name; // zero3. null安全运算符这个特性会非常高效的解决代码的冗余。在PHP7中,有时会存在类属性多条件的判断,如下代码<?php class Person{ public $user; public $country; public function __construct(){ $this->user = $this; $this->country='yes'; } public function getAddress(){ return $this; } } $session=new Person(); if($session!==null){ $user = $session->user; if($user!==null){ $address = $user->getAddress(); if($address!=null){ $country = $address->country; if($country!==null){ var_dump($country); } } } }以上代码返回结果为string(3) "yes"但是在PHP8中就完美的解决了这种代码冗余的问题代码<?php class Person{ public $user; public $country; public function __construct(){ $this->user = $this; $this->country='yes'; } public function getAddress(){ return $this; } } $session=new Person(); echo $session?->user?->getAddress()?->country;同样返回结果也是string(3) "yes"可以看到在PHP8中用一行代码即可实现PHP7的7行代码,是不是很nice。4. 构造函数属性提升PHP7构造函数代码<?php class User { public string $kaka; public function __construct(string $kaka) { $this->kaka = $kaka; } }PHP8构造函数代码<?php class User { public function __construct(public string $kaka;) { echo $this->kaka; } }5. 注解新增的这个注解特性,咔咔在写了这几个特性之后唯独感觉这个用处不是很大,估计还是很菜的原因。但是为了文章的完整性还是写出来给给你们看一下。直接上代码了,PHP7获取代码的注释就是用下文代码进行获取的。<?php class User { /** * @api http://www.kaka.com/api */ function show($name){} } $re=new ReflectionClass(new User); $doc = $re->getMethod('show')->getDocComment(); $res=substr($doc, strpos($doc, "@api") + strlen("@api "),-2); var_dump($res); // string(32) "http://www.kaka.com/api "在上边代码中使用了好几个字符串的操作,假设注释写的不规范,出错的概率不亚于你写代码少个分号。既然有这样的问题,那么官方就给咱们解决了这个问题,接下来看一下在PHP8中是怎么写的。<?php #[api("http://www.kaka.com/api")] function show($name){} $ref=new ReflectionFunction("show"); $attr=$ref->getAttributes("api")[0]; $name=$attr->getName(); $value=$attr->getArguments(); var_dump($value[0]);// string(24) "http://www.kaka1.com/api"关于类的注解这里就不说了,有兴趣的可以去官网查阅相关资料。6. 命名参数最后一点关于PHP8命名参数在PHP7使用的都是位置参数,例如如下代码也就说你传入的参数是什么在方法接收的地方就对应的是什么。<?php class User { public function paramTest ($name,$age) { var_dump($name.$age); } } $user = new User(); $user->paramTest('咔咔',24); // string(8) "咔咔24"那么在PHP8中新增了一项特性就是命名参数,如下代码跟上述代码不同的是在方法传参时给每个参数都起了个名字,但是这个名字只能是没有了$ 的参数。这个新特性在代码的维护性来看没有一点的优势,咱也不知道设计这个出来干嘛!<?php class User { public function paramTest ($name,$age) { var_dump($name.$age);// string(8) "24咔咔" } } $user = new User(); $user->paramTest(age: '咔咔',name: 24);但是在参数这块还是有值得点赞的功能,那就是可变参数,类似于Go的切片但是在这里一定要注意的一件事情就是,如果使用了可变参数,那么在传参的时候就不能使用位置参数,而需全部使用命名参数。<?php class User { public function paramTest ($name,$age,...$other) { var_dump($other); // array(2) { ["sex"]=> int(1) ["like"]=> string(6) "篮球" } } } $user = new User(); $user->paramTest(age: '咔咔',name: 24,sex:1,like:"篮球");四、总结以上就是关于PHP8新特性咔咔总结的几个常用点。PHP8也添加了新的功能和类库、错误处理、字符串处理、面向对象编程的更改等。当然在关注新特性的同时还是需要关注废弃的东西,在PHP8中废弃最多的都是关于反射的几个方法。以上就是咔咔总结的PHP8更新的主要特性,没有总结的特别全面,只是把在开发过程中能使用到的写了出来。
解读PHP8特性前言一、给小皮面板下载PHP8二、部分下载PHP8会出现502解决方案三、介绍特性1. 联合类型2. 匹配表达式3. null安全运算符4. 构造函数属性提升5. 注解6. 命名参数四、总结前言PHP8在2020年11月26日正式发布,又一个里程碑到来。根据官网的信息目前已经发布到了8.0.2版本,但目前许多的项目还是保持在5.6版本,旧的不去新的不来,该升级升级哈!接下来咔咔将对PHP8.0的新特性进行解析。一、给小皮面板下载PHP8既然要学习新特性那总得要先下载下来。咔咔本地环境一直使用的是phpstudy,目前PHP版本只给提供到了7.4,。打开PHP官网地址https://www.php.net/,选择window下载。然后点击咔咔圈住的这个下载即可下载到的源码就可以直接放到面板的D:\phpstudy_pro\Extensions\php这个目录下面然后随便给文件夹起个名字,咔咔是按照phpstudy的目录结构写的然后跳转面板的PHP环境,设置为PHP8的环境即可访问一下呗!二、部分下载PHP8会出现502解决方案如果你下载完了PHP8,也按照咔咔的流程执行了,但是访问时就是返回502。那么不用着急,这只是一个小问题,如果发现PHP无法运行成功。使用cmd来到PHP8的目录下,执行php -v这个命令,如果出现以下情况就说明咔咔说的这个可以解决,如果不是就另行百度哈!出现以下错误,原因是VCRUNTIME140.dll与PHP版本不兼容PHP Warning: 'C:\Windows\SYSTEM32\VCRUNTIME140.dll' 14.0 is not compatible with this PHP build linked with 14.28 in Unknown on line 0解决这个问题也是很简单https://www.yuque.com/u30882/rx39g7/kns2a2,咔咔已经将软件下载下来了,点击进去直接下载即可。下载完之后,就直接打开安装,然后重启电脑,万事大吉。如果不使用咔咔提供的安装包,也可以直接访问官网下载。官网进去之后,拉到最下边有个其它工具和框架,点击咔咔选择的那个下载即可。
三、Postman与Talend Api互通数据首先得先需要安装Talend Api这个扩展,由于这个扩展是谷歌商店下载的。那么肯定一部分人下载不了,懂的都懂,不懂得略过即可。咔咔给你们一个可以离线下载谷歌插件的一个地址crxdl.com至于下载的插件怎么安装就自行搜索哈!很简单的,将压缩包直接拖入扩展里边即可。下图是咔咔已经安装好的截图如果需要互通数据,那么我们只能在Talend Api这个扩展里边寻找关于Postman的信息。当在Talend Api这个扩展中点击Import时你会发现第一个就是导入Postman 集合 V2。那么肯定想都不用想,这个入口就是为了Postman的用户可以直接使用的。那么再来到Postman中鼠标右击集合就可以看到导出这个按钮。然后你就会看到一个熟悉的身影,这个集合V2那就直接点击导出吧!然后将导出的文件现在尝试着往Talend Api这里导入。此时就可以在Talend Api这个里边看到我们刚刚在Postman中创建的集合和方法了。但是你会发现此时的请求时不通的,那是因为Talend Api还是没有识别到在Postman中设置的环境变量host那么就要在Talend Api这个里边设置host的环境变量了,将刚刚在Postman中设置的值复制过来。然后发起请求即可,此时就可以看到已经返回成功了四、总结本文主要介绍了Postman与Postman之间、Postman与Talend Api之间互通数据。其它的接口测试工具咔咔没有使用过,应该大多数都支持这样的操作,在使用的过程中多点疑问,你就会发现很多新鲜东西。如果你暂时使用不到这个功能,那就先知道有这个事情的存在,后期如果需要你的脑海中最起码有这个印象,知道有这么回事。而不是拿起键盘就是干,浪费时间和精力,有可能还会出错。
文章目录前言一、从头开始二、Postman与Postman互通数据三、Postman与Talend Api互通数据四、总结前言之前的接口测试都使用的是postman,最近将调整为Talend Api Tester。于是就有一个很大的问题,所有的接口信息都在postman上,如何导入到alend Api中。跟随着这个问题我们来一起发现测试工具中的新大陆。一、从头开始既然要对测试工具有一个新的认识,那就应该从头开始。为了测试方便,咔咔就先建立一个集合为A然后在集合A里边新添加一个请求,下图就是添加成功的图这样的请求有一个很大的弊端就是当域名发生改变时,那么在这个集合下的所有接口都需要修改请求域名。针对这一需求Postman是早已经给准备好了。添加成功后就可以在这里看到刚刚添加的环境变量了然后将刚刚的请求域名改为{{host}}即可,发送请求依然可以获取到数据。做完以上几个操作后,我们一个完整的集合就创建的差不多了,接下里就需要回归正题。继续针对文章开头说的互相转换接口数据。二、Postman与Postman互通数据其实在Postman中存在着几种方式的数据互相导入。这里咔咔给大家介绍其中的俩种方式,第一种通过分享链接、第二种是导出为json文件。俩种方式都可以将接口数据导入到另一个postman中。同样也可以将数据导入到其它支持Postman Collection的接口测试工具中。首先来聊聊关于postman如何协同工作使用分享链接的的方式如何工作假设小Q已经写完了一部分接口数据,并且在postman中已经调试好了。很不巧的事情是由于工作原因需要做一些调整,那么小Q就需要把写好的接口数据给接收的人。这个时候最直接的办法就是小Q将自己的postman账号给接手的人, 但是这个也不是很现实哈!于是小Q就在想有没有一种办法来解决这个问题。其实这种情况postman早就有这个功能了,只是平时开发过程中只关注自己的那一亩三分地。只要我调试的接口可以跑的通就行,其它的功能你爱咋牛逼咋牛逼去。当你鼠标右击一个集合时你会发现在一列就是分享集合然后点击获取公共的链接当你点击了获取公共链接之后就会出现一个链接地址获取到了这个链接地址之后就可以将这个地址发给你的小伙伴,然后你的小伙伴就可以拿着这个地址直接在自己的postman中将这个集合给复制出来然后就可以将刚刚复制出来的地址放到这里就可以了这里由于是咔咔直接在自己的postman中进行的导入所以会出现集合已存在,要么选择替换,要么选择复制一份以上就是关于postman与postman之间数据的互通。
四、关于getLastSql的实现过程还是之前的案例,我们来使用这个方法打印一下结果来看一下是什么。看到上图就知道是框架最终给生成的SQL语句,那么接下来咔咔就会带大家一起来探讨一下,这个sql语句是如何生成的。下图为本次演示的案例,也就是咔咔下图圈出来的地方。从上图圈出来的地方进行代码追踪会到文件thinkphp/library/think/Db.php,并且会去执行本类的__callStatic方法,这个方法就不在进行解释了,在上文和之前也已经提到过多次了。并且返回结果也不去做声明了,上文也提到了,这里只需要知道最终返回结果为返回 object(think\db\Query)根据上图的返回结果可以知道最终回去调用object(think\db\Query)这个类的getLastSql这个方法根据这个方法可以知道是获取最近一次查询的sql语句。这里就会有点疑问了,关于属性connection到底是什么,这里在进行一次简单的简析。关于这种属性的声明一般都会在本类的构造函数或者父类的构造函数中进行声明,这也是在阅读源码时的一个小窍门。于是我们首先就需要来到本类的构造函数来看一眼。通过上图可以得知,此处使用了依赖注入的方式,所以Connection就是一个对象,并且也是框架中所谓的连接器。所以说这个Connection对象就是下图打印出来的。根据上图得知使用的类文件应该就是think\db\connector\Mysql那么就会执行这个类里边的getLastSql方法。但是来到这个类执行你会发现这个类里边根本是没有这个方法。根据上图的继承关系,我们就知道这个方法是在thinkphp/library/think/db/Connection.php这个类文件里边。下图即是这个方法执行过程,可以看到存在俩个参数,但是这俩个参数还是一头雾水根本不知道是什么。根据代码追踪我们对上图所出现的俩个参数先进行简单的说明$this->queryStr当前SQL指令$this->bind 绑定参数追踪$this->queryStr这个属性值走到这里估计有点蒙了吧!对于这个值有点确定不了了,指定不是靠打印可以获取到结果的。当然还有另一种办法就是进行debug来断点调试。但是既然咔咔带大家看源码呢!就不会用上边的俩种方式,会直接从源码中找到蛛丝马迹。根据咔咔上边给提供的案例,执行的最后一步就是find方法,这个方法也是在thinkphp/library/think/db/Connection.php这个类里边,寻找单条记录。那么我们就在这个方法中进行一点点的寻找,这里咔咔已经给大家圈好了,就是下图咔咔圈其起来的地方。根据上图咔咔给的代码注释,第一个参数就是生成的SQL语句,来继续追踪这个方法看一下,此时这个方法依然会在本类thinkphp/library/think/db/Connection.php这个文件中实现query方法。在这个方法中一眼就可以看见对于这个queryStr属性的设置,是直接给这个属性赋值,那么也就是说这个属性的值就是上一个SQL语句生成的SQL语句。所以说这个getLastSql获取的就是在这个语句之前执行的SQL语句,也只能获取出最近执行的那个SQL语句。以上就是关于getLastSql的实现原理,在这里需要注意的就是关于SQL的生成,这块属实有点复杂。五、总结截止到这里关于数据库中Db类的操作场景分析以及关于结合连接器,查询器,生成器就到这里结束了。这里咔咔主要就是使用了俩种案例来进行执行,第一种为原生案例,第二种为框架封装的案例。使用了这俩种案例来对源码进行了深度解析,但是在文档还有很多的实现方法,其它的方法只需要根据咔咔给的提示然后一点点解析即可。不需要对所有的方法都进行执行,不管任何方法走的都是上文分析的方法,也是很简单。最后在演示了一下关于使用getLastSql来获取最后一次执行的SQL语句查询,这里的实现原理主要就是在Db类操作数据库时,不管是使用find方法还是select方法最终都会走向一个方法那就是query方法。同样在这个方法中存在一个属性值queryStr,也就是在这个时候将SQL语句赋值进去的,然后在使用getLastSql这个方法使用queryStr和bind属性在对SQL进行拼接,最总返回SQL语句。
三、Db类库巧妙结合连接器、查询器、sql生成器使用在上目录中咔咔使用了query作为案例演示,这个使用在框架中是不建议使用的,因为在维护的方面会有一定的难度。本节案例将会使用框架常用的查询数据库方式进行查询。在上图中可以看到使用了平时最常用的查询方式,接下来将会对这组案例进行详细分析。同样代码会来到Db类的__callStatic这个方法,这个方法就是在调用没有声明的静态方法会进行执行的。这个方法跟__call方法是有区别的,__call方法是调用不存在的方法会进行调用,一定要注意俩者的区别。对于上图方法中static::connect()执行最后会返回 object(think\db\Query)这个对象,至于内部流程的执行可以参考第二目录的内容。所以执行流程会来到thinkphp/library/think/db/Query.php这个类的table方法。参数就是table中传递的数据库表名tp_test。按照上图提供的代码会对传递过来的表名进行三次判断。第一次判断是否为字符串第二次判断是否存在 )第三次判断是否存在 ,根据传递过来的字符串以上三个判断均不成立,于是会执行到下面流程。在table这个方法中可以看到最后的执行流程就是将传递过来的表名存放在属性options这个里边。并且最后会将think\db\Query Object这个对象进行返回。where方法解析table方法分析完成后会紧接着执行where方法,同样还是在类thinkphp/library/think/db/Query.php上图中在这个类中可以看到一个方法func_get_args,这个方法会返回一个包含函数参数列表的数组。这个方法平时都是跟call_user_func_array同时使用,之前咔咔也使用这俩个方法进行过一次案例实验。然后会使用函数array_shift删除数组中的第一个元素(red),并返回被删除元素的值。下图第一个结果为func_get_args这个方法获取出来的数据,第二组结果为array_shift这个方法返回的结果。俩组结果返回的值可以进行对比一下,可以更好的理解array_shift的使用场景。紧接着会进行分析查询表达式,也就是方法parseWhereExp做的事情。在这个方法中需要注意一个点就是关于传递过来的这俩个参数。参数一为查询逻辑,参数二就是在使用案例时传入的参数。在代码的第一行就需要我们来学习的一个知识点instanceof。instanceof可以判断某个对象是否是某个类的实例,判断一个对象是否实现了某个接口。关于这个的使用案例在文章ThinkPHP源码解析之控制器这一文中做了详细的说明。根据学习instanceof的作用可以清晰的明白第一个判断不会进行执行。在继续学习以下的执行流程,根据咔咔圈出来的框来进行对代码进行简单的解析。根据上图首先会对查询逻辑的符号全部转为小写然后在进行判断$field instanceof Where传递过来的参数是否为Where类的实例。最后一个判断就是$field instanceof Expression跟上一步是判断同样的功能。所以说代码最终的执行逻辑就是下图圈到的部分。还记得在案例过程中给where传递的参数就是一个数组。如果将参数改为where('t_id',1)则就会走is_string($field)的这个流程,这个流程就交给大家了,咔咔就不去解析。这里咔咔还是使用数组作为参数进行解析,那么代码依然会执行本类的parseArrayWhereItems这个方法在这个方法中先需要知道key会返回什么,从当前内部指针位置返回元素键名。所以代码会去执行if语句的判断,根据上边的所有判断都不符合所以会执行这段代码$where[] = [$key, is_array($val) ? 'IN' : '=', $val];这段代码会判断循环数组的value值是否为数组,如果为数组就是in,反之为=,由于value为1所以数组的第二个值为=。那么最终where的值就是下图打印的数据。由于where不为空,代码执行流程会执行到下图位置,最终在返回本类实例。find()执行流程接着代码会还是执行本类的find方法,查找单条记录。由于find中是没有传递参数的,所以代码会执行到$this->parseOptions();分析表达式(可用于查询或者写入操作)就目前写的案例而言,这段看似很长的代码大家好好看看都可以看明白,最终依然是返回当前的所有参数。以下就是返回的所以结果真正的查询数据是这块代码$result = $this->connection->find($this);,这段代码会执行到文件thinkphp/library/think/db/Connection.php从这块代码可以看到当查询一条数据时框架默认给加上了limit为1,至于为什么这么加你就需要查看一下sql优化方面的知识了。在这里就是关于sql语句的生成,代码自己好好看看就会明白,咔咔解析的只是执行流程和具体代码简单的了解一下即可。至于具体实现流程咔咔在后期如果有机会会单独把每个方法进行深度解析,那时就是主要针对代码的解析。最终返回结果如下以上就是关于Db在结合连接器,查询器,生成器实现的数据库查询功能。截止到这里关于Db的场景就分析到这里,接下来咔咔将会对Model进行简单的分析。
二、Db类库场景分析先从一个简单的案例进行解析,先来看一下数据库的数据。然后来到控制器写一个简单的查询案例,在创建控制器之前先使用命令进行创建一个测试控制器。在这个控制器进行简单的查询数据。查询结果如下在这个案例中,可以看到使用的是Db::query这种查询方式,接下来对于这种查询方式进行简单的剖析。接着执行流程就会来到Db这个类,在这个类中可以看到关于当对象访问不存在的静态方法时,__callStatic()方法会被自动调用。这个方法在之前门面的讲解中进行了深度讲解。从上图可以看到当执行访问对象不存在的静态方法时会执行到call_user_func_array调用回调函数,并把一个数组参数作为回调函数的参数接着代码就会执行到static::connect()这行代码,由于本类Db没有继承任何的类,所以对于static这个的使用就是调用本类。如果当Db类继承了其它类那么就会有一定的区别,这个区别就是关于static关键字,给大家做的一点点冷门知识得补充,当一个类继承一个类时,在父类实用static关键字时,默认调用的子类的方法。切换数据库连接因为没有任何继承,所以会来到本类的connect这个方法。在这个类里边首先会返回结果为数据库配置信息。然后会从配置信息中获取到query这个索引,最终返回\think\db\Query这个字符串,这里一定要注意返回的是字符串,不是这个类的实例。紧接着就会执行到第三步创建数据库连接对象实例,接下里将会对这一步进行解析。紧接着会来到文件实际执行的为 new \think\db\Query,最终会返回执行查询 返回数据集,返回数据为返回 object(think\db\Query)关于这个$this->connection是在本类的构造函数进行设置的。先简单的看一下这个构造函数,在这个构造函数中直接就设置了connection这个属性的值,所以在上图中可以使用。在这里执行完成之后就会将返回的值给从一开始就解析的这个调用未声明的静态方法会进行调用。其中static::connect()就是最终返回的值static::connect() 返回 object(think\db\Query)。所以接下来代码会执行到 thinkphp/library/think/db/Query.php 的 query方法$sql 就是在Db::query()中传递的sql语句,并且执行查询 返回数据集最后这段代码会执行think\db\connector\Mysql的query方法接下来来到think\db\connector\Mysql的query方法在这个方法中主要做了三件事情。$this->initConnect 初始化数据库连接$this->PDOStatement->execute(); 执行查询return $this->getResult($pdo, $procedure); 返回结果集解析$this->initConnect 初始化数据库连接在这个方法中可以看到是进行了一次配置信息获取,首先需要明白这个配置信息是什么。这个配置项是在配置文件database中配置的,根据注释提供的信息可以看到主要是关于主从服务器设置的。一般情况下是不会在框架中配置主从信息的,这里就不去解析框架是如何实现数据库的主从配置了。在这个判断中在进行了一次判断当前数据库连接的id,然后执行了连接数据库方法。这个方法最终会返回object(PDO)#33的一个实例信息。$this->PDOStatement->execute(); 执行查询第二件事情做的就是执行查询,接下来我们来详细说明一下这个到底是如何执行的。在返回pdo实例时,将这个实例赋值给了$this->PDOStatement这个属性,所以会去PDO类中进行执行。在这里大家需要明白一件事情就是关于execute这个方法,用于执行返回多个结果集、多个更新计数或二者组合的语句。第三件事情返回结果集直到这里就是执行的最后一步就是返回结果集。这里使用的方法都是查询底层,就在去解析了,在这里就会返回最终查询结果。最终结果就会返回给这里__callStatic方法,并且返回给上层的$res变量。直到这里关于使用Db查询的执行流程就解析完了, 但是框架给封装的方法不仅仅只有query,其它的查询方式可以按照咔咔的这个流程在进行简单的分析。最后执行的都会是这一节的最后几个流程,只是前边执行会有一点点区别而已。
前言在日常开发过程中模型的使用是非常之多的,但是在开发过程只知道如何使用,并不知道内在是如何实现的,模型是不管接口还是后台都会使用到的东西。关于视图在前后台分离的大趋势下,框架存在视图大多数还是针对于后台开发的使用。本文也是对框架解读快到最后阶段了,接下来咔咔将带领大家一起学习关于在框架中Db类的奥秘。下图为咔咔提供的脑图可以根据这个脑图进行阅读文章。一、Db操作类和其它类对应关系解刨在学习模型之前一定要知道的就是DB这个类,这个类也是对数据库的操作。在框架中存在这样一个配置文件,在这个配置文件里边会存在关于数据库配置的一系列信息。在接下来的过程中咔咔也会简单的创建一个数据库来做演示。同样在框架的核心层存在俩个类,分别为Db类和Model类,这俩个类就是接下来的解析对象。在解析Db操作类和其它类对应关系解刨之前,我们先创建一个数据库作为演示使用。首先先来看一下Db类的信息。通过上图我们可以看到关于Db类的一部分信息,就是使用Db类的一些查询方法。但是来到Db类的最后可以看到一个熟悉的方法__callStatic。这个方法在一直读咔咔文章的读者应该已经很是熟悉了,这个方法在门面源码解析那一节中进行过深入的了解。对于这个方法只需要记住的是在调用没有声明的静态方法时会进行调用。至于call_user_func_array这个函数的使用可以理解为,这个方法是内置函数,可以直接调用函数运行,也就是可以直接运行方法。在通过刚刚的查看Db类的注释信息时可以看到Db类是使用着Connection这个类,也就是连接数据库类。进入到这个类里边简单的看一下构造函数即可,至于是怎么一个运行顺序会在下文进行讲解。在框架中操作控制器有俩大场景,第一中为Db类操作,第二种就是Model操作。其中Connection·为连接器,Query为查询器,Builder为sql生成器,exception为异常类。知道了以上的几个信息,在接下来的理解过程中会有一定的帮助,在下一节中将会对Db类库场景分析。
4.汉化包咔咔不太喜欢使用汉化包,于是你们就自自行搜索即可搜索内容phpstorm汉化包_咔咔博客5. 插件Thinkphp5 Plugin 可以在控制里直接跳转到视图编辑SonarLint 编码不规范时,它就如站在你身边的大牛,提醒你CodeGlance 这个插件可以添加代码地图实用的插件咔咔就介绍这几个,这几个插件也是咔咔一直在用的插件。6.模板咔咔在编辑器里添加的模板只有俩个,一个是打印,一个是建立方法的模板,如果你有需要那就直接复制到你模板库直接使用即可。dump($END$);die; 1 public function $END$ () { $END$ }使用方法就是点击新增,然后在框里写上咔咔提供的俩个模板即可。7.代码颜色咔咔使用色号:F8F8F2四、特性在这次的新版本中,咔咔感觉有一个特别优秀的功能就是在底部栏出现的Problems这个tab这个功能会显示出这个文件中所有的语法错误、未使用的变量、未使用的文件都会分析出来。可以看到系统将分析出来的问题都列了出来,点击问题就可以直接到对应的行数。大家也能看到上边有四条错误信息,都是未定义类Db,在thinkphp5.1中给Db类注册了门面类,但是编辑器还是没有识别到。不过这个问题也不影响什么操作,咔咔感觉这个功能挺好的。五、问题一(分析)在第四节中咔咔说了新版本带来的特性,但是也还是有自己的问题。在上边的截图不知道大家有没有看到,方法名是灰颜色的。编辑器分析出来的这个问题属于未使用的元素,这个也是很尴尬哈!如果想要关掉这个提示,按照下图点击关闭即可然后在右上方将分析问题改为只分析语法错误就可以了然后就可以看到方法名的颜色就正常了六、问题二(鼠标悬停出现的框)就是在新下载的编辑器后,只要鼠标悬停在不管方法还是变量上都会出现一些信息,例如:方法来源于哪里、属于哪个文件、注释。这些信息在看源码时是很有用处的,但是在开发的过程中咔咔感觉没有什么用。至于这个信息框如何关闭,鼠标悬停后点击右下角的三个点,然后依次将选中的给关闭了就可以了。由于咔咔已经将那个玩意给关闭了,就没有了截图,知道怎么取消就行。至于怎么在添加回来,咔咔暂时还没有找到开关。七、配置dubug如果你使用的phpstudy继承环境,这一步可以略过!不会没有debug的编程是不完正的coding。读取phpinfo的信息,然后右键将源码复制出来打开网址将复制的源码复制到框里https://xdebug.org/wizard然后将适合自己版本信息的debug文件放到系统提示的位置然后给php.ini文件进行配置相关信息,以下是咔咔配置的信息[Xdebug] zend_extension = D:\phpstudy_pro\Extensions\php\php7.3.4nts\ext\php_xdebug-2.9.6-7.3-vc15-nts-x86_64.dll xxdebug.idekey=PhpStorm xdebug.remote_enable = On xdebug.remote_host=localhost xdebug.remote_port=9003 xdebug.remote_handler=dbgp xdebug.idekey="PHPSTORM"配置PHP版本信息debug的端口号不用修改,在新版本的编辑器里,将9003作为debug的默认端口配置DBGP信息按照咔咔箭头方向添加php web page当你打开这个页面的时候会是空的,还是需要配置server的,点击咔咔指向的箭头即可配置相关信息配置完成后,返回php web page配置项目地址然后打上断点,点击臭虫即可然后就可以直接进入到断点位置了以上就是关于phpstorm配置debug的解决方法。八、关于debug的扩展大多数的伙伴还是使用的集成环境来开发的,那么肯定都配置了虚拟域名,那么使用这个域名怎么进行debug呢!只需要修改对server的域名即可还有一种情况就是如何debug后台应用呢!还是刚刚的操作,将php web page的开始地址配置为项目的入口位置即可。同样的给后端需要断点的代码打上断点,然后点击臭虫即可然后从后台进入到你打断点对应的方法即可以上就是关于phpstorm配置debug的全部操作总结其实写这篇文章的原因就是在之前写过关于一些PHPstorm的文章,但是都一篇文章就解决一个问题。然而今天把这篇文章把咔咔使用phpstorm的所有功能基本都说全了,也就是说如果新安装一个软件,咔咔根据这篇文章可以打造出一毛一样的编辑器出来。习惯很重要,同样编码习惯工具习惯都是一样的。
六、如何输出数据到终端当执行完控制器中的方法响应数据给App类的run方法,直到这里就已经执行完了。是不是有点懵这里的数据最终会返回哪里呢!之前写过的框架执行流程、路由、控制器实例化都是从这里开始进入的。所以当run方法执行完成之后,就会把对应的结果给返回到这里。这一部分的代码Container::get('app')应该都知道了是返回一个App类的实例。然后通过App类去执行run方法,才会有之前讲过的一切。下图是咔咔从半中腰做的一个思维导图,前面的没有,后边的所有知识点都会写在这个思维导图里。执行完run方法就会去执行Container::get('app')->run()->send()send这个方法,有多少人会认为在App类里边执行send方法。其实不是的,回想一下之前执行控制器方法然后返回的响应结果是什么?如果你不是很粗略的看都会记得是Response的一个对象实例。所以说send方法会去response类里边去执行。先不看其它的,先看这行代码$this->app['hook'],现在知道是执行的那里吗?这种形式就是通过访问数组形式去访问对象的属性,也就是之前解析的ArrayAccess这个类。当访问的属性不存在时会去执行offsetGet,然后执行魔术方法__get,最终通过make方法返回实例,这一切的操作都是在容器中。对这行代码具体是监听的什么就不去做解析了。接着需要看处理输出数据的这行代码$data = $this->getContent();这个方法做的事情就是将传过来的数据赋值给本类的content属性。其实在获取输出数据这个方法中,请看咔咔圈出来的第一个地方感觉是很没有必要。可以看到根本对数据就没有任何的处理,只是简单的返回了,所以说框架有好的地方也有不好的地方,只有你去阅读了才会知道,否则你会对你经常使用的工具一无所知。在接着就是Trace调试注入,就是通过配置文件配置的,通过调用debug类实现的,这里就不详解了。然后就是缓存判断,缓存会在后文中单独拎出来讲,所以也是过。在接下来就对响应头的设置了,检测 HTTP 头是否已经发送,这块的东西就很重要了,也是平时接触不多的知识点了。headers_sent() : 检测 HTTP 头是否已经发送http_response_code() :获取/设置响应的 HTTP 状态码header : 函数向客户端发送原始的 HTTP 报头。最后一步,来了来了,它来了,它带着echo来了,执行了一个方法$this->sendData($data);给人一种媳妇熬成娘的感觉,终于来到的终点站,一个echo输出了咔咔几十天的心酸啊!为了到达这个echo咔咔是经历九九八十一难啊!战斗还未停止,同志仍需努力啊!那么到这里关于框架执行然后到应用初始化,在到路由检测、控制器的实例化、然后返回response实例,在通过入口文件执行send方法。最后将数据输出到终端,也就是一个echo的事情。虽然这里的战斗结束了,但是在下面还有一个非常重要的知识点,咔咔将重新提一节来进行说明。七、fastcgi_finish_request方法巧用在上一节中通过Container::get('app')->run()->send();在response类中执行了send方法,输出了数据。但是在输出数据之后还执行了一个方法fastcgi_finish_request();,给的注释是提高页面响应,接下来好好来扒一扒其中的奥秘。在PHP官网中看到这样一段话The script will still occupy a FPM process after fastcgi_finish_request(). So using it excessively for long running tasks may occupy all your FPM threads up to pm.max_children. This will lead to gateway errors on the webserver.在fastcgi_finish_request()之后,脚本仍将占用FPM进程。 因此,对于长时间运行的任务过度使用它可能会占用您的所有FPM线程,直到pm.max_children。 这将导致Web服务器上的网关错误。所以说在没有彻底的了解这个方法之前不要轻易的在自己的项目中使用这个方法。接下来咔咔将使用一个案例来演示这个方法的使用,仅仅只是演示使用,如果需要使用到项目中请仔细阅读文档应该注意的问题。案例演示公司有一个业务需要发送通知给用户,但是由于发送时间太久,非常费时间,有可能需要好几十秒的时间,更严重的会直接导致浏览器连接超时。在一个问题就是用户体验的问题,用户等待时间过程,体验当然不好。为了解决以上俩个问题,今天谈论的fastcgi_finish_request就派上了用场。理解对这个函数的理解其实就是发送响应给浏览器,用户等待时间大大缩短,但是PHP进程还是在运行的。这样就达到了来个目的,就类似于我们经常说的异步执行。直观的来说就是发送邮件有可能需要10秒,但是用户是没有感知的,用户点击发送邮件之后直接就返回发送成功,浏览器响应结束,用户做其它事情,后台进程继续执行发送邮件的任务。案例
四、路由地址是怎么进行控制器实例化的在上一节中我们对路由进行了三四期的讲解,最终讲解的位置就路由调度,那么设置的路由是如何执行呢!接下来使用这个路由作为案例还记得在开始进行路由检测时的返回值是什么吗?请看下图当时没有对接下里的代码进行详解,直接说明了实例化控制器,现在要说的就是记录当前调度信息这行代码。在这里$this->request是使用的当访问不存在的属性时会去执行容器类的魔术方法,最后通过容器返回一个实例。所以说代码会执行到下图位置,设置或者获取当前请求的调度信息通过在控制器实例化这里进行打印会发现在这里的返回的值是index,这个值是在控制器进行设置的,接下来来到控制器进行查看一下。来到init方法对result做打印查看结果,使用的是路由地址你知道到为什么这里的值发生改变了吗?在上文打印出来的值为下图,为什么在这里就是上图的呢!在路由那一节中最后一步就是发起路由调度,最后调用了一个路由到模块/控制器/操作这个方法。这个方法dispatchModule最后也是实例化一个类,接下来需要对这个类进行深究根据代码追踪可以看到其实就是think\route\dispatch\Module这个类来到Module这个类,又会发现继承着Dispatch类在thinkphp/library/think/route/Dispatch.php这个类的控制器中,会发现对dispatch这个变量进行了设置。这个时候回头在看一下路由到模块/控制器/操作这里的方法传入的参数是什么,哈哈所以说最终的值就是刚刚打印的只是单独的数组形式的。那么接下来的动作就跟不使用路由访问的流程一样的,就不用在进行解析了。直到这里关于路由地址是怎么进行控制器实例化的就结束了。关于给$this->app->controller传入的是index,返回的是整个类名,具体的实现过程就不去解析了,实现的方法是$this->parseModuleAndClass,可以自行进行研究哈!五、执行autoResponse调度在第四节中只提到了执行控制器中的方法是从下图的地方进行返回的,但是怎么返回的没有进行详解。接下里会用一丢丢的时间来说一下是如何执行的。访问路由地址为下图,可以看到返回的数据就是控制器中需要返回的数据。打印的值是下图地方,这里就需要明确哈!源码阅读就是这样需要一点点的进行摸索,时间长了就对其中的东西就明白了。接下来就对$this->autoResponse($data);这个方法进行深入的解析,这个方法按照字面意思就是自动响应。在这个执行流程的第一行中$data instanceof Response,对这个不了解接下来就没办法阅读了。不会和不明白的还是需要去解决的,阅读源码就这样,一点点的攻克才能获得胜利。关于instanceof的使用instanceof可以判断某个对象是否是某个类的实例,判断一个对象是否实现了某个接口。接下来咔咔针对这个做一个简单的实例给大家演示一下,就明白这个是怎么回事了。案例一首先建立俩个类,案例如下图。下图就是打印结果,可以看到第一个返回true,第二个返回false。判断某个对象是否是某个类的实例,也就是说$instance就是类Test的实例,所以会返回true。案例二案例二跟案例一是不同的,建立了一个接口,然后类实现接口的案例。最终返回结果全是true,也就是说如果一个类实现了另一个接口,那么在判断时都会是true。以上就是针对instanceof给出的俩种演示案例,对其理解就是判断一个实例是否为某个类的实例。那么就在回到正文,$data instanceof Response这行代码肯定不会成立,因为data传过来的就是控制器返回的值。所以说代码执行流程会执行到下图位置,使用了is_null函数来做判断,判断肯定为false,所以会执行以下的代码。在这块代码中前俩个点就不去解析了。第一个就是默认自动识别响应输出类型,这里就是在判断是否为ajax请求,具体实现方法等咔咔这次把框架源码解析完之后,然后每天会抽一点时间,对框架的一些方法进行一点点的剖析。第二处位置就是在配置文件获取对应的配置信息,看的是执行的rule类的方法,但是在方法中是执行的获取配置信息的代码。接下就需要对上文没提到的第三处进行解析了,也就是代码$response = Response::create($data, $type);来到类thinkphp/library/think/Response.php的方法create中,这个方法就是用来创建Response对象。这里只需要去关注一下咔咔圈出来的地方即可,在thinkphp/library/think/response这个目录下是不存在html的。所以代码会直接去实例化本类,然后进行返回。来到本类的构造函数就主要做一下几件事情。将返回值赋给本类的data属性设置页面输出类型返回状态码设置app实例对象头部信息然后代码会将返回值赋值给autoResponse这个方法的$response这个变量。最后就是将这个$response给返回出去,并且返回信息如下图打印结果。然后代码依然会向上层返回,回到最初的闭包函数。在咔咔圈出来的地方,下一行代码也是关于中间件,只需要知道最终返回结果跟上图打印的结果一样即可。最终返回结果回到thinkphp/library/think/route/Dispatch.php,咱们也就是从这里开始的解析的。将返回的结果返回给$data,然后在进行执行return $this->autoResponse($data);你没看错,这里的代码熟悉吧!这个时候返回的结果就是Response的实例,所以会直接返回$response。直到这里关于执行控制器中的方法,并且响应就都解析完了。不轮是设置的路由规则,还是直接使用模块控制器方法的方式访问最终都会通过上文的方式进行返回响应结果。
__get方法使用详解这个案例请看下图中的这个$this->hook。同样的道理先来调试一下这个$this是什么值。打印这个值都没什么必要,因为就是在本类中。在类中属性的访问应该都会,就是直接使用$this-> 即可。所以说当系统访问$this->hook这个的时候,由于App类是不存在hook这个属性的,所以就会去执行容器类的魔术方法。然后在去执行make方法,创建类的实例。三、执行控制器中的方法本文的请求地址为配置的域名。通过上文可以知道$instance的值就是app\index\controller\Index的实例。这块也是存在中间件的概念,依然如此中间件会在后文中单独提到,这里不做解释。在这里$this->app['middleware']->controller这段代码的使用,还能记得是使用的ArrayAccess还是直接为__get吗?这里是在使用访问数组的形式访问对象,所以使用的是ArrayAccess的形式,这俩种概念一定要区分清楚。接下来就会执行获取方法名,至于这个方法名怎么获取的是在本类的init方法执行的,这里只需要知道返回的是index即可。在这里需要注意的就是这行代码$this->rule->getConfig('action_suffix'),这里获取的是操作方法后缀。假如现在给这个操作方法后缀设置一个值会变成什么样子呢!给添加一个kaka的值,进行访问一下看会是什么结果。这个时候进行访问会提示indexkaka的这个方法不存在,是不是清晰可见了,说明这个参数是在为所有的方法名追加一个kaka。对获取当前操作名的代码进行扩展完成之后,紧接着就是if (is_callable([$instance, $action])) {,在这里可以看见我们的老朋友is_callable。对于这里的is_callable俩个参数通过上文都知道是什么了,第一个参数为app\index\controller\Index的实例,第二个参数为index执行操作方法。那么is_callable的作用就是检测在app\index\controller\Index类中的index方法名是否可以执行。很明显这里会返回一个true,因为在index类里边存在index方法的。这里在做测试之前一定要把刚刚在app的配置文件中配置的方法名后缀那个给取消掉。通过这个is_callable判断会存在三种情况,接下来咔咔将会从三个方面给大家进行解析。第一种情况:类里边存在可执行的方法首先返回一个 ReflectionMethod 类获取方法名 index未设置方法名后缀返回空设置当前的操作名在这个案例中没有设置参数,所以最终的$vars就是一个空数组。为了测试带有参数的这一段代码,我们需要对路由地址进行一点改动。在之前没有使用路由,而是直接使用的默认地址,接下来将使用这个路由地址使用这个路由地址进行一下数据的打印,可以看到就是我们设置的路由参数。这段获取请求变量的方法会进入到$this->request->param();这行代码框架是如何获取参数的访问地址:http://www.source.com/index.php/hello/hello在上文知道是通过$this->request->param()来获取参数的,那么在框架是如何获取参数的呢!根据流程代码会执行到下图,根据获取的请求方式来使用对应的方式来获取参数,在这里需要明确的是我们使用的是get请求。所以代码会执行到$this->param,当前请求参数和URL地址中的参数合并这里,在这里注意咔咔圈出来的地方。由于咔咔是使用路由方式进行的请求所以,在这里框架专门为路由封装了一个获取请求参数。来到这个route方法,看到注释就明白是用来获取路由参数的,但是还是需要在进入一层到input。在之前路由的那一期文章中在获取到路由参数的时候会把参数合并到request的route属性。所以说$this->route就是存放的这条路由规则所有的参数,包含路由参数。这时执行流程会执行到获取变量 支持过滤和默认值,在上文中$this->route穿进来的参数是false,所以说这块会直接返回。这里返回的结果会返回给上文我们开始解析的地方,也就是说这个$vars就是获取到的路由参数。第二种情况:类里边不存在可执行的方法当第一种判断执行is_callable判断类里边的方法不可执行时,就会执行到第二种情况。先来请求的一个没有设置的路由地址,看会返回什么。根据代码给的提示,我们来到index控制器建立一个_empty方法,然后在来请求一次,看一下会发生什么。根据打印结果就可以看出来当访问的方法不存在时就会去执行_empty这个方法。那么这个方法是怎么执行的呢!这种执行方式就是利用反射机制来实现的,关于反射咔咔之前专门出了一篇文章来讲解的,但是大家还是需要对着文档进行阅读查看。第三种情况:类里边不存在可执行的方法也不存在_empty方法这种情况就比较简单了,就是直接返回错误信息,关于这里异常处理咔咔也会在后文说到。三种情况执行完三种情况分析完了,最后都会去执行统计的方法。调用反射执行类的方法 支持参数绑定,也就说这里的闭包执行流程到这里就执行完了。关于后边的自动请求,在第五节中详细说明。
ThinkPHP源码解析之控制器前言一、实例化控制器二、关于ArrayAccess和直接执行魔术访问返回实例的区别三、执行控制器中的方法四、路由地址是怎么进行控制器实例化的五、执行autoResponse调度六、如何输出数据到终端七、fastcgi_finish_request方法巧用八、trait特性讲解总结————————————————前言在上文中对路由进行了特别的详解,也从应用初始化开始解析一直到路由调度返回给路由检测这一环节。路由检测获取到的值如下图,也就是路由调度最终返回的值。使用的路由规则为Route::get('hello/:name', 'index/index/:name');从上图可以看出重要数据都是在dispatc中存放的,接下来就会对控制器进行详解。最先说明的就是的当路由检测完毕之后执行的实例化控制器操作。一、实例化控制器先来看一下是怎么执行到实例化控制器吧!毫无疑问代码肯定是先从入口文件开始执行的,这里使用容器返回一个App的实例,然后去调用App类中的run方法。下来就会来到执行应用程序,在这个方法中也是在上文刚刚解析的路由。所以检测路由执行完就会去执行实例化控制器。在路由检测执行完之后返回的是think\route\dispatch\Module Object这个类,并且这个类赋值给了变量$dispatch接着看一下本方法的这块代码,这里使用的是中间件,在这快代码中还是用了闭包,对闭包的概念不清晰的就需要回头啃基础了。在上图中咔咔圈出来的一个地方就是$dispatch->run()这块代码,接下来就要对这块代码进行解析了。在检测路由最终的返回值可以知道其实这个方法是在think\route\dispatch\Module这个类中。接着就需要对这个类中的run方法进行解析了,这个方法也就是执行路由调度。在这个方法中不管是获取路由参数还是检测路由、数据自动验证都不会执行(是按照咔咔上文给的路由地址为案例)。所以根据上图代码就会执行到$data = $this->exec();这里。跟踪这个方法会到下图地方存在一个抽象类,这里需要知道的是抽象类。对抽象类做出解释抽象类不能被实例化有抽象方法的类一定是抽象类;类必须要abstract修饰抽象方法不能有函数体;即abstract function fun();抽象类中的非抽象方法,可以被子类调用非抽象子类继承抽象类,子类必须实现父类的所有抽象方法抽象子类继承抽象类,无需继承父类的抽象方法根据上图的原则可以看到Dispatch这个类是抽象类。所以就会有俩种情况, 一种是抽象类继承抽象类,无需继承父类的抽象方法。另一种是非抽象子类继承抽象类,子类必须实现父类的所有抽象方法。怎么去找谁继承了Dispatch这个时候是不是有一个疑问就是怎么去找Dispatch的子类。在这个图中可以看到本类Dispatch,但是还有一个dispatch这个目录。根据路由检测返回的数据可以轻而易举的就知道是thinkphp/library/think/route/dispatch/Module.php这个类。来到thinkphp/library/think/route/dispatch/Module.php查看exec方法。那么接下来的任务就是对这个方法进行深入的解读了。先看第一行代码$this->app['hook']->listen('module_init');,在这里使用了容器ArrayAccess用数组的形式访问对象,然后执行的魔术方法__get,当访问不存在的属性时会去执行make方法。使用编辑器追踪这个app会到thinkphp/library/think/route/Dispatch.php这个类里边,在这个类的构造函数中可以看到对于app这个属性是赋值了一个App实例。接着来到App类可以看到继承的是Container类。在容器这块已经不止一次的说过这块的知识点了,访问不存在的属性回去执行容器的__get魔术方法。所以说这块的参数会传入hook,并且会返回hook的实例,关于这个实例是怎么返回的在容器那一节中说的很是详细,可以去看一下哈!
前言使用这个layui已经快俩年时间了,刚刚新搭xadmin的框架,于是就出现了表单会重复提交这个问题,很懊恼啊!如果你想直接知道解决方案,那就直接到第三小节即可。一、起初操作首先来看看咔咔都做了什么操作。使用的xadmin模板,估计是没有把js文件引全面造成的问题。咔咔想实现的效果如下图。这是已经实现的效果,之前是没有的,就是因为加了这个玩意酿成的果。不知道你们平时开发是怎么找代码的,咔咔是直接去代码库里边直接搜链接格式不正确。下图就是咔咔搜索出来的结果,那么接下来就是引入这个js文件试试呗!于是拿起键盘就是干,将这个文件给引入进去以上就是酿成后果的原有!二、问题重现添加一条数据果然没让人失望,出现了俩条数据,哈哈!懵逼了三、解决问题其实出现这个问题就是粗心大意造成了,还有就是对layui框架还是不熟悉。解决方案就是将form这个js文件给干掉即可,是不是很搞笑,嗯呐!十分搞笑。当你打开layui.js文件时就会恍然大悟原来,layui.js自动所有包含了modules模块,我本地额外的引入了,所以会出现这样的情况。当然问题不仅仅是咔咔这种情况。如果连续引用两次layui.all.js文件, 会导致连续触发两次, 或者先引用layui.all.js文件,然后在引用layui.js文件也会触发两次, 请细心检查自己引用的文件!!!引用了layui.all.js文件之后, 再次引用了layui.js, 导致连续触发了两次!
八、检测域名路由先给大家把流程图画出来,然后根据流程跟这咔咔的节奏即可。首先要确认的一件事情就是检测域名路由是在执行应用程序中执行的。上层执行流程就是在入口文件哪里。首先代码会执行到routeCheck这个方法里边,那么就先看这个文件。先看注释,对这个方法的解释就是URL路由检测。在这个方法里边先是会对路由缓存进行检测,这块内容就是关于Cache的。在这个方法里边最重要的的就是路由检测 返回一个Dispatch对象就是这个方法。那么接下里就是看这个方法。首先要明确的就是传进去的俩个参数都是什么。$path : string(4) “blog”$must : bool(false)在检测URL路由中会做以下几件事情。pathinfo分隔符 : 把url中的 / 改为 |路由是否完全匹配检测域名路由默认路由解析接下来就只需要对检测域名路由流程进行深度解析。关于前俩个执行只是一些字符串的处理,看看就行,知道最终返回什么即可。同样在检测域名路由的执行中明确三个参数的含义。$this->request : 通过容器类的__get魔术方法,执行容器类的make方法,最终返回request的实例对象,这列不会的去看第六节的文章$url : string(4) “blog”$completeMatch : 路由是否完全匹配来到$result = $domain->check($this->request, $url, $completeMatch);这里,也就是本节的重点了。在这个方法里边会执行以下几个流程,会针对重要的执行流程进行深度解析。检测路由别名 : checkRouteAlias检测URL绑定:checkUrlBind判断路由参数添加域名中间件检测分组路由 : parent::check检测路由别名 : checkRouteAlias参数解释$request : request类的实例$url : 传过来的 blog在这个方法里边存在俩个需要明确的知识点strpos : 查找在字符串中第一次出现的位置strstr : strstr返回一个指针,指向string2在string1中首次出现的位置,strstr(“Helloworld!”,“world”);?>\n输出:\nworld!首先会对URL地址进行处理:返回blog获取别名路由定义 NULL以资源路由blog为例 返回false在检测路由别名中存在一个方法需要去在看一下参数就是上图中传入的blog来到这个方法,首先要明确的事情就是此方法在类thinkphp/library/think/Route.php中并且此类使用了think\route下的所有类这个方法就会把从检测路由过来的blog然后会在Route类中的alias属性里边进行获取,如果不存在则会返回NULL这个别名的使用会在下文中提到来到检测别名路由的最后return $item ? $item->check($request, $url) : false;也就是这行代码,从上图中就可以知道,这个item就是NULL并且最终将这个NULL给返回回去。检测URL绑定:checkUrlBind参数说明$request : request类的实例$url : 传过来的 blog在这个方法中只对下图咔咔圈出来的地方进行详解。来到方法getBind读取路由绑定,可以看到咔咔已经将传入的参数打印出来了。本方法是在thinkphp/library/think/route/Domain.php这个类里边,还记得在设置路由规则的$This->group就是使用的这个类,不知道的可以去看路由文章的第一节。同时在这个方法中会进行一次subDomain当前子域名的获取。在这个方法最终会返回www,主要看一下圈出来的第一个部分。通过request类中的host方法来获取当前域名,然后进行分割。返回数据:array(1) { [0] =>\n string(3) “www”\n}给子域名赋值:$this->subDomain返回最终结果返回子域名 : www接着就会返回到上层,在上层进行判断获取的当前子域名WWW。一些是所有的判断处理,第一个判断肯定是不会成立的,因为只返回了www,并没有.下边的判断是根据路由绑定进行的判断,这里只需要知道最总会返回NULL就可以了。知道了在底层返回了NULL,所以在这里的判断同样也不会成立,所以最终给上层返回的结果就是false。判断路由参数根据上图执行流程最终还是会返回到thinkphp/library/think/route/Domain.php这个方法check检测域名路由。然后开始进行判断路由参数。没有路由参数跳过不执行。存在路由参数:执行方法setRouteVars :设置路由变量 这个参数是在框架版本5.1.5以上才可以使用,由于咔咔使用的版本有点低,就不对其详解了。添加域名中间件关于中间件这里也不对其进行解释,因为后期会新开一篇文章来详解,本文还是以路由为重点哈!检测分组路由接着就会来到检测域名路由的最后一个流程,执行代码return parent::check($request, $url, $completeMatch);会跳转到类文件:thinkphp/library/think/route/RuleGroup.php,因为Domain类是继承RuleGroup这个类的。参数说明$request : request类的实例$url : 传过来的 blog$completeMatch : 路由是否完全匹配在这个方法中咔咔只会对这里的其中的一个流程进行详解,也就是合并分组参数。因为这个方法也是贯穿执行流程的一条主线,其余的都是方法都是在进行检测,判断。九、总结关于路由用了俩篇文章还没有结束,看了这么长时间的源码也就是路由这块是最复杂并且最难理解的。其中的类是一环套一环,路由先暂时了解到这里,后期在阅读其它源码时在进行其它内容补充。在路由这一篇中主要执行的流程图大家一定要仔细看。在就是通过在注册路由规则时的group这个属性最终返回的是Domain类,这里的内容一定要清晰。主要知道在路由中域名的配置流程,域名是在何时进行配置的。路由文件中的返回数组和在导入路由文件流程要有清晰的思路。再就是回顾之前学习的ArrayAccess,像访问数组一样访问对象。容器中的魔术方法__get方法,在这个魔术方法中存在make方法,主要用来返回一个类的实例,并且存放到容器中。关于路由的方面暂时就说到这里的,预计在有一篇就会把路由写完了。
四、微信开发者工具中运行项目项目创建成功后就需要运行了来到项目目录执行命令npm run dev:mp-weixin看到下图提示即可。运行完毕之后你会在项目目录中看到以下结构,会多出一个dist目录需要给微信开发者工具导入的项目路径就是下图咔咔圈起来的地方微信开发平台怎么导入项目自己研究哈,下图就是咔咔运行的展示图总结以上就是咔咔使用vue的脚手架创建uni-app项目的全过程。初次接触vue更是初次接触uni-app来开发小程序,所以对于文章中部分命令的执行和问题的解决如果有什么不对的地方,望大家可以给提出来。在这个创建的过程中咔咔也是根据其它技术文章一步一步操作的,最后总结出来的文章,也是为了给跟我一样同样的新手一点帮助。
前言本文主要针对小程序框架uni-app的创建展开介绍,如果你也是新手那就可以跟着咔咔的节奏一起来。一、安装node.js打开官网安装咔咔圈起来的版本。安装完成后使用node -V来检验一下是否安装成功为什么要安装node.js估计很多跟咔咔一样是写后端的有点理解不了。因为node.js软件内置了npm,所以windows系统在安装nodejs后,打开cmd即可使用npm下载资源.linux在这里就暂时不说了哈!npm,全称【node package management】,是nodejs内置的软件包管理器,这也就是为什么要安装node.js的原因。二、使用npm安装vue脚手架网上看了一下大多数第一步让执行命令npm install -g @vue/cli但是安装的vue-cli是2.9版本的,但是在创建uni-app项目时需要使用create命令,所以需要更高的版本。这里也是咔咔踩过的一个坑,你们就不踩了哈!如果已经安装的可以使用npm uninstall -g vue-cli这个命令进行卸载。然后执行npm install -g cnpm这个命令。看网上有人使用的淘宝的npm install -g cnpm --registry=http://registry.npm.taobao.org 安装淘宝的cnpm。咔咔理解的就是一个镜像,但是咔咔有神器所以就直接使用安装了,没有使用淘宝的。安装成功就是下图的样子然后再次安装vue的脚手架,执行命令cnpm install -g @vue/cli再次安装@vue-cli下图为安装过程,流程没有截完。安装完成后再使用vue -V查看一下版本就是4.5.9的了,只要大过3就可以了。三、创建项目经过上面无伤大雅的安装之后,那么我们接下来就是在面向窗口cmd中输入:vue create -p dcloudio/uni-preset-vue 项目名称;创建项目首次创建会提示,我们选择默认即可,回车即选。不知道你们在创建项目时会不会出现以下问题,但是咔咔这里出现了问题,咔咔将这个问题的出现和解决方案写下来。如果有遇到的就跟这来,没遇到的直接跳过即可。下图就是安装过程中出现的错误。解决方案使用管理员模式打开命令行执行命令npm cache clean -f清除缓存清除完缓存后,安装最新版本的Node helper:npm install -g n然后执行以下命令npm install -g n --force最后执行npm install即可。然后重新执行创建项目`vue create -p dcloudio/uni-preset-vue lottery看到下图咔咔圈到的就代表已经创建成功了。
接着来到构造函数的方法中查看。这里一定要注意第二步的注释,这个已经说了很多次了。如果有疑问就是为什么使用了ArrayAccess。咔咔就给大家捋一下,首先App类是继承的Container类。Container类又继承着ArrayAccess类,所以就可以使用像之前的说,像数组一样访问对象。注入的App类此处使用了ArrayAccess像数组一样访问对象,但是$app中不存在request属性,所以就会去执行容器类中的__get魔术方法,在__get方法中调用的是容器中的make方法,第一个参数为request,最终会返回request的实例。注入的config类此属性返回域名,初始化默认域名接着就会来到方法setDefaultDomain。这里需要注意一下咔咔圈的地方,在这一节的开头就使用的这个属性来执行的rule中的addRule方法。这个group就是think\Route\Domain这个类。又因为think\Route\Domain继承着think\Route\Group所以会执行到think\Route\Group到这个类里边。这下所有的流程就都已经理顺了。通过上面的这一顿分析和刨铣相信大家对这一块的内容就十分了解了。四、路由规则预处理接下来的内容就是对路由规则的处理方式进行解析。这几个参数就对前俩个做一个简单的打印,然后看一下这俩个参数分别都是什么。参数rule参数$route可以看到这俩个参数几个就是 路由的前半部分,一个是路由的后半部分。从而就可以得知代码会执行到下图第一处圈起来的地方。此时需要注意一点就是在上图中圈起来的第二处地方,这个地方会在什么时候执行呢!就是当下图路由这样设置的时候才会执行那段代码。这里的路由设置只是为了做演示,在实际工作中不能这样设置路由啊!第二个路由地址会把第一个路由地址给覆盖的。接着代码就会执行到创建路由规则实例,也就是下图圈出来的地方。关于创建路由规则实例的几个参数需要进行简单的介绍一下第一个参数:$this->router第二个参数:$this,就是指的think\route\Domain。接下来的几个地址就是路由规则和地址,就没有必要看了,主要就是第一个参数。接下来就需要进入到创建路由规则实例的方法中代码就会追踪到thinkphp/library/think/route/RuleItem.php这个类中。在这个类中做的事情就是设置规则,也就是下图咔咔圈出来的地方。路由规则预处理setRule方法解读。上图中最后执行的流程就会来到setRule这个方法。这段代码没有我们眼看这那么容器阅读,接下来就跟着咔咔一步一步的去阅读这块代码。
框架路由解析前言一、路由初识化简单分析二、通过定义路由再谈门面三、路由定义rule方法中的$this->group到底执行了什么四、路由规则预处理五、解析生成路由标识的快捷访问六、总结前言使用框架写过项目的肯定都使用过路由,使用路由来进行接口的管理,那么为什么要使用路由呢!使用路由会保护项目的真实请求路径。使请求地址更加规范和简洁,在开发过程中方法名有时候会很长,就可以直接使用路由进行简洁处理。可以统一对请求请求进行拦截并且进行权限检查的操作。并且在5.1版本支持了注解路由,方便在开发的过程中进行调试。方便直接对请求进行缓存,并且还支持了路由中间件。接下来咔咔会对路由的方方面面进行全面的解析,并且会给大家带上脑图方便大家最直观的预览。一、路由初识化简单分析在框架执行流程那一篇文章中,都知道路由初始化是在初始化应用那个过程中执行的。然后进入到routeInit这个方法,进行代码解析。来到这个方法先看代码注释,注释为导入路由定义规则。这段代码的全部我给复制出来了,接下来就是对这段代码进行解析。/** * 路由初始化 导入路由定义规则 * @access public * @return void */ public function routeInit() { // 路由检测 // scandir:返回置顶目录的文件数组形式 $files = scandir($this->routePath); foreach ($files as $file) { if (strpos($file, '.php')) { $filename = $this->routePath . $file; // 导入路由配置 $rules = include $filename; if (is_array($rules)) { $this->route->import($rules); } } } if ($this->route->config('route_annotation')) { // 自动生成路由定义 if ($this->appDebug) { $suffix = $this->route->config('controller_suffix') || $this->route->config('class_suffix'); $this->build->buildRoute($suffix); } $filename = $this->runtimePath . 'build_route.php'; if (is_file($filename)) { include $filename; } } }首先会获取route目录下的文件,函数scandir会返回指定目录的文件并且用数组形式返回。这里返回结果有三个,第一个为当前目录,第二个为父级目录。这俩个数据不用过多追究。接着就会将route目录下存在php结尾的文件给导入进来,也就是route.php文件。当把路由文件导入进来之后,进行了一次判断然后进行导入规则。但是在路由文件可以看到是没有返回任何数据的。路由文件的return演示那么这里的return是干嘛的呢!在5.1版本之前是没有这一操作的,但是在5.1是存在的,接下来咔咔给大家演示一下这个使用方法。首先在路由文件的return中配置一条数据。然后在index文件中创建一个新的方法vpn此时可以直接访问路由vpn即可。当route文件的return存在的数据的时候,就会执行到$this->route->import($rules);这一步,本节暂时不对这里做出探讨,会在后文中大家详细说明。
咔咔在学习过程中会大量写关于一些解决方法和解决方案,但是在写文章时就会遇到几点问题。点开编辑文件时左侧列表不能定位源文件定位到源文件时会展示出这个文件的所有方法截图时特别不方便为什么会说截图不方便,因为在截取对应的代码时你需要直接指出对应的文件是在目录的那个位置啊!要不看了你的文章的人都是迷迷糊糊的,都不知道这段代码在哪里。为了解决上边咔咔说的几个问题,咔咔也是把phpstorm的英文设置用翻译软件大概翻译了一下,解决了困然咔咔好久的问题。实现方式一在对于代码进行截图时,就会遇到你编辑的文件在左侧没有准确的定位到。这时就需要花费一点点时间去找这个文件,对文件目录熟悉的话还可以。如果碰到一个陌生的框架和代码找起来就特别的麻烦。先来看这个问题怎么处理。第一种解决方案,就是点击咔咔用框圈出来的地方,只要你在右侧打开编辑的文件,然后点击一下那个圈就会自动定位到编辑的文件。虽然说这种方案可以间接的解决咔咔所说的需求,但是还是有点麻烦,因为每次都需要点击一次,这样每次点每次点,真的是很烦。那么这个时候怎么弄,继续看。咔咔圈起来的地方翻译过来就是自动滚动到源,那么点击上试一下。这下就很舒服了,点击那个文件就会自动定位到那个文件。那么第一个问题就完美的解决了,也不用每次去点那个圈了,是不是很是舒服,反正对于咔咔来说可以节省很多的时间。因为咔咔平时就是写文章,所以这个功能对咔咔是非常实用的。但是你会发现定位出来的文件会把这个文件中的所有方法全部给展示出来。解决第二个问题截图的时候又需要把这个文件的所有方法给隐藏掉,还是多了一道工序。这怎么可以,咔咔是真的有一丢丢的强迫症。那么就继续解决这个问题。在来到刚刚的设置界面可以看到这个英文的意思就是显示方法。那就直接点就给关闭了呗!经过了这一系列的处理,终于解决了咔咔的俩大心头之患,这下在写东西就省事了很多。虽说就这俩个小功能,但是咔咔也是使用了phpstorm这款软件长达三年之久了,对这款软件还是很不熟悉。这俩个功能点对咔咔的帮助可不是一星半点的多。让咔咔在截图的时候在也不用去对左侧的文件进行标注了,因为在定位源文件时会有高亮颜色显示。咔咔也不用在去找文件在左侧哪里了。就这俩个功能得省多少事情啊!使用idea是为了方便开发的,不是给自己添加麻烦的,所以没事还是对自己的工具好好了解了解吧!会对自己的开发效率提升不少的时间。
ThinkPHP容器源码深度解析前言一、单例模式二、注册树模式三、如何理解控制反转和依赖注入四、必会反射机制五、玩转自己的容器类六、Container容器类剖析之Countable巧用七、Container容器类剖析八、容器源码阅读后总结前言在这之前已经剖析过了类的自动加载、配置文件加载的源码解析,本文为第三期的文章,主要针对容器以及门面类的实现,解析源码。以及学习实现此功能的一些知识点。第一期文章:ThinkPHP自动加载Loader源码分析第二期文章:ThinkPHP配置文件源码分析一、单例模式在学习容器以及门面之前需要必须了解的俩个设计模式,单例模式、注册树模式。先对单例模式做一个简单的说明。拥有一个构造函数,并且属性为private拥有一个静态成员变量来保存类的实例拥有一个静态方法来访问这个实例一下就是咔咔实现的一个简单的单例模式,对照一下上面的三大特性看是否一致。静态变量为instance拥有构造并且还是私有的最后一个就是有一个getInstance这个静态方法接下来进行一下简单的测试还是在index控制器中做测试,为了证实其类只被实例化过一次,调用了其四次访问这个方法来看一下new-class只执行了一次,就直接证明了创建的类只实例化了一次。在这里咔咔之前有过一个疑问就是,这里的构造函数为什么要使用私有的属性。你之前有过这个疑问吗?咔咔带你一起来解答一下在本类定义私有属性的构造方法是为了防止其类在外部被实例化。当在外部实例化这个类就会报下图的错。那么为什么会在这里提一嘴单例模式呢!是因为在接下来的学习容器的源码中会使用到例如下图thinkphp/library/think/Container.php类中就存在一个获取当前容器的实例。截止到这里单例模式就简单的了解完了,了解单例模式也是为了更好的理解容器。二、注册树模式为什么在这里说这个注册树模式,因为在框架中注册树模式就是一个主导位置,所以必须去了解它!那什么是注册树模呢!注册树模式就是将对象实例注册到一颗树上(这里的树可不是真的树啊!就是注册到一个全局的属性里边)然后可以通过内部方法从全局的树上获取对应的对象实例。这样说的话肯定也不能更好的理解,接下来咔咔带大家看一个简单的案例来简单的了解一下。一个注册树模式需要的东西就是四个,注册树的池子,将对象挂载到注册池里,从注册池里获取对象,从注册池里卸载对象。如下图是咔咔写的一个简单的注册树模式。代码如果看不懂的就需要去补补基础了哈!接下来在到同一目录创建一个TestTree文件来到控制器测试写的注册树模式是否有问题在做测试的时候一定要注意命名空间问题哈!这里的kaka目录是之前在类的自动加载哪里配置的,如有不会的可以去第一期文章查看。这里就相当于先把TestTree这个类实例化出来然后使用注册树模式把这个实例注册到object树池子中最后使用get方式将这个类获取出来就可以直接调用TestTree中的方法了。最后看一下最终打印结果,结果就是TestTree类中getTreeContent方法的返回值。注册树模式就是以上咔咔说明的这些内容,就是不去针对源码学习,这些内容也是我们必须要去学会使用的。
十一、解析如何获取config如何获取配置都知道在获取配置信息的时候直接使用\Config::get()就可以获取到配置文件的信息。接下来咔咔就来剖析一下获取配置的流程。框架给提供了几个方法来获取配置信息。\Config::get(‘配置参数’);\Config::get(‘配置文件’);\Config::pull(‘配置文件’);这其中估计使用第一种的就很少了,第一种的方式就是直接获取所有配置文件中的对应的配置。例如:想获取config目录下的应用名称配置就可以直接用\Config::get(‘app_name’);来直接获取那么这个流程是怎么样的呢!当直接获取配置参数时,走的代码流程就只有这俩个。第一段是给加上前缀app第二段是循环在config文件中获取数据。这段代码如果你直接断点调试的话是看不到什么效果的,咔咔把这段代码给大家移到外面去执行,就会看的很清楚了。咔咔将这段代码给移到了index控制器中,这样就可以看到的很清晰了先看打印结果,确认没啥问题其实这里的代码如果放在源码中执行你会看到很多其它的信息,会很影响信息的解读的。但是咱们移植出来后,就可以确保代码的运行时没有其它的杂乱信息,有利于对信息的正确解读。然后紧接着看这段代码,这段代码之前咔咔看的时候感觉没什么,但是越看你会越发现这块代码的设计很是优秀。为什么会这样说呢!首先这段代码会走第一次循环就是执行app,这次执行会在全部的config中获取出键值为app的配置信息。然后把值再次赋值给config变量,执行第二次循环为app_name。这里循环获取数据就是在第一次循环获取数据的基础上得到的。也就是第二次是在$config[‘app’]下获取的数据。由此可见这段代码设计的是多好啊!至于其它俩个方法就交给你们了,可以简单的试着跟着咔咔一样把代码移植出来,然后一步一步的解读。你就会发现代码的优美之处,看的多了,对于以后自己写代码也会提供很多的思路的。十二、总结对于框架中config源码的解析就到这里结束了,其实源码的解析并不是很多,而是用了大量的篇幅来介绍了间接使用的一些技术。虽说这些技术在这个已经成型的框架中不能再进行好好的利用,但是最起码让我们知道了他们每一个扩展的作用。例如Yaconf对于项目配置这块会有很大的帮助,可以让配置文件跟项目分离,确保项目安全和跟运维之间的协同。在例如开篇说的ArrayAccess,这个就是提供像访问数组一样访问对象的接口而已,这个也就是一种好的思想,同理在以后得开发中也可以借鉴这种思想。在配置文件这一篇中,咔咔认为最重要的就是使用工厂模式加载的不同类型配置文件,在这一节中咔咔也说了后期会在出一篇文章在进行解析的,这一节点的文章如果没事的话真的可以好好的阅读一下。这个也是目前在阅读源码的过程中直接碰到的第一个设计模式,后边会遇到越来越多的设计模式,遇到在进行解析之前跟着咔咔一起实现的优化框架源码的过程中,这个配置一定要改过来,否则你需要把框架所有的配置类型都需要改为对应的。
二、简说类的加载过程在刚刚开始解析这里的源码时就有一个函数spl_autoload_register当需要使用的类没有被引入时,这个函数会在PHP报错前被触发,未定义的类名会被当作参数传入这里会直接去执行think\\Loader::autoload这个方法经过断点第一个未加载的类就是think\Error为什么是think\Error呢!可以在回到thinkphp/base.php看一下,当自动加载完执行完成后第一个执行的类就是Error可以简单的做个测试,将这Error改为Kaka,进行打印一下,这时的类就改变为Kaka。到这里大家对这个类的自动加载机制就有一定的了解了。当使用的类没有被引入时会把这个类当做参数传到thinkphp/library/think/Loader.php的autoload方法中。到这里在进行看一下autoload这个方法先从findFile这个方法走,把未引入的类传入这个方法中,在findFile这个方法中会直接从classMap这个属性中直接把think\Error这个类映射的文件直接返回出来将think\Error这个类的完整路径返回给autoload的file变量后,把win环境的大小写给判断了一次。然后直接使用include引入文件即可,直到返回。直到这里就是一次完整的类的自动加载解析。虽然到这里结束了,但是还是得在提一点就是$classMap这个属性,这个属性是基于文件classmap.php来到,这个文件的生成也是需要执行命令php think optimize:autoload生成的。当没有生成这个文件时程序是如何执行的呢!之前的所有流程都是一样的,只有在findFile这里不一样,接下来进行简单的梳理一下。这时代码肯定不会走classMap先获取think\Error文件然后经过Composer自动加载中的俩个属性进行获取命名空间,在把think\Error.php文件进行拼接最终返回的结果也是D:\phpstudy_pro\WWW\ThinkPHPSourceCodeAnalysis\thinkphp\library\think\Error.php这个文件。这里的代码需要好好的阅读一下。类的自动加载到这里就是完全结束了。
三、优化翻译速度在第二步中,咔咔做到最后是发现翻译的速度是有点慢的,碰到字段多的更是慢的离谱。于是咔咔就想了一个办法。这些数据是之前是一个一个翻译出来的,那么我们是不是也可以一次性给翻译出来,然后我们在来自己组装数据。带着这个想法我们开始实施。这几行代码最终实现的就是把所有的字段注释放在一起,并且用“,”隔开打印出来的数据可以看一下,out字段就是翻译出来的数据,其实想都不用想一次翻译肯定比6次翻译速度快。然后拿着这组数据在转化成数组在重新组装到原数据里就ok了。这里有个小问题,可以一起关注一下。中文打印出来的是原始数据里边的,英文是数据经过处理的。很明显可以看到返回的英文结果跟原始数据对不上。这里的处理也是比较简单,如果你有更好的方案评论区见处理方式在获取数据库字段中文注释之前把对应的索引保存到$needkey然后重新定义一个变量fanal,让fanal的索引等于needkey的v,而对应的值就是result[needkey的k]四、总结这样一个翻译接口的使用就完成了,项目截图就不给大家展示了哈!把多次翻译改为一次翻译,提升翻译速度,毕竟是请求别人的东西,肯定没有我们自己重组数据快。最后给大家发一个所有语言的对照表
前言项目中必要的数据是需要写语言包的,就像那种几百年不变的数据,但是有一类数据就是在项目的运行的过程中就在一直变化。对于这样的数据我们写成语言包显然是不合适的,所以我们就需要借助翻译接口来实现我们的需求了。一、翻译接口简单介绍先看一张图吧!看一下这个翻译接口在我们的项目中如何运用。这个接口总共有4个参数,分别为a、f、t、w。这四个参数分别的意思为,a是固定的值就是fy。f指的是翻译的语种。t指的是需要翻译成我们需要的语种。w指的是需要翻译的数据。二、项目实战项目需求就是把左边的框出来的在切换语言后跟随这语言变动。首先说明一下,在上边的这些文字不是手动写上去的,html文件也是不存在的。是在数据库注释里边配置的。下图就是我们的数据库创建。为什么这么创建,这样创建有什么好处,我就不提了,每个团队都有自己的想法哈!接着我们来到正题使用的代码也就这点,接口地址就是上边postman里边的地址,后边只需要传输一个需要翻译的数据即可。本文实现的数据就是上图左侧的文字,也就是我们从数据库中把注释读出来然后根据一定的规则把这个名字就直接作为添加或者修改时的列即可。那么就只需要把这个注释直接给追加到翻译接口后边即可。给你提供一个php发起curl请求的代码public function translateRequest($url, $data=array()){ $ch = curl_init();//初始化 //curl_setopt();//设置 //设置 curl_setopt($ch,CURLOPT_URL,$url); //需要获取的 URL 地址 curl_setopt($ch,CURLOPT_HEADER,0); //启用时会将头文件的信息作为数据流输出, 此处禁止输出头信息 curl_setopt($ch,CURLOPT_RETURNTRANSFER,1); //获取的信息以字符串返回,而不是直接输出 curl_setopt($ch,CURLOPT_CONNECTTIMEOUT,30); //连接超时时间 curl_setopt($ch, CURLOPT_ENCODING, 'gzip'); //避免https 的ssl验证 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_SSLVERSION, false); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); if($data){ curl_setopt($ch, CURLOPT_POST, 1); //post请求 curl_setopt($ch, CURLOPT_POSTFIELDS, $data);//post参数 } //执行 $data = curl_exec($ch);//执行 不输出 内容返回给它 //判断是否请求成功 if(curl_errno($ch)){//错误码 echo 'curl error: '.curl_error($ch);//错误信息 } $response = curl_getinfo($ch); switch($response['http_code']){ case 200: return $data; break; default: exit('程序异常'); } curl_close($ch);//关闭 } 经过测试后看看效果,效果是实现了,但是你们可以私下试试哈!翻译一组数据时速度还行,但是翻译几个数据时就有点慢了。添加页面打开的时间大概需要3-5S,这肯定是不可以的,所以我们需要想一个办法来解决这个问题。
实现多语言后台前言一、后台实现多语言二、视图切换语言三、测试四、总结前言公司需要写一个多语言后台,以前也没做过,于是到面向百度了一下。这是用了TP框架俩年了,第一次发现框架是自带的多语言切换功能。这里着实给点一个赞。接下来我们来实现一下这个功能。一、后台实现多语言使用框架为v5.1.38 LTS来到配置文件config/app.php可以看到关于语言的配置信息就只有这俩个参数。lang_switch_on这个参数是自动监听的浏览器语言,default_lang关于这个参数就是在没有切换其它语言之前就直接默认中文配置文件了解之后我们需要在需要的模块创建lang目录,并且配置对应语言的语言包。这里咔咔只配置了英文和中文 如还需要其它语言可自行添加即可。到这一步你会发现从头到尾都没有一个参数说是配置语言列表的参数。配置文件也没有找到。但是在文档里你可以看到这行代码。我们去源码看看在源码中就可以看得明白,语言的cookie的name值也是固定的,并且cookie的有效期时间为3600,这些都是需要根据自己项目进行调整的。最重要的是看允许语言列表。我们继续追溯这个变量在文件thinkphp/library/think/Lang.php最后我们就可以看到一个方法,这个方法也正是文档给出的一个方法由于我们整个后台都需要使用多语言,所以我们需要在common中进行配置二、视图切换语言在视图模板里直接调用languageChange方法在视图中有一个变量为$languageName,这个变量是咔咔直接从配置文件直接取出来的,后期会调整到数据库中。然后在控制器写方法changeLanguage,用来处理语言切换后的cookie改变此时我们的后台页面就是这样的三、测试在经过上面的操作之后,我们就可以根据自己设置的语言包来进行语言切换了在模板中我们就需要使用lang这个模板标签进行获取数据这个是英文状态这个是中文状态到这关于多语言的实现就完成了。四、总结上面说了这么多,只是把每一步的步骤给大家写出来了。其中关键点如下application/config.php配置文件修改在对应的模块里添加lang目录,并且创建需要的语言包视图进行语言切换调用后台接口存储cookie值。保存在think_var模板使用lang标签即可
数据库分库分表估计很多伙伴都没有实践过,就是因为自己公司的业务不是很多,没有那么多数据。假如有一天项目的人数上来了,你写的系统支撑不住了,希望这篇文章带给你一丝丝的思路。这里写目录标题前言一、初始架构二、用户开始激增解决方案三、保证查询性能四、配置读写分离来按需扩容五、并发数据库结构总结前言在面试过程中你是不是会经常遇到对于数据库分库分表你有什么方案啊!在平时看博客时你是不是也经常刷到MySQL如何分库分表。然而你是不是点进去看了不到10秒就直接退出窗口。那是因为写的文章不是什么水平切分,就是垂直切割,在一个自身所在的公司根本使用不到。如果你只知道分库分表但是不知道怎么弄的话,花个五分钟看完你会收获到不一样的思路。一、初始架构公司的规模小,项目针对的用户群体属于小众。日活就几千、几万的用户这样的数据每天的数据库单表增加一般不会超过10万。并发更不沾边了。这种项目规模,我们就是认真、快速开发业务逻辑,提升用户体验,从而提升项目的用户粘度并达到收纳更多用户的准备。这个时候我们项目一台16核32G的服务器就完全可以了,可以将数据库单独放一个服务器,也可以都放在一个服务器。这个时候项目架构图是这个样子的。二、用户开始激增解决方案在经历了第一阶段后,由于项目的用户体验度极高,项目又吸引了大量的用户。这时我们项目日活达到了百万级别,注册用户也超过了千万。这个数据是咔咔根据之前公司项目数据推算的。这时每天单表数据新增达到了100万,并发请求每秒也达到了上万。这时单系统就扛不住了。假设每天就固定新增100万,每月就是3000万 ,一年就是接近5亿数据。数据库以这种速度运行下去,数据到2000W到3000W还能勉强撑住,但是你会发现数据库日志会出现越来越多的慢查询。虽说并发1W,但是我们可以部署个10台机器或者20台机器,平均每台机器承担500到1000的请求,还是绰绰有余的。但是数据库方面还是用一台服务器支撑着每秒上万的请求,那么你将会遇到以下问题。数据库所在的服务器磁盘IO、网络带宽、CPU负载、内存消耗都会非常高单表数据已经很大,SQL性能已经已经出现下坡阶段,加上数据库服务器负载太高导致性能下降,会直接导致SQL性能更差这个时候最明显的感觉,就是用户获取一个数据可能需要10s以上的时间才会返回。如果前期服务器配置不是很高的话,你就会面临数据库宕机的情况那么这个时候我们要怎么对项目进行架构的优化呢!根据大佬们的经验是数据库的连接数每秒控制在2000即可、那么我们1W并发的情况下部署5台机器,每台机器都部署一个同样结构的数据库。此时在5台数据库拥有同样的数据库。表名规则可以这样设置。这时每个project库都有一个相同的表,比如db_project1有tb_play_recode1、db_project2有tb_play_recode2…这样就实现了一个基本的分库分表的思路,从原来一台数据库服务器变成了5台数据库服务器,从原来一个库变成了5个库,原来的一张表变成了5张表。这时我们就需要借助数据库中间件来完成写数据,例如mycat。这个时候就需要使用播放记录表的自增ID进行取模5,比如每天播放记录新增100W数据。此时20W数据会落到db_project1的db_play_recode1,其它的四个库分别也会落入20W数据。查询数据时就根据播放记录的自增ID进行取模5,就可以去对应的数据库,从对应的表里查询数据即可。实现了这个架构后,我们在来分析一下。原来播放记录就一张表,现在变成了5张表,那么每个表就变成了1/5按照原项目的推算,一年如有1亿数据,每张表就只有2000w数据。按照每天新增50W数据,每张表每天新增10W数据,这样是不是就初步缓解了单表数据量过大影响系统性能问题了。另外每秒1W的请求,这时每台服务器平均就2000,瞬间就把并发请求降低到了安全范围了。三、保证查询性能在上边的数据库架构会遇到一个问题,就是单表数据量还是很大,按照每年1亿的数据,单表就会有2000W数据,还是太大了。比如可以将播放记录表拆分为100张表,然后分散到5台数据库服务器,这时单表的数据就只有100W,那查询起来还不是洒洒水的啦!在写数据时就需要经过俩次路由,先对播放记录ID取模数据库数量,这时可以路由到一台数据库上,然后再对那台数据库上的表数量进行取模,最终就会路由到数据上的一个表里了。通过这个步骤就可以让每个表的数据都非常少,按照100张表,1亿数据,落到每个表的数据就只有100W。这时的系统架构是这个样子的。四、配置读写分离来按需扩容以上的架构整体效果已经很不错了,假设上边分了100张表还是不满足需求,可以通过用户增量计算来配置合理的表。同时还可以保证单表内的SQL执行效率。这时还会遇到一个问题,假如每台服务器承载每秒2000的请求,其中就400是写入,1600是查询。也就是说,增删改查中增删改的SQL才占到了20%的比例,80%的请求都是查询。安装之前的推理,现在所有数据进行翻倍计算,每台服务器并发达到了4000请求了。那么其中800请求是写入,3200请求是查询,如果说安装目前的情况来扩容,就只需要增加一台数据库服务器即可。但是就会涉及到表的迁移,因为需要迁移一部分表到新的数据库上,那是很麻烦的事情了。其实没有这个必要的,可以使用读写分离来解决这个问题,也就是主从复制。写的时候走主库,读数据时走从库,这样就可以让一个表的读写请求分开落到不同的数据库上去执行。这样的设计后,我们在推算一下,假如写入主库的请求是400/s ,查询从库的请求是就是1800/s,只需要在主库下配置俩台从库即可。这时的架构是如下的。实际的生产环境,读请求的增长速度远远高于写的请求,所以读写分离之后,大部分就是扩容从库支撑更高的读请求就可以了。而且另外一个点,对同一个表,如果既写数据,还读数据,可能会牵扯到锁冲突问题,无论读还是写性能都会有影响。所以一旦读写分离之后,对主库的表就仅仅是写入,没任何查询会影响主库。对从库就仅仅是查询了。五、并发数据库结构总结关于并发场景下,数据库层面的架构是需要进行精心的设计的。并且在配置主复制时,也会有很多的问题来等着去解决。本文就是从一个大的角度来梳理一个思路给大家,可以根据自己公司的业务和项目来考虑自己的系统如何分库分表。分库分表的落地需要借助mycat或者其他数据库中间件来实现。这几天就会再出一篇文章对于本文的一个实现,并且还有很多问题等着去解决。例如:自增ID问题,主从复制数据不一致问题,在主从复制这块很多问题,作为程序员的我们就是需要不停的打boos获得最终的胜利。
本文使用Go来实现字符串逆序这个功能,用最简单的话术让你理解附带在Go中debug的小技巧例如:Hello 转换为 olleH一、实现字符串的逆序在go中,字符串要根据索引获取值是需要转为字节的。接下来我们看一个实现代码代码应该都看的明白,下面咔咔用图解来帮助解释一下package main import ( "fmt" ) func stringReverse() { var str = "Hello" // 字符串转字节 var bytes []byte = []byte(str) for i := 0; i < len(str)/2; i++ { // 定义一个变量存放从后往前的值 tmp := bytes[len(str)-i-1] // 从后往前的值跟从前往后的值调换 bytes[len(str)-i-1] = bytes[i] // 从前往后的值跟从后往前的值进行调换 bytes[i] = tmp } str = string(bytes) fmt.Println(str) } 这段代码可以看到循环的最大次数就是将字符串的长度除了2在这副图中我们可以看到第一次循环时是将第一字符串跟最后一个字符串进行调换第二次循环时将第二个值跟倒数第二值进行调换这就是这块代码的意义所在先将索引最后的字符串的值拿出来接着让最后索引的字符串跟第一个索引字符串相等 也就是上图中第一个步骤 让最后一个值跟等于第一个值然后把第一个索引的字符串改为我们第一步保存的值 同理 让第一个值等于最后一个值在go中还有好几种实现这个过程,这里咔咔在提供一种供大家参考这种方式需要引入包strings,也是官方推荐的一种方式func stringReverse1() { var str = "hello" var bytes []byte = []byte(str) var build strings.Builder for i := 0; i < len(bytes); i++ { i2 := bytes[len(bytes)-i-1] build.WriteString(string(i2)) } s3 := build.String() fmt.Println(s3) }执行俩个代码,检测是否可行二、给你一个小技巧让你在用Go的Debug时游刃有余假设我们想调试一下这几个值的时候,就会发现go会直接报出一个变量没有使用的错误。这种写法在PHP中是不存在报错的,这个错误就会导致go的程序编译无法通过那么我们应该如何模拟已经使用了这个值呢!可以使用一个底杠来解决这个问题这时就可以使用debug来调试了我们想要得值了
Debug对一个程序员是多么的重要,然后在升级了go1.14后,我的debug却使用不了了,这就很尴尬。遇到问题解决问题。这个问题估计在未来大多数人都会遇到,咔咔在学习过程中一直坚持使用最新版本。根据咔咔现在的这个标题去搜索,只有几篇文章解析,但是他们给解决方法我却实现不了,于是咔咔就再出一篇文章,来对这个问题进行解析。给出的最多解决方法就是go get -u github.com/derekparker/delve/cmd/dlv执行这个命令,这个指令不知道什么原因在咔咔这里执行没有结果,也不下载,反正过一会命令就结束了。大大方方的解决这个问题这个问题是因为在1.14版本没有安装这个delve咔咔是在windows操作,使用的编辑器是Goland。咔咔的GOROOT是在C:Go,咔咔给的这个图缺什么目录自己补好就是创建github.com/go-devel这几个文件夹然后在go-devel这个文件夹下面执行git clone https://github.com/derekparker/delve.git下载完后就会出现以下文件,并且都在delve这个文件夹下最后我们需要使用go install来安装我们的包执行指令go install github.com/go-delve/delve/cmd/dlv这里你需要看清楚目录结构执行完指令后就会在GOPATH/bin下面产生一个dlv的可执行程序,如果没有这个文件则安装失败。回过头在看看哪里的操作跟咔咔不一样哈。打开Goland编辑器,根据咔咔给的箭头提示点击,这时会弹出来一个create,点击即可然后输入以下内容dlv.path=C:/Go/bin/dlv.exe,就是刚刚安装的可执行程序此时所有工作就都已经完成了,重启我们的Goland,打断点看一下。go的断点看起来还是很舒服的,非常的nice。到这里这个问题就完美的解决了坚持学习、坚持写博、坚持分享是咔咔从业以来一直所秉持的信念。希望在诺大互联网中咔咔的文章能带给你一丝丝帮助。
2022年05月