前段时间做了很多慢SQL
的优化工作,这周刚好又被反馈服务出现了死锁导致了业务报错。看了一下云数据库的告警日志,发现出现了比较多的事务未提交
、死锁
、等待行锁
的严重警告。都是一些棘手的运维工作,涉及到业务流程的梳理、SQL的优化等工作。
今天趁这个机会,我们一起看下如何去分析这些问题,主要看下等待行锁、死锁。
每次说数据库锁,感觉一大堆。其实如果按照一定的纬度去整理下,还是比较清晰的。如图:
力度划分:表锁、页锁、行锁
算法划分:Record Lock
、Gap Lock
、Next-key Lock
实现机制:乐观锁、悲观锁
兼容性:排它锁、共享锁、意向锁
每次说锁,其实也要跟它的隔离级别挂钩才行,其实都是为了去实现某一个功能才产生的。所以不可以一概而论,总之记住几个大的背景:
RR
才会出现Gap Lock
,因为要避免幻读的问题,所有要把它相邻的数据也要锁住。1 | SET GLOBAL innodb_status_output=ON; -- 开启输出 |
通过SHOW ENGINE INNODB STATUS
可以看到锁的一些信息
先确定一下自己数据库的隔离级别信息,我现在数据库的版本是8.0.26
。
1 | 如果是比较老的数据库 |
查看锁的情况
1 | # 老版本 |
这个问题也会比较常见,如果出现一个事务获取了锁,如果它不释放或者提交的话,那么后面的人就一直获取不到锁。如果获取锁的时间过长的话,后面的流程就会一直卡住。
1 | -- 建表 |
事务1:
1 | mysql> BEGIN; |
这个时候不要提交事务,看一下现在加锁的情况。
事务2:
1 | mysql> update id_test_rr set name = 'x' where id = 5; |
当事务1提交的时候,事务2马上就获取到了锁。
如何设置超时时间:
1 | show variables like 'innodb_lock_wait_timeout'; |
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
其实,产生死锁的条件无非就是这4个条件,其实大学里学习操作系统的时候就有学习到过。解决死锁,也只需要让它们只要有一个条件不满足就可以了。
建表语句
1 | CREATE TABLE t (i INT) ENGINE = InnoDB; |
事务1:
1 | mysql> BEGIN; |
事务2:
1 | mysql> BEGIN; |
然后事务1继续进行删除操作
1 | mysql> DELETE FROM t WHERE i = 1; |
事务2报错了:Deadlock found when trying to get lock;
死锁产生了!因为事务1需要锁X来删除行,而事务2拿着锁X并正在等待事务1释放锁S。看看2个事务的状态:
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e
,他会拉你们进群。
我们来先看一个图,了解一下故(事)事(故)的背景:
有2
个跑批任务,其实做的事情是同一件事情,都是为了跟下游系统保持数据的一致性。大任务是每个2h
跑一次,小任务是每隔10mins
跑一次。除了这2
个定时任务以外,还有一个额外的监控任务来做类似的对账,如果发现出现对账不平,就会出现邮件/短信告警到相关的责任上。
这是一个非常有特点的定时任务跑批任务+监控告警的场景了。
从上面的场景上看,我们可以得出一些结论:为了保证一致性写了大小Job来保证,并且还给出了监控告警,说明数据的重要性是比较强的。
某天,出现了频繁的告警提示,每10
分钟就告警一次,而且内容没有发生变化,说明同步的index
没有变化过。
第一反应肯定是在思考,我的大任务与小任务都有正常执行吗?因为之前的都是正常的。上午看了一下日志与进程发现有在跑,还跑除了多次任务,日志打印不明确,看不到具体分支的逻辑。总结一下问题点:
先修复上面2
个问题,短时间对接一个新的分布式调度时间上不可能,只能简单的改shell
脚本让其不执行。
1 | d=`date` |
然后在重点的地方添加上日志,其实这些操作都是一些非常简单,但是可以带来明显效果的步骤。反正,我基本都是如此的去做的,你什么信息都拿不到,你根本无法入手。
部署上去后,发现每次在insert into
一条数据的时候,日志就卡住了,结合代码确定,确定就是insert into
的时候,数据库没有返回,而其他的表以及其他数据的都是可以正常操作的。
第一反应,就是看下这条SQL
现在是一个什么状态?我们可以利用SHOW PROCESSLIST
看下
1 | select * from information_schema.PROCESSLIST t; |
然后就发现该SQL
语句的trx_state=LOCK WAIT
,那说明没有获取到锁。那我们具体如何推断是谁没有释放锁了?
1 | 第1步: |
可惜,那条语句已经是sleep
的状态了,无法看到具体的SQL
。在这里可以推断,就是有一条SQL
在对数据{local_data}
操作的时候获取了一把锁,但是因为事务未提交,导致后面的SQL
再对{local_data}
操作的时候要获取锁,无法获取到。理论上获取不到锁,一会儿也会释放掉报错出来。通过查询innodb_lock_wait_timeout=7200
,默认值应该是50
。
到这一步就很明确了,就是让未提交事务的SQL
结束掉,或者提交掉。此时只有kill
掉这个进程的选项了。执行:
1 | kill {lock_trx_id}; |
再执行就立马发现数据没有了,获取到了锁。
1 | SELECT * FROM information_schema.innodb_trx t where t.trx_state = 'LOCK WAIT'; |
innodb_lock_wait_timeout
设置不合理,时间太久了就直接看脚本好了
当右边的事务对同一条数据进行X操作的时候,它是要获取锁的。
1 | ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction |
这个时候可以去看下锁的表
1 | select * from information_schema.innodb_locks; |
查看一下设置的超时时间
1 | show variables like 'innodb_lock_wait_timeout'; |
看关于事务的描述
1 | show engine innodb status |
查看当前的事务
1 | mysql> show processlist; |
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e
,他会拉你们进群。
现在对于一个开发来说,Docker
应该是再熟悉不过了。还记得在20132014左右的时候,听说多最多的就是 Cloud Foundry
,那个时候就一直在说云的事情。后面Docker
就绝杀了它
那它帮我们解决了一个什么问题了?面试的时候也许会问到。
在很久以前,我们开发代码,估计最蛋疼的事情就是发布版本了。我还记得在房多多的时候(2014~2016)左右,每次发布几个开发围绕在运维的身边,有时候运维忙不过来,开发就直接在运维的电脑上开始VIM
干活了,修改若干配置。由于多环境的原因,我们无法保证每个环境都是一样的。
那么Docker
就可以把我们的操作系统、代码、脚本等都一起打包成一个Image
,就可以保证只要是运行同一个Image
,我们的所有内容都是一样的。就不会出现,我在测试环境跑的好好的,一到生产连启动都成问题。
现在一般一个POD
就只跑一个进程,DevOps
会根据我们的发布流水线自动的将一个项目进行打包、发布,整套的CI
、CD做的是行云流水。但是,每个项目ROOT
下都会需要一个叫Dockerfile
的文件。但偏偏有一些历史项目,没有Dockerfile
文件,只有一个Run
的容器再跑,真的是非常惊悚。docker rm [OPTIONS] CONTAINER [CONTAINER...]
,就GAME OVER
了。
真的,什么也不想。先保个底,把你当前的容器打包成一个镜像推送到仓库里去,哪天有以外或者说需要基于它做一些事情的时候才有可能。比如:你要本地也部署一份代码来debug
。
一般都是私有的仓库,会需要输入用户名与密码
1 | ➜ ~ docker login {仓库地址} |
然后,将镜像打包推送到私有仓库去
1 | docker commit -a "name" -m "小陈来拯救你" 706e502e8693 {镜像地址}:{tag} |
但是这样子的问题在于,我们无法知道环境依赖了哪些模块,如果需要重新再部署一套,我为了保证环境的干净又需要删除哪些东西。就是无法知道增加与减少哪些东西,也就会导致环境存在不一致性,失去了我们的初衷。
先把镜像跑起来,然后从运行起来的容器中复制文件出来,复制命令如下:
1 | 从容器复制文件或目录到宿主机器 |
第一种方法并不是万能的,因为有些镜像过于简单,少了许多基础命令,以至于无法复制文件,也无法进入shell
环境。其次,要运行起来再操作,也有点占用资源,比较麻烦。
此方法就是相当于反编译,拿到当时打镜像时候你做的详细操作。比较麻烦,但是是最靠谱的,最具有操作性的。
先将镜像保存为tar文件,命令如下:
1 | docker save -o {name}.tar {镜像地址}:{tag} |
下载后就会有一个tar包在本地,然后就解压出来。可以看一下manifest.json
文件的内容
1 | [ |
图片是解压后的效果,里面都会存在一个layer.tar
,这里再解压就是当时打镜像时候的一些资源文件。
红色的部分就是我们想要的内容。再辛苦一点,把自己想要的东西整理出来。描述的比较轻描淡写,任何事情只要手动去做一遍,就会理解与记住。
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e
,他会拉你们进群。
之前在思考双活/多活架构的时候,其实对于蓝绿发布是有一些了解的,也梳理过在底层存储是一份,服务是多份的模式有做过深入的分析。但那个时候对于Kubernetes
的了解还不是很熟悉,是通过传统的方式来考量的。
因为现在的互联网公司基本都是上云了,我们也必须对于Kubernetes
那一整套要有比较深入、熟悉的运用才能真的提高我们的效率。先聊一下,我为什么需要利用灰度+蓝绿发布的模式来去做?
现在有一个比较老的项目,应该在10年+,每天请求量大概在1.5亿+,峰值的QPS
在6000/s,存在着比较多性能问题。现在需要在它上面新增一个服务,为了后面优化做准备,比如:请求的分流、限流、熔断、日志的上报与监控(新)、统一编译处理,特殊报文转换等。也就是说,只要你新增加了一层,你才有可能更好的去做更多的事情。
那么我们需要达到一些什么的基础条件了?
如果是你?针对于这2个基础的要求,你会如何去思考的你架构方案呢?
新增服务的思考:
I/O
模型、线程模型、数据结构、算法等息息相关。比如:你在思考Redis
单线程为什么快的时候?应该就很能get到这里的点了。解决这个问题,我们选择了Go
语言来开发(当然,最熟悉的语言风险最小),为了保证性能,也是做了2轮非常细致的压测。502
。如果说我在发布的过程中,我把滚动这一步省略掉,直接先准备好一份最新的,验证可以后,我一刀直接把流量引导最新服务上,老的服务也不会断掉,这是否就可以达到效果了?下面是我画的一个架构图,方便大家的理解,一共是3条路线:
Ingress
→ Service:server-read
→ StatefulSet:server-g3-read + server-g3-read-gray
,整条链路是通过ingress
的指向与selector
的标签:k8s-app:server-read
。就是我只能让一少部分的流量进入到新的服务(2%~10%,支持慢慢调整,其实就是pod的数量占比)。
2%
的概率走的路径:Ingress
→ Service:server-gateway-read01
→ StatefulSet:server-gateway-read01
→ 注册中心获取负载地址
→ Service:server-read
→ StatefulSet:server-g3-read + server-g3-read-gray
,整条链路是通过ingress
的指向与selector
的标签:server-app:server-gateway-read01
98%
的概率还是走的路线1Ingress
→ Service:server-gateway-read01
→ StatefulSet:server-gateway-read01
→ 注册中心获取负载地址
→ Service:server-read
→ StatefulSet:server-g3-read + server-g3-read-gray
。但是它的selector
的标签:server-app:server-gateway-read02
后面如果长期稳定后,方案2其实就没有必要再继续灰度了,直接就替换成线路3了。相当于是一个蓝绿+主备的模式了。优缺点非常的明显:
Graceful Shutdown
优化。再温馨提示一下,因为做了蓝绿发布,我们的系统对应的配置中心应该也最好是要分开的。系统之间要避免蓝色通过与绿色通道之间的交叉访问等问题。
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e
,他会拉你们进群。
SLA
(需要达到多少个9)、QPS
、TPS
等。因为这些量化的数字让我们更加的了解我们的系统。我们如何压测?其实个人觉得有2种场景。
第一种:是我们明确的知道目标,看我们通过大量的并发看我们是否有达到。如果没有达到,我们需要通过水平扩容、性能优化等让其达到。
第二种:是我们不知道目标,通过压测可以知道一个固定配置下的单机单服务的最大性能,让我们对它有一个彻底的认识。为后面的目标做更多的铺垫与准备,或者跟行业水平对比,看看差距有多少。
Github地址:https://github.com/wg/wrk,该项目也是开源项目,关注的人还不少,有30.4K。咨询了一下身边的同事,使用它的人还不少。主要的语言的是C语言。
1 | git clone https://github.com/wg/wrk |
压测脚本press.sh
1 | !/bin/bash |
Lua脚本post_512.lua
1 | wrk.method = "POST" |
这里是一个通用脚本,大致的含义是:
总结一下,这些时间需要根据自己的服务器性能去调整,有可能压测出来的数据就是空的,因为超时了未返回Response。
1 | 使用方法: wrk <选项> <被测HTTP服务的URL> |
1 | start length 512k |
会给一个分布非常的好:50%、75%、90%、99%。
但是如果说这么看大量的数据不够直观,这里再提供一个一个python脚本来解析里面的值。使其能把这些日志的重要的信息提取出来:
1 | import math |
最终你可以再在文本里面利用列操作的方式,将内容归整到Excel中去。你就可以很好的汇报与分享给他人了~
Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试,它最初被设计用于Web应用测试,但后来扩展到其他测试领域。 它可以用于测试静态和动态资源,例如静态文件、Java 小服务程序、CGI 脚本、Java 对象、数据库、FTP 服务器, 等等。JMeter 可以用于对服务器、网络或对象模拟巨大的负载,来自不同压力类别下测试它们的强度和分析整体性能。另外,JMeter能够对应用程序做功能/回归测试,通过创建带有断言的脚本来验证你的程序返回了你期望的结果。为了最大限度的灵活性,JMeter允许使用正则表达式创建断言。
Apache jmeter 可以用于对静态的和动态的资源(文件,Servlet,Perl脚本,java 对象,数据库和查询,FTP服务器等等)的性能进行测试。它可以用于对服务器、网络或对象模拟繁重的负载来测试它们的强度或分析不同压力类型下的整体性能。你可以使用它做性能的图形分析或在大并发负载测试你的服务器/脚本/对象。
Jmeter也是在进行压测中使用场景很多的软件,图形界面操作起来非常的友好。简单的写一个Demo流程出来。
官网:http://jmeter.apache.org/download_jmeter.cgi
下载解压完毕后大概的一个目录结构,可以把bin配置到path中就可以直接通过jmeter密令激活软件了。
1 | ➜ jmeter pwd |
先看一张整体的图吧,对它有一个比较整体的认识:
我们可以按照https://www.cnblogs.com/stulzq/p/8971531.html 的步骤一步的去做。我这里就不做太多重复的介绍。因为相对比wrk来的简单。
最后可以点击运行来跑单测,一般我们会调整线程数的大小、发送的频率来进行压测看结果。
我们在断言的地方是可以做很多的事情的,因为什么样的结果是正确的,什么样的结果是失败的。获取需要从Response中、Header里截取一些关键的key与value来做逻辑。这些可以通过编写脚本来去做到,那就算相对高阶一点的操作了。后续可以继续深入一下~
最后可以在汇总报告里可以看出来我们的一个性能情况,SLA的比例等。
作为一名后端开发,对自己写出来的服务进行一个非常全面的性能压测是很有必要的。对于系统的一个QPS、TPS、SLA这些数字应该随口就能说出来。哪些地方存在性能瓶颈?然后再去找相应的方案去优化掉。很多时候,性能可能就会是一个最大的风险,它会导致我们的服务整体的瘫痪、不可用。这些很有可能就跟我们的KPI、奖金挂钩~
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e
,他会拉你们进群。
项目中开始用Go
,最近写了一下Demo
,发现语法还是非常好用,大部分比Java
还是简洁很多,也有一些很细节的约定。比如:
更好的就是方法可以返回多个值,这个跟Java
比较就是减少很多的封装。因为Go
的线程模型特点,用来写一些需要高并发、高性能的项目还是非常好的。所以,趁这个机会也好好的深入了解下。现在也是把Python
、PHP
、Go
等都学习一遍,每种语言都有它的优缺点,其实都还挺不错的。
针对于Go
语言里:&
与*
的区别,什么时候该用什么做一个总结。
我们经常会听到别人说Go
是值传递,某某某是引用传递,某某某是指针传递,等等各种各样的说法。
那么首先他们的区别是什么呢?什么是指针?指针其实也是一个变量,只不过这个变量里面存的不是int
,float
,struct
,而是一个地址address
,然后在这个address
上所存储的数据可以通过指针来被阅读到。
OK
,指针变量存储的是一个地址,地址从哪里来的?那就得问一个变量的地址怎么取得呢?在变量前面加上一个&符号就行。
好的,指针变量存储了这个地址了,那这个地址所存储的值怎么被阅读到呢?也就是指针所指向的值怎么拿到呢?在指针变量前面加上一个*
符号就行。
怎么修改指针所指向的数据呢?在前面加上*
符号之后再赋一个新的值就可以了。
我们来看一个栗子:
1 | package main |
执行输出的结果是
1 | a的值 vernonchen |
为什么要有指针这个东西?它有什么关键性的作用呢?
我们来看下面这段代码:
1 | package main |
执行完后,输出的结果:
1 | userName = vernonchen |
所以指针的作用:
指针不但可以帮助函数内部修改外部变量的值,还可以帮助我们在任何地方修改其所指向数据的值;
传递指针参数可以节省拷贝大结构体的内存开销;
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e
,他会拉你们进群。
如果说你的数据量并发量不大,或者你的数据量很少没有到千万级别,也许pt-osc
、gh-osc
,online-ddl这些工具都用不着。但是,如果你的数据量很大,数据又很热。如果你没有这些工具,你可能无法完成对一个数据库新增一个字段或者任何一个简单的DDL
语句。
简单的分析一下,为了保证数据一致性问题,我们在哪儿都会遇到锁的问题,锁是用来保证顺序性的。谁先拥有锁,谁就可以先执行。锁也会存在力度问题,它跟你要做的一件事情息息相关,我们也会在性能上去做取舍,所有就好了行锁、表锁等。
说一下我遇到的这个场景,数据量数据大概在800W左右,但是表非常的热,长事务也很多。当我要对一个表新增字段的时候,这个时候如果你经验不够足,可能就会“量成大祸”。一般在做DDL
会出现:Waiting for table metadata lock。
如果长时间获取不到锁的话,就出现一个可怕的情况:
DDL
语句DDL
语句未执行之前,后面的请求全部是被hold住的这样子就会导致一前一后同时夹击,导致整个业务不可用。那么出现Waiting for table metadata lock可能是由哪些原因导致的?
通过show processlist
可以看到TableA
上有正在进行的操作(包括读),此时alter table语句无法获取到metadata
独占锁,会进行等待。
这是最基本的一种情形,这个和mysql 5.6
中的online ddl
并不冲突。一般alter table
的操作过程中,在after create
步骤会获取metadata
独占锁,当进行到altering table
的过程时(通常是最花时间的步骤),对该表的读写都可以正常进行,这就是online ddl
的表现,并不会像之前在整个alter table
过程中阻塞写入。(当然,也并不是所有类型的alter操作都能online的,具体可以参见官方手册:http://dev.mysql.com/doc/refman/5.6/en/innodb-create-index-overview.html)
处理方法: kill 掉 DDL所在的session.
1 | # 找出所有执行时间超过 5 分钟的线程,拼凑出 kill 语句,方便后面查杀 |
通过show processlist
看不到TableA
上有任何操作,但实际上存在有未提交的事务,可以在 information_schema.innodb_trx中查看到。在事务没有完成之前,TableA上的锁不会释放,alter table
同样获取不到metadata
的独占锁。
处理方法:通过 select * from information_schema.innodb_trx\G
, 找到未提交事物的sid
, 然后 kill
掉,让其回滚。
通过show processlist
看不到TableA
上有任何操作,在information_schema.innodb_trx
中也没有任何进行中的事务。这很可能是因为在一个显式的事务中,对TableA
进行了一个失败的操作(比如查询了一个不存在的字段),这时事务没有开始,但是失败语句获取到的锁依然有效,没有释放。从performance_schema.events_statements_current
表中可以查到失败的语句。
官方手册上对此的说明如下:
If the server acquires metadata locks for a statement that is syntactically valid but fails during execution, it does not release the locks early. Lock release is still deferred to the end of the transaction because the failed statement is written to the binary log and the locks protect log consistency.
也就是说除了语法错误,其他错误语句获取到的锁在这个事务提交或回滚之前,仍然不会释放掉。because the failed statement is written to the binary log and the locks protect log consistency 但是解释这一行为的原因很难理解,因为错误的语句根本不会被记录到二进制日志。
处理方法:通过performance_schema.events_statements_current找到其sid, kill 掉该session. 也可以 kill
掉DDL
所在的session
.
上述是手动操作,毕竟是一个比手速的过程,而且也不能保证保证100%,还需要经常的kill进程才行。
请看参考地址,对于一些online ddl工具研究后,对gt-osc做了一个初步的使用。
1 | pt-online-schema-change --host=xxx -uxxx -pxxx \ |
执行后其实它做了这些工作,具体如下:
db
.table_name
TO db
._table_name_old
, db
._table_name_new
TO db
.table_name
可以去看一下网站的说明:https://www.percona.com/blog/2019/06/07/how-pt-online-schema-change-handles-foreign-keys/
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e
,他会拉你们进群。
大家在做微服务拆分后,难免会导致Application
项目以及一些二房包的数量加剧,10+
个项目我想应该是很容易的超过。然后这些细粒度的拆分后就会导致发布版本时候的麻烦。
展示一下现阶段我们的一个git
的分支流程图,仅供参考。
简单说明一下:
dev
环境每次都是从master
拉取一个分支,取名为dev-{发布日期}-{sequenceId}
,sequenceId
从01
开始叠加,避免一个版本需要反复拉取多次。test
阶段,也是为了收敛(前期严格一点)。提测的时候代码需要合并到test
分支来。UAT
阶段,还是需要从master
拉取分支,如果出现要重新拉取分支的情况下,还是严格拉取master
分支的代码。主要是为了与master
保持一致,避免把别人的覆盖掉。取名为release-{发布日期}-{sequenceId}
,规则同上。master
分支,并且打包tag
。但是在这个过程中,需要有拉取新分支,合并分支,批量删除分支,打tag
等等繁琐的操作,项目一多如果有一个批量脚本就更好了。下面我就列举一些平时使用最多的几个,仅供参考:
比如一个新的迭代要开始了,就需要从master
拉取dev
分支。
1 | ! /bin/bash |
具体使用,比如以master
为基础拉取一个新分支
1 | ./git_batch_co_push_remote.sh master dev-20210419-01 |
做不同的需求时候,需要来回的切换分支。
1 | ! /bin/bash |
具体的使用,先切换成master
分支。
1 | ./git_batch_co_brach.sh dev-20210412-01 |
有时候不小心写错了分支名字,或者分支太多,需要删除批量删除远程分支
1 | ! /bin/bash |
具体的使用
1 | ./git_batch_delete_remote.sh dev-20210412-01 |
结果就是可以把远程分支dev-20210412-01
一次性全部删除掉。
从一个环境到另外也跟环境,就需要大量的merge
操作。其实Git
的merge
操作比SVN的要好很多。
1 | ! /bin/bash |
具体使用,记得要先更新相关的代码哦~
比如是从dev
合并到test
,那么先更新对应的代码,然后co
到test的目录去。
1 | ./git_batch_co_merge_push_remote.sh dev-20210412-01 |
比如我们的流程是在上生产后为master打一个新的tag
1 | ! /bin/bash |
具体使用,首先切换为master
分支。
1 | ./git_batch_tag_push.sh v1.0.0 |
1 | ! /bin/bash |
具体使用
1 | ./git_batch_deploy_snapshot.sh |
上面的这些脚本也是因为当时自己要发布版本,总共写脚本花费了20~30mins
的时候,发布时间也才30mins
。如果那天我不写这个脚本,业务我每次都要花费60+mins
去做,而且其他不熟悉的人做发布的时候肯定会花费更多的时间。开发就应该把那些重复的事情标准化与产品化。能够真的做到可持续~
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e
,他会拉你们进群。
(Knapsack Problem, KP)
是NP
完全问题,也是一类重要 的组合优化问题 ,在工业 、经济 、通信、金融与计算机 等领域的资 源分配 、 资金预算 、 投资决策 、 装载问题 、 整数规划 、 分布式系统 与密码系统中具有重要的理论和应用价值。通俗说法
贼,夜入豪宅,可偷之物甚多,而负重能力有限,偷哪些才更加不枉此行?
抽象说法
给定一组多个())物品,每种物品都有自己的重量())和价值()),在限定的总重量/总容量()内,选择其中若干个(也即每种物品可以选0个或1个),设计选择方案使得物品的总价值最高。
更加抽象的说法
给定正整数)、给定正整数,求解0-1规划问题:
, s.t. , 。
定义子问题 为:在前 个物品中挑选总重量不超过 的物品,每种物品至多只能挑选1个,使得总价值最大;这时的最优值记作 ,其中 , 。
考虑第 个物品,无外乎两种可能:选,或者不选。
最优方案就是比较这两种方案,哪个会更好些:
。
得到
。
时才会有“取第 件物品”发生。
所以从表格右下角“往回看”如果是“垂直下降”就是发生了 ,而只有“走斜线”才是“取了”物品。
这个算法的复杂度就很容易算了——每一个格子都要填写数字,所以时间复杂度和空间复杂度都是 。当” “时(就不严谨地使用渐近分析的语言了),复杂度是 。
1 | package com.cyblogs.algorithm; |
最后输出的结果是:26。
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e
,他会拉你们进群。
相信很多程序员对于 Linux
系统都不陌生,即使自己的日常开发机器不是 Linux
,那么线上服务器也大部分都是的,所以,掌握常用的 Linux
命令也是程序员必备的技能。
但是,怕就怕很多人对于部分命令只是一知半解,使用不当就能导致线上故障。
前段时间,我们的线上应用报警,频繁 FGC
,需要紧急处理问题,于是有同事去线上重启机器(正常程序应该是先采集堆 dump
,然后再重启,方便排查是否存在内存泄露等问题)。
但是在重启过程中,同事发现正常的重启命令应用无反应,然后尝试使用 kill
命令 “杀” 掉 Java
进程,但是仍然无效。于是他私自决定使用 “kill -9
“ 结束了进程的生命。
虽然应用进程被干掉了,但是随之而来带来了很多问题,首先是上游系统突然发生大量报警,对应开发找过来说调用我们的 RPC
服务无响应,频繁超时。
后来,我们又发现系统中存在部分脏数据,有些在同一个事务中需要完整更新的数据,只更新了一半…
为什么正常的 kill
无法 “杀掉” 进程,而 kill -9
就可以?为什么 kill -9
会引发这一连串连锁反应?正常的 kill
执行时,JVM 会如何处理的呢?
要搞清楚这些问题,我们要先从 kill
命令说起。
我们都知道,想要在 Linux
中终止一个进程有两种方式,如果是前台进程可以使用 Ctrl+C
键进行终止;如果是后台进程,那么需要使用 kill
命令来终止。(其实 Ctrl+C
也是 kill
命令)
kill
命令的格式是:
1 | kill [参数] [进程号] |
其中 [参数] 是可选的,进程号可以通过 jps/ps/pidof/pstree/top
等工具获取。
kill
的命令参数有以下几种:
1 | -l 信号,若果不加信号的编号参数,则使用“-l”参数会列出全部的信号名称 |
通常情况下,我们使用的 - l
(信号) 的时候比较多,如我们前文提到的 kill -9
中的 9
就是信号。
信号如果没有指定的话,默认会发出终止信号 (15
)。常用的信号如下:
1 | HUP 1 终端断线 |
比较常用的就是强制终止信号:9
和终止信号:15
,另外,中断信号:2
其实就是我们前文提到的 Ctrl + C
结束前台进程。
那么,kill -9
和 kill -15
到底有什么区别呢?该如何选择呢?
kill
命令默认的信号就是 15
,首先来说一下这个默认的 kill -15
信号。
当使用 kill -15
时,系统会发送一个 SIGTERM
的信号给对应的程序。当程序接收到该信号后,具体要如何处理是自己可以决定的。
这时候,应用程序可以选择:
因为 kill -15
信号只是通知对应的进程要进行 “安全、干净的退出”,程序接到信号之后,退出前一般会进行一些 “准备工作”,如资源释放、临时文件清理等等,如果准备工作做完了,再进行程序的终止。
但是,如果在 “准备工作” 进行过程中,遇到阻塞或者其他问题导致无法成功,那么应用程序可以选择忽略该终止信号。
这也就是为什么我们有的时候使用 kill
命令是没办法 “杀死” 应用的原因,因为默认的 kill
信号是 SIGTERM(15)
,而 SIGTERM(15)
的信号是可以被阻塞和忽略的。
和 kill -15
相比,kill -9
就相对强硬一点,系统会发出 SIGKILL 信号,他要求接收到该信号的程序应该立即结束运行,不能被阻塞或者忽略。
所以,相比于 kill -15
命令,kill -9
在执行时,应用程序是没有时间进行 “准备工作” 的,所以这通常会带来一些副作用,数据丢失或者终端无法恢复到正常状态等。
我们都知道,在 Linux
中,Java
应用是作为一个独立进程运行的,Java
程序的终止运行是基于 JVM
的关闭实现的,JVM 关闭方式分为 3 种:
System.exit
或者通过其他特定平台的方法关闭(接收到 SIGINT(2)
、SIGTERM
(15)信号等)Runtime.halt
方法或者是在操作系统中强制 kill
(接收到 SIGKILL(9)
信号)RuntimeException
异常等JVM
进程在接收到 kill -15
信号通知的时候,是可以做一些清理动作的,比如删除临时文件等。
当然,开发者也是可以自定义做一些额外的事情的,比如让 tomcat
容器停止,让 dubbo
服务下线等。
而这种自定义 JVM
清理动作的方式,是通过 JDK
中提供的 shutdown hook
实现的。JDK
提供了 Java.Runtime.addShutdownHook(Thread hook)
方法,可以注册一个 JVM 关闭的钩子。
1 | package com.cyblogs.thread; |
执行命令:
控制台输出内容:
1 | hook execute... |
可以看到,当我们使用 kill
(默认 kill -15
)关闭进程的时候,程序会先执行我注册的 shutdownHook
,然后再退出,并且会给出一个提示:interrupted by signal 15: SIGTERM
如果我们执行命令 kill -9
:
控制台输出内容:
1 | Process finished with exit code 137 (interrupted by signal 9: SIGKILL) |
可以看到,当我们使用 kill -9
强制关闭进程的时候,程序并没有执行 shutdownHook
,而是直接退出了,并且会给出一个提示:interrupted by signal 9: SIGKILL
kill
命令用于终止 Linux
进程,默认情况下,如果不指定信号,kill
等价于 kill -15
。
kill -15
执行时,系统向对应的程序发送 SIGTERM(15)
信号,该信号是可以被执行、阻塞和忽略的,所以应用程序接收到信号后,可以做一些准备工作,再进行程序终止。
有的时候,kill -15
无法终止程序,因为他可能被忽略,这时候可以使用 kill -9
,系统会发出 SIGKILL(9)
信号,该信号不允许忽略和阻塞,所以应用程序会立即终止。
这也会带来很多副作用,如数据丢失等,所以,在非必要时,不要使用 kill -9
命令,尤其是那些 web
应用、提供 RPC
服务、执行定时任务、包含长事务等应用中,因为 kill -9
没给 spring
容器、tomcat
服务器、dubbo
服务、流程引擎、状态机等足够的时间进行收尾。
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e
,他会拉你们进群。
Consumer Group
是Kafka
提供的可扩展且具有容错性的消费者机制。在组内多个消费者实例(Consumer Instance
),它们共享一个公共的ID即 Group ID
。组内的所有消费者协调在一起消费订阅主题(Subscribed Topics
)的所有分区(Partition
)。当然一个分区只能有同一个消费者组的一个Consumer
实例消费。Consumer Group
有三个特性:
Consumer Group
下可以有一个或多个Consumer
实例。 这里的实例可以是一个单独的进程,也可以是同一进程下的线程;Group ID
是一个字符串, 在Kafka集群中唯一标识,Consumer Group
;Consumer Group
Consumer
实例消费。同一个分区消息可能被多个Group
消费。传统的消息系统中,有两种消息引擎模型:点对点模型(消息队列)、发布/订阅模型
传统的两种消息系统各有优势,我们里对比一下:
Consumer
消费。严格的说这不是它的缺陷,Scalability
)很差,因为下游的多个Consumer
都要“抢”Consumer
消费,但它的问题也是伸缩性不高,因为订阅者都必须订阅所有主体的所有分区。Kafka
为规避传统消息两种模型的缺点,引入了 Consumer Group
机制:
Consumer Group
订阅多个主题后,组内的每个实例不要求一定要订阅主题的所有分区,它只会消费部分分区中的消息;Consumer Group
之间彼此队里,互不影响,它们可以订阅同一组主题而互不干涉。加上Broker
端的消息留存机制,Kafka
的Consumer Group
完美的避开了伸缩性差的问题;kafka
是用Consumer Group
机制,实现了,传统两大消息引擎。如果所有实例属于同一个Group
,那么它实现的就是消息队列模型;如果所有实例分别属于不同的Group
,且订阅了相同的主题,那么它就实现了发布/订阅模型;最理想的情况是Consumer
实例的数量应该等于该Group
订阅主题的分区总数。例如:Consumer Group
订阅了 3个主题,分别是A、B、C
,它们的分区数依次是1、2、3
,那么通常情况下,为该Group
设置6
个Consumer
实例是比较理想的情形。
如果设置小于或大于6
的实例可以吗?当然可以,如果你有3
个实例,那么平均下来每个实例大约消费2
个分区(6/3=2)
;如果你设置了9
个实例,那么很遗憾,有3
个实例(9-6=3
)将不会被分配任何分区,它们永远处于空闲状态。
消费者在消费的过程中要记录自己消费了多少数据,即消费位置信息,在Kafka
中叫:位移(offset
)。
看上去该Offset就是一个数字而已,其实对于Consumer Group 而言,它是一组KV对,Key是分区,V对应Consumer 消费该分区的最新位移。
老版本的Consumer Group
把位移保存在Zookeeper
中。将位移保存在Zookeeper
外部系统显然好处是减少了Kafka Broker
端的状态保存开销。现在比较流行的提法是将服务器节点做成无状态的, 这样可以自由扩缩容,实现超强的伸缩性。不过在实际使用场景中,发现ZooKeeper
这类元框架并不是适合进行频繁的写更新,而Consumer Group
的位移更新却是一个非常频繁的操作。 这种大吞吐量的写操作极大的拖慢了ZooKeeper
集群的性能,在新版本的Consumer Group
中,Kafka
社区采用了将Consumer Group
位移保存在Broker
端的内部主题中。
Rebalance
本质上是一种协议,规定了一个Consumer Group
下所有Consumer
如何达成一致,来分配订阅Topic
的每个分区。比如:某个Group
下有20个Consumer
实例, 它订阅了一个具有100个分区的Topic
。正常情况下,Kafka 平均会为每个Consumer
分配5个分区。这个分配的过程叫Rebalance
。Consumer Group
触发 Rebalance
有三种情况:
Consumer
实例加入组或离开组,抑或是有Consumer
实例崩溃被“踢出”组。Consumer Group
可以使用正则表达式订阅主题,比如consumer.subscribe(Pattern.complile(“t.*c”))
就表明该Group
订阅所有t
开头,字母c
结尾的主题。在Consumer Group
运行时,新创建一个满足这样条件的主题,那么会触发订阅该主题所有Group
开始Rebalance
。Kakfka
一个主题,当分区数增加时,就会触发订阅该主题的所有Group
开启Rebalance
。Consumer Group
发生Rebalance
的过程:某个 Consumer Group
下有两个Consumer
,比如A
和B
,当第三个成员C
加入时,Kafka
会触发Rebalance
,并根据默认的分配策重新分配A、B、C
分配分区,如下图:
注意:目前Rebalance
的设计是所有Consumer
实例共同参与,全部重新分配所有分区,Rebalance
过程所有Consumer
实例都会停止消费,等待Rebalance
完成。Rebalance
很慢,一个Group
内有几百个Consumer
实例,成功进行一次Rebalance
需要好几个小时。 目前社区没有终极解决方案,最好的解决方案是规避Rebalane
的发生。
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e
,他会拉你们进群。
在讲重排序之前,先来看一个例子:
1 | package com.cyblogs.thread; |
上面一段代码是非常经典来讲CPU对指令重排序的案例。因为我们经过一段时间的Run出的结果很惊讶:
1 | [a=0,b=0, a=1,b=0, a=0,b=1, a=1,b=1] |
对于a=1,b=1
的出现,是会让人非常的奇怪的。出现这个情况,那代码执行的顺序可能是:
1 | Thread one = new Thread(() -> { |
如果是这样子的话,那我们还敢写多线程的代码吗?如果没有一定的规范与约定,那肯定是没人可以写好代码。
其实这些约定都是在JSR-133内存模型与线程规范
里面,它就像是Java的产品需求文档或者说明书。
百度云盘:链接: https://pan.baidu.com/s/1cO5d95Za8lyz8dMaN0i9lA 密码: l08w ,大家可以去下载查阅,这些都比较底层,并不能几句话,几篇文章可以讲清楚。
看完上面,你可能会有疑问,为什么会有重排序呢?
我的程序按照我自己的逻辑写下来好好的没啥问题, Java 虚拟机为什么动我的程序逻辑?
你想想 CPU
、内存这些都是非常宝贵的资源, Java 虚拟机如果在重排序之后没啥效果,肯定也不会做这种费力不讨好的事情。
Java 虚拟机之所以要进行重排序就是为了提高程序的性能。你写的程序,简简单单一行代码,到底层可能需要使用不同的硬件,比如一个指令需要同时使用 CPU
和打印机设备,但是此时 CPU 的任务完成了,打印机的任务还没完成,这个时候怎么办呢? 不让 CPU 执行接下来的指令吗? CPU 的时间那么宝贵,你不让它工作,确定不是在浪费它的生命?
重排序:重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序,如下图所示:
这些重排序可能会导致多线程程序出现内存可见性问题。在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial
语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。
回到文章刚开始举的那个例子,重排序提高了 CPU 的利用率没错,提高了程序性能没错,但是我的程序得到的结果可能是错误的啊,这是不是就有点儿得不偿失了?
因为重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致
凡是问题,都有办法解决,要是没有,那就再想想。
它是怎么解决的呢? 这就需要来说说,顺序一致性内存模型和 JMM (Java Memory Model , Java 内存模型)
我们知道Java线程的所有操作都是在工作区进行的,那么工作区和主存之间的变量是怎么进行交互的呢,可以用下面的图来表示。
Java通过几种原子操作完成工作区内存和主存的交互
read
操作传过来的变量值储存到工作区内存的变量副本中。store
操作传过来的值赋值给主存变量。as-if-serial
语义的意思是:不管怎么重排序,(单线程)程序的执行结果不能被改变。编译器、runtime
和处理器都必须遵守as-if-serial
语义。
为了遵守as-if-serial
语义,编译器和处理器不会对存在数据依赖关系的操作做重排序。as-if-serial
语义把单线程程序保护了起来,as-if-serial
语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。
终于讲到了 happens-before
,先来看 happens-before
关系的定义:
happens-before
另一个操作,那么第一个操作的执行结果就会对第二个操作可见happens-before
关系,并不意味着 Java
平台的具体实现就必须按照 happens-before
关系指定的顺序来执行。如果重排序之后的执行结果,与按照 happens-before
关系来执行的结果一直,那么 JMM
也允许这样的重排序看到这儿,你是不是觉得,这个怎么和 as-if-serial
语义一样呢。没错, happens-before
关系本质上和 as-if-serial
语义是一回事。
as-if-serial
语义保证的是单线程内重排序之后的执行结果和程序代码本身应该出现的结果是一致的, happens-before
关系保证的是正确同步的多线程程序的执行结果不会被重排序改变。
一句话来总结就是:如果操作 A happens-before
操作 B ,那么操作 A 在内存上所做的操作对操作 B 都是可见的,不管它们在不在一个线程。
在 Java
中,对于 happens-before
关系,有以下规定:
happens-before
于该线程中的任意后续操作happens-before
于随后对这个锁的加锁volatile
变量规则:对一个 volatile
域的写, happens-before
与任意后续对这个 volatile
域的读happens-before
B , 且 B happens-before
C ,那么 A happens-before
Cstart()
规则:如果线程A执行操作ThreadB.start()
(启动线程B),那么A线程的ThreadB.start()
操作happens-before
于线程B中的任意操作。join()
规则:如果线程A执行操作ThreadB.join()
并成功返回,那么线程B中的任意操作happens-before
于线程A从ThreadB.join()操作成功返回。如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e
,他会拉你们进群。
大家对Paxos
的看法基本是“晦涩难懂”,虽然论文和网上文章也很多,但总觉得“云山雾罩”,也不知道其具体原理以及到底能解决什么问题。
究其原因,一方面是很多Paxos
的资料都是在通过形式化的证明去论证算法的正确性,自然艰深晦涩;另一方面,基于Paxos
的成熟工程实践并不多。本章试图由浅入深,从问题出发,一点点地深入Paxos
的世界。
先看一个基本的并发问题,如图116所示。假设有一个KV存储集群,三个客户端并发地向集群发送三个请求。请问,最后在get(X)的时候,X应该等于几?
图116(K,V)集群多写答案是:X=1、X=3或X=5都是对的!但X=4是错的!因为从客户端角度来看,三个请求是并发的,但三个请求到达服务器的顺序是不确定的,所以最终三个结果都有可能。
这里有很关键的一点:把答案换一种说法,即如果最终集群的结果是X=1,那么当Client1发送X=1的时候,服务器返回X=1;当Client2发送X=3的时候,服务器返回X=1;当Client3发送X=5的时候,服务器返回X=1。相当于Client1的请求被接受了,Client2、Client3的请求被拒绝了。如果集群最终结果是X=3或者X=5,是同样的道理。而这正是Paxos
协议的一个特点。
把问题进一步细化:假设KV集群有三台机器,机器之间互相通信,把自己的值传播给其他机器,三个客户端分别向三台机器发送三个请求,如图117所示。
图117三台机器组成的(K,V)集群多写示意图假设每台机器都把收到的请求按日志存下来(包括客户端的请求和其他Node的请求)。当三个请求执行完毕后,三台机器的日志分别应该是什么顺序?
结论是:不管顺序如何,只要三台机器的日志顺序是一样的,结果就是正确的。如图118所示,总共有3的全排列,即6种情况,都是正确的。比如第1种情况,三台机器存储的日志顺序都是X=1、X=3、X=5,在最终集群里,X的值肯定等于5。其他情况类似。
而下面的情况就是错误的:机器1的日志顺序是1、3、5,因此最终的值就是X=5;机器2是3、5、1,最终值是X=1;机器3的日志顺序是1、5、3,最终值是X=3。三台机器关于X的值不一致,如图109所示。
通过这个简单的例子就能对“时序”有一个直观的了解:虽然三个客户端是并发的,没有先后顺序,但到了服务器的集群里必须保证三台机器的日志顺序是一样的,这就是所谓的“分布式一致性”。
问题在例子中,Node1收到了X=1之后,复制给Node2和Node3;Node2收到X=3之后,复制给Node1和Node3;Node3收到X=5之后,复制给Node1和Node2。
客户端是并发的,三个Node之间的复制也是并发的,如何保证三个Node最终的日志顺序是一样的呢?也就是图118中6种正确情况中的1种。
比如Node1先收到客户端的X=1,之后收到Node3的X=5,最后收到Node2的X=3;Node2先收到客户端的X=3,之后收到Node1的X=1,最后收到Node3的X=5……
如何保证三个Node中存储的日志顺序一样呢?这正是接下来要讲的Paxos
要解决的问题!
在上文谈到了复制日志的问题,每个Node存储日志序列,Node之间保证日志完全一样。可能有人会问:为何要存储日志,直接存储最终的数据不就行了吗?
可以把一个变量X或一个对象看成一个状态机。每一次写请求,就是一次导致状态机发生变化的事件,也就是日志。
以上文中最简单的一个变量X为例,假设只有一个Node,3个客户端发送了三个修改X的指令,最终X的状态就是6,如图1110所示。
图1110状态机X示意图把变量X扩展成MySQL数据库,客户端发送各种DML操作,这些操作落盘成Binlog。然后Binlog被应用,生成各种数据库表格(状态机),如图1111所示。
这里涉及一个非常重要的思想:要选择持久化变化的“事件流(也就是日志流)”,而不是选择持久化“数据本身”(也就是状态机)。为何要这么做呢?原因有很多,列举如下:
(1)日志只有一种操作,就是append。而数据或状态一直在变化,可以add、delete、update。把三种操作转换成了一种,对于持久化存储来说简单了很多!
(2)假如要做多机之间数据同步,如果直接同步状态,状态本身可能有一个很复杂的数据结构(比如关系数据库的关联表、树、图),并且状态也一直在变化,要保证多个机器数据一致,要做数据比对,就很麻烦;而如果同步日志,日志是一个一维的线性序列,要做数据比对,则非常容易!
总之,无论从持久化,还是数据同步角度来看,存储状态机的输入事件流(日志流),都比存储状态机本身更容易。
基于这种思路,可以把状态机扩展为复制状态机。状态机的原理是:一样的初始状态+一样的输入事件=一样的最终状态。因此,要保证多个Node的状态完全一致,只要保证多个Node的日志流是一样的即可!即使这个Node宕机,只需重启和重放日志流,就能恢复之前的状态,如图1012所示。
因此,就回到了上文最后的问题:复制日志!复制日志=复制任何数据(复制任何状态机)。因为任何复杂的数据(状态机)都可以通过日志生成!
Paxos
的出现先经过了Basic Paxos
的形式化证明,之后再有Multi Paxos
,最后是应用场景。因为最开始没有先讲应用场景,所以直接看Basic Paxos
的证明会很晦涩。本文将反过来,就以上文最后提出的问题为例,先介绍应用场景,再一步步倒推出Paxos
和Multi Paxos
。
当三个客户端并发地发送三个请求时,图118所示的6种可能的结果都是对的。因此,要找一种算法保证虽然每个客户端是并发地发送请求,但最终三个Node记录的日志的顺序是相同的,也就是图108所示的任取一种场景即可。
这里提出一个朴素而深刻的说法:全世界对数字1,2,3,4,5,……顺序的认知是一样的!所有人、所有机器,对这个的认知都是一样的!
当我说2的时候,全世界的人,都知道2排在1的后面、3的前面!2代表一个位置,这个位置一定在(1,3)之间。
把这个朴素的想法应用到计算机里面多个Node之间复制日志,会变成如下这样。当Node1收到X=1的请求时,假设要把它存放到日志中1号位置,存放前先询问另外两台机器1号位置是否已经存放了X=3或X=5;如果1号位置被占了,则询问2号位置……依此类推。如果1号位置没有被占,就把X=1存放到1号位置,同时告诉另外两个Node,把X=1存放到它们各自的1号位置!同样,Node2和Node3按此执行。
这里的关键思想是:虽然每个Node接收到的请求顺序不同,但它们对于日志中1号位置、2号位置、3号位置的认知是一样的,大家一起保证1号位置、2号位置、3号位置存储的数据一样!
在例子中可以看到,每个Node在存储日志之前先要问一下其他Node,之后再决定把这条日志写到哪个位置。这里有两个阶段:先问,再做决策,也就是Paxos
2PC的原型!
把问题进一步拆解,不是复制三条日志,只复制一条。先确定三个Node的第1号日志,看有什么问题?
Node1询问后发现1号位置没有被占,因此它打算把X=1传播给Node2和Node3;同一时刻,Node2询问后发现1号位置也没有被占,因此它打算把X=3传播给Node1和Node3;同样,Node3也打算把X=5传播给Node1和Node2。
结果不就冲突了吗?会发现不要说多条日志,就算是只确定第1号位置的日志,都是个问题!
而BasicPaxos
正是用来解决这个问题的。
首先,1号位置要么被Node1占领,大家都存放X=1;要么被Node2占领,大家都存放X=3;要么是被Node3占领,大家都存放X=5,少数服从多数!为了达到这个目的,BasicPaxos
提出了一个方法,这个方法包括两点:
第1,Node1在填充1号位置的时候,发现1号位置的值被大多数确定了,比如是X=5(node3占领了1号位置,Node2跟从了Node3),则Node1就接受这个事实:1号位置不能用了,也得把自己的1号位置赋值成X=5。然后看2号位置能否把X=1存进去。同样地,如果2号也被占领了,就只能把它们的值拿过来填在自己的2号位置。只能再看3号位置是否可行……
第2,当发现1号位置没有被占,就锁定这个位置,不允许其他Node再占这个位置!除非它的权利更大。如果发现1号位置为空,在提交的时候发现1号位置被其他Node占了,就会提交失败,重试,尝试第二个位置,第三个位置……
所以,为了让1号位置日志一样,可能要重试好多次,每个节点都会不断重试2PC。这样不断重试2PC,直到最终各方达成一致的过程,就是Paxos
协议执行的过程,也就是一个Paxos
instance,最终确定一个值。而MultiPaxos
就是重复这个过程,确定一系列值,也就是日志中的每一条!
接下来将基于这种思想详细分析Paxos
算法本身。
Paxos
算法在前面的场景中提到三个Client并发地向三个Node发送三条写指令。对应到Paxos
协议,就是每个Node同时充当了两个角色:Proposer和Acceptor。在实现过程中,一般这两个角色是在同一个进程里面的。
当Node1收到Client1发送的X=1的指令时,Node1就作为一个Proposer向所有的Acceptor(自己和其他两个Node)提议把X=1日志写到三个Node上面。
同理,当Node2收到Client2发送的X=3的指令,Node2就作为一个Proposer向所有的Acceptor提议;Node3同理。
下面详细阐述Paxos
的算法细节。首先,每个Acceptor需要持久化三个变量(minProposalId,acceptProposalId,acceptValue)。在初始阶段:minProposalId=acceptProposalId=0,acceptValue=null。然后,算法有两个阶段:P1(Prepare阶段)和P2(Accept阶段)。
Prepare阶段P1a:Proposer广播prepare(n),其中n是本机生成的一个自增ID,不需要全局有序,比如可以用时间戳+IP。P1b:Acceptor收到prepare(n),做如下决策:
1 | if n > minProposalId,回复Yes |
P1c:Proposer如果收到半数以上的yes,则取acceptorProposalId最大的acceptValue作为v,进入第二个阶段,即开始广播accept(n,v)。如果acceptor返回的都是null,则取自己的值作为v,进入第二个阶段!否则,n自增,重复P1a。
P2a:Proposer广播accept(n,v)。这里的n就是P1阶段的n,v可能是自己的值,也可能是第1阶段的acceptValue。P2b:Acceptor收到accept(n,v),做如下决策:
1 | if n > minProposalId,回复Yes。同时 |
P2c:Proposer如果收到半数以上的yes,并且minProposalId=n,则算法结束。否则,n自增,重复P1a。
通过分析算法,会发现BasicPaxos
有两个问题:
(1)Paxos
是一个“不断循环”的2PC。在P1C或者P2C阶段,算法都可能失败,重新进行P1a。这就是通常所说的“活锁”问题,即可能陷入不断循环。
(2)每确定一个值,至少需要两次RTT(两个阶段,两个网络来回)+两次写盘,性能也是个问题。而接下来要讲的MultiPaxos
就是要解决这两个问题。
Paxos
算法在前面已经知道,BasicPaxos
是一个不断循环的2PC。所以如果是多个客户端写多个机器,每个机器都是Proposer,会导致并发冲突很高,也就是每个节点都可能执行多次循环才能确定一条日志。极端情况是每个节点都在无限循环地执行2PC,也就是所谓的“活锁问题”。
为了减少并发冲突,可以变多写为单写,选出一个Leader,只让Leader充当Proposer。其他机器收到写请求,都把写请求转发给Leader;或者让客户端把写请求都发给Leader。
Leader的选举方法很多,下面列举两种:
方案1:无租约的Leader选举
Lamport在他的论文中给出了一个Leader选举的简单算法,算法如下:
(1)每个节点有一个编号,选取编号最大的节点为Leader;
(2)每个节点周期性地向其他节点发送心跳,假设周期为Tms;
(3)如果一个节点在最近的2Tms内还没有收到比自己编号更大的节点发来的心跳,则自己变为Leader;
(4)如果一个节点不是Leader,则收到请求之后转发给Leader。可以看出,这个算法很简单,但因为网络超时原因,很可能出现多个Leader,但这并不影响MultiPaxos
协议的正确性,只是增大并发写冲突的概率。我们的算法并不需要强制保证,任意时刻只能有一个Leader。
方案2:有租约的Leader选举
另外一种方案是严格保证任意时刻只能有一个leader,也就是所谓的“租约”。租约的意思是在一个限定的期限内,某台机器一直是Leader。即使这个机器宕机,Leader也不能切换。必须等到租期到期之后,才能开始选举新的Leader。这种方式会带来短暂的不可用,但保证了任意时刻只会有一个Leader。具体实现方式可以参见Paxos
Lease。
我们知道BasicPaxos
是一个无限循环的2PC,一条日志的确认至少需要两个RTT+两次落盘(一次是Prepare的广播与回复,一次是Accept的广播与回复)。如果每条日志都要两个RTT+两次落盘,这个性能就很差了。而MultiPaxos
在选出Leader之后,可以把2PC优化成1PC,也就只需要一个RTT+一次落盘了。
基本思路是当一个节点被确认为Leader之后,它先广播一次Prepare,一旦超过半数同意,之后对于收到的每条日志直接执行Accept操作。在这里,Perpare不再是对一条日志的控制了,而是相对于拿到了整个日志的控制权。一旦这个Leader拿到了整个日志的控制权,后面就直接略过Prepare,直接执行Accept。
如果有新的Leader出现怎么办呢?新的Leader肯定会先发起Prepare,导致minProposalId变大。这时旧的Leader的广播Accept肯定会失败,旧的Leader会自己转变成一个普通的Acceptor,新的Leader把旧的顶替掉了。
下面是具体的实现细节:在BasicPaxos
中,2PC的具体参数形式如下:
1 | prepare(n) |
在MultiPaxos
中,增加一个日志的index参数,即变成了如下形式:
1 | prepare(n,index) |
对于一条日志,当Proposer(也就是Leader)接收到多数派对Accept请求的同意后,就知道这条日志被“choose”了,也就是被确认了,不能再更改!
但只有Proposer知道这条日志被确认了,其他的Acceptor并不知道这条日志被确认了。如何把这个信息传递给其他Accepotor呢?
方案1:Proposer主动通知
给accept再增加一个参数:
1 | accept(n,v,index,firstUnchooseIndex) |
Proposer在广播accept的时候,额外带来一个参数firstUnchosenIndex=7。意思是7之前的日志都已经“choose”了。Acceptor收到这种请求后,检查7之前的日志,如果发现7之前的日志符合以下条件:acceptedProposal[i]==request.proposal(也就是第一个参数n),就把该日志的状态置为choose。
解决方案2:Acceptor被动查询
当一个Acceptor被选为Leader后,对于所有未确认的日志,可以逐个再执行一遍Paxos
,来判断该条日志被多数派确
认的值是多少。
因为BasicPaxos
有一个核心特性:一旦一个值被确定后,无论再执行多少遍Paxos
,该值都不会改变!因此,再执行1遍Paxos
,相当于向集群发起了一次查询!
至此,MultiPaxos
算法就介绍完了。回顾这个算法,有两个精髓:
精髓之1:一个强一致的“P2P网络”
任何一条日志,只有两种状态(choose,unchoose)。当然,还有一种状态就是applied,也就是被确认的日志被apply到状态机。这种状态跟Paxos
协议关系不大。
choose状态就是这条日志,被多数派接受,不可更改;
unchoose就是还不确定,引用阿里OceanBase团队某工程师的话,就是“薛定谔的猫”,或者“最大commit原则”。一条unchoose的日志可能是已经被choose了,只是该节点还不知道;也可能是还没有被choose。要想确认,那就再执行一次Paxos
,也就是所谓的“最大commit原则”。
整个MultiPaxos
就是类似一个P2P网络,所有节点互相双向同步,对所有unchoose的日志进行不断确认的过程!在这个网络中可以出现多个Leader,可能出现多个Leader来回切换的情况,这都不影响算法的正确性!
精髓之2:“时序”
MultiPaxos
保证了所有节点的日志顺序一模一样,但对于每个节点自身来说,可以认为它的日志并没有所谓的“顺序”。什么意思呢?
(1)假如一个客户端连续发送了两条日志a,b(a没有收到回复,就发出了b)。对于服务器来讲,存储顺序可能是a、b,也可能是b、a,还可能在a、b之间插入了其他客户端发来的日志!
(2)假如一个客户端连续发送了两条日志a、b(a收到回复之后,再发出的b)。对于服务器来讲,存储顺序可能是a、b;也可能是a、xxx、b(a与b之间插入了其他客户端的日志),但不会出现b在a的前面。
所以说,所谓的“时序”,只有在单个客户端串行地发送日志时,才有所谓的顺序。多个客户端并发地写,服务器又是并发地对每条日志执行Paxos
,整体看起来就没有所谓的“顺序”。
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e
,他会拉你们进群。
Spring
是一个非常优秀的开源项目,而且基本是互联网的标配。随着这几年的源码阅读习惯,有用一套自己的源码阅读笔记项目已经是水到渠成。今天就来在本地编译一份,以便于后面记录核心笔记用。github
同步一次)fork
一份)为什么贴这些url
地址,是因为提醒我们所有人,官网与github
是我们最先关注的地方。
gradle.properties
1 | version=5.3.5-SNAPSHOT |
从上面可以看出来,我们现在用到是spring 5.3.5-SNAPSHOT
版本。后面会遇到一些编译问题,避免大家踩坑,我直接说。这里依赖的JDK需要在JDK11。
下载JDK11
的版本:https://www.oracle.com/java/technologies/javase-jdk11-downloads.html
但是,但是,但是官网的实在是太慢了,所以我这里就直接改为openjdk11
了。传送门:https://mirrors.tuna.tsinghua.edu.cn/AdoptOpenJDK/11/jdk/x64/ 下载速度就看你家里的带宽了,我基本在3M+
左右~
没有对比就没有伤害啊。
配置好项目中使用的JDK
版本,这边因为诉求不一样,平时基本还是用的JDK8
。
因为我这里是Mac
系统,所以一般的软件我都会采用brew
来实现,因为实在太方便了。一行命令搞定~
1 | brew install gradle |
下面是我安装的gradle信息,是最新的版本6.7。
1 | ➜ ~ gradle -v |
因为spring
在代码中有对Kotlin
有一些依赖(低版本的可能会出现依赖问题),建议搭建升级到比价新的版本。
因为spring
依赖与gradle
,而且都是国外的网站,相对来说网速会非常的慢。这里也是采用更换镜像的方式来加速处理。
build.gradle
1 | repositories { |
settings.gradle
1 | repositories { |
配置一下gradle
的参数
找到ApplicationContext
接口,显示它的一个类关系图。如果出现下图所示类图界面说明构建成功了!(构建过程就是找依赖对象的过程)
查看官方import-into-idea.md文档,如下内容:
1 | 1.Precompile spring-oxm with ./gradlew :spring-oxm:compileTestJava |
先编译spring-oxm
下的compileTestjava
,点击右上角gradle
打开编译视图,找到spring-oxm
模块,然后在other
下找到compileTestjava
,双击即可!
利用同样的方式,编译spring-core
模块,出现BUILD SUCCESSFUL
代码成功。
打开右侧Gradle
插件 spring->build->build
。
编译时间,每个人电脑的性能不一样,所需时间也不一样。
我这里基本上全部成功,但是在Task :spring-webflux:test
模块有一个test
跑失败。算是有一点小瑕疵,但不影响整个编译。
构建成功了,源码编译成功了,能否用还不知道,还要测试通过才说明源码可以用了。直接在spring-context
里面创建我们的一个service
,然后通过ApplicationContext
去获取它,并执行它的一个方法。
代码部分我已经push
上去了:https://gitee.com/vernon/Spring-Framework,后续就可以自由自在的在上面标注以及验证了。
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e
,他会拉你们进群。
这里的二值状态就是指集合元素的取值就只有 0 和 1 两种。在签到打卡的场景中,我们只用记录:
所以它就是非常典型的二值状态,在签到统计时,每个用户一天的签到用 1 个 bit 位就能表示,一个月(假设是 31 天)的签到情况用 31 个 bit 位就可以,而一年的签到也只需要用 365 个 bit 位,根本不用太复杂的集合类型。
这个时候,我们就可以选择 Bitmap
。这是 Redis
提供的扩展数据类型。我来给你解释一下它的实现原理。Bitmap
本身是用 String
类型作为底层数据结构实现的一种统计二值状态的数据类型。String
类型是会保存为二进制的字节数组,所以,Redis
就把字节数组的每个 bit
位利用起来,用来表示一个元素的二值状态。
你可以把 Bitmap
看作是一个 bit
数组。Bitmap
提供了 GETBIT/SETBIT
操作,使用一个偏移值 offset
对 bit
数组的某一个 bit
位进行读和写。不过,需要注意的是,Bitmap
的偏移量是从 0 开始算的,也就是说 offset
的最小值是 0。当使用 SETBIT
对一个 bit 位进行写操作时,这个 bit 位会被设置为 1。
Bitmap 还提供了 BITCOUNT
操作,用来统计这个 bit 数组中所有“1”的个数。那么,具体该怎么用 Bitmap
进行签到统计呢?我还是借助一个具体的例子来说明。
假设我们要统计 ID 3000
的用户在 2020 年 8 月份的签到情况,就可以按照下面的步骤进行操作。
第一步,执行下面的命令,记录该用户 8 月 3 号已签到。
1 | SETBIT uid:sign:3000:202008 2 1 |
第二步,检查该用户 8 月 3 日是否签到。
1 | GETBIT uid:sign:3000:202008 2 |
第三步,统计该用户在 8 月份的签到次数。
1 | BITCOUNT uid:sign:3000:202008 |
这样,我们就知道该用户在 8 月份的签到情况了,是不是很简单呢?
接下来,你可以再思考一个问题:如果记录了 1 亿个用户 10 天的签到情况,你有办法统计出这 10 天连续签到的用户总数吗?
在介绍具体的方法之前,我们要先知道,Bitmap
支持用 BITOP
命令对多个 Bitmap
按位做“与”“或”“异或”的操作,操作的结果会保存到一个新的 Bitmap
中。
我以按位“与”操作为例来具体解释一下。从下图中,可以看到,三个 Bitmap
: bm1
、bm2
和 bm3
,对应 bit 位做“与”操作,结果保存到了一个新的 Bitmap 中(示例中,这个结果 Bitmap 的 key 被设为“resmap”)。
回到刚刚的问题,在统计 1 亿个用户连续 10 天的签到情况时,你可以把每天的日期作为 key,每个 key 对应一个 1 亿位的 Bitmap
,每一个 bit 对应一个用户当天的签到情况。
接下来,我们对 10 个 Bitmap
做“与”操作,得到的结果也是一个 Bitmap
。在这个 Bitmap
中,只有 10 天都签到的用户对应的 bit 位上的值才会是 1。最后,我们可以用 BITCOUNT
统计下 Bitmap 中的 1 的个数,这就是连续签到 10 天的用户总数了。
现在,我们可以计算一下记录了 10 天签到情况后的内存开销。每天使用 1 个 1 亿位的 Bitmap
,大约占 12MB
的内存(10^8/8/1024/1024),10 天的 Bitmap
的内存开销约为 120MB
,内存压力不算太大。不过,在实际应用时,最好对 Bitmap 设置过期时间,让 Redis
自动删除不再需要的签到记录,以节省内存开销。
所以,如果只需要统计数据的二值状态,例如商品有没有、用户在不在等,就可以使用 Bitmap,因为它只用一个 bit 位就能表示 0 或 1。在记录海量数据时,Bitmap
能够有效地节省内存空间。
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e
,他会拉你们进群。
我们通过Docker快速的拉取一个环境,这样子对于学习来说成本比价低。直接参照https://hub.docker.com/r/mysql/mysql-server/ 来做就好了。
1 | 快速拉取一个mysql的image |
这是我认为非常快速的一个环境搭建的方式。
一般像Orace、MySQL这些好的软件都会提供一些官网的数据来让方便学习着来学习。https://dev.mysql.com/doc/index-about.html
那我们就下载sakila database
来学习一下。
1 | show tables; |
整体的来看一下数据的一个关系
这里会有一个非常奇怪的问题,就是第一条sql竟然走了idx_actor_last_name
索引。
1 | EXPLAIN SELECT actor_id FROM actor WHERE actor_id + 1 = 4; |
可以理解为是走的B+树,叶子节点里面刚好存储的是主键,而主键刚好是actor_id,select的时候刚好是主键一个值吗?
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e
,他会拉你们进群。
还记得年初的时候,怕被疫情隔在家里了,正月初三就开车到了深圳。那时候听新闻说可能要带半年或者一年的口罩,当时感觉不可思议,根本不可能。现在一年已经已经过去了,但口罩还依然要继续……
说到口罩,记得刚刚开始在老家带口罩的时候,家里好多人不理解,为什么要戴口罩?(估计跟很多老美一样,觉得不尊重,觉得自己没病等)到了“恐慌”的时候,家里人比我们更着急,更加上心。在这次疫情中,真的也是感受到了人间百态,看到了白衣天使的伟大,看到了一些黑心的商家,看到了那些平凡的人做着不平凡的事儿,感受到了近些年来祖国的伟大(非常感谢国内短视频)。
今年给我最大的感受是繁忙、成长、充实,总的来说是收获的一年吧。
年初的时候开启了我的个人公共账号「简栈文化」,一共发表了文章112篇,几乎全部都是跟技术相关的,在编写与梳理这些知识的时候,内心是充满热情与喜悦的。特别是大家去催更的时候,或者有问题请教你的时候,跟你讨论的时候,所有的熬夜与辛苦都是值得的,因为你获得的知识与朋友。后面也是因为工作原因,更新就慢慢的停了下来,但内心一直牵挂着写作这件事儿。
今年可以说自己是换了工作,也可以说自己没有换工作,因为做的事情不同了,从楼上换到了楼下,但最大的老板还是同样的老板。不过,还是真的很感谢保险与国际的同事,都给了我很大的帮助、鼓励、肯定。在保险从事的是信贷相关的业务,从小白变成了有一丝丝信贷经验的从业者,也有着自己的小团队,而且氛围非常好,非常的优秀。到国际做的是证券业务,团队又是从0开始搭建,业务又不是很熟悉,感觉一下突然回到了4年前。经过了4个月左右的时间,终于又把团队搭建起来了,同时也在努力的去学习业务与技术。更重要的,我自己对自己的要求更高了,想做的更好。这次的团队跟之前一样,无可挑剔,更加的棒~
这一年虽然看的书籍不是特别多(差不多12本),但是因为这一年的写作原因,内心是非常愿意静下来去好好看一本的。不管是技术的书籍,还是人物传记的书籍,总能在从中学到一些东西,哪怕只有其中只有某一句话、一件事儿、一个技术难点、一个技巧也是非常开心的。现在看书主要是在Kindle上面,技术书籍就会买纸质版本的,非常棒的书籍纸质版与电子版都买,还是为了图方便。不管如何,只要找到自己喜欢的方式,做任何事情都是对的~
这一年,跟我老婆吵架非常的少,但记忆中好像也有这么12次是比较严重的,但都能很快过去。都说一个成功男人的背后一定有一个优秀的女人,我非常赞同。我觉得在思想上,我家那位比我要强,我是属于追赶她的情况。今年,一起去南京休闲游完了几天,特别喜欢这种慢悠悠,自由自在的旅行。一转眼,我们已经认识了10年,本想2人在很久以前就一直筹划着要过一个非常难忘的10周年,为此要做很多的准备,也是因为工作忙碌的原因,算是过了一个开心的10周年吧。这一年我从以前什么都不管慢慢开始的变为想去用心准备点什么,付出一点什么。为了明年去更多的没有去过的地方,特此还跟我老婆买了南航的随心飞,希望能去更多的地方看看这个美好的世界,去更多的地方打打卡。这几天还对摄影有点兴趣,日后可以好好学习学习
最近2年左右的时间,真的听说了太多太多亲人们身体不好做手术的,家里的老奶奶、舅姥姥等去世的消息。其实人到30~40岁之间,应该是蛮有经济压力的,自己的梦想还有很多未实现,爸爸妈妈的年纪也开始慢慢到了一个体质变弱、疾病变多的情况,所以今年我们为2边的家人都配置了意外险、百万医疗险,为自己也配置了这些,也终于在2020年的最后一天为我们俩配置了重疾险(本来想着买港险的,可这疫情一拖就是一年)。虽然这些保险远远还不够,但是也不用太着急,首先有了基本的保障,后面再慢慢的去添加就好了。起码,真的哪天有意外来临,也不会变得很被动。
今年一年跟我老婆也获得一些新朋友,特别是公共的朋友。在深圳这个城市,能够找到一些私下要好的朋友是很难的,因为大家的生活节奏都很快,大部分都不认识隔壁的邻居是谁,每天都是很早的去上班,很晚才回家来。如果有一些私下的朋友,节假日的时候可以一起凑个饭局,一起郊游,一起钓个鱼什么的都是一件很幸福的事情。所以,珍惜现在已有的好朋友,结交更多更优秀的人。
我知道立再多的flag也不如把一个flag做好来的重要,我希望自己在2021年里能更多知道自己的内心,知道自己追求的是什么?如何让自己的内心世界丰富起来,如果让自己做任何的事情都不会被别人牵着鼻子走,如何去影响、号召更多的其他人一起来做事。
2021年给自己的目的是:抓重点、求突破、顾家庭。
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e
,他会拉你们进群。
今天本来是为了解决一个Apollo
配置与Code
同时变更不一致问题,我需要去通过SPI
的方式去重写Apollo
刷新Remote
配置。所以,我就写一个很小的DEMO
来验证每次变更Apollo
配置对我服务端的取数。
1 |
|
对应在Apollo
的配置
1 | Test1111 = |
Controller
层的代码
1 |
|
然后我就发起请求来验证我的取数,结果给我报了NullpointException
的异常。因为这种代码几乎每天都在写,一下次说我的Bean
没有注入进来,还有点方。
然后,我细心的debug
了看了一下。如图所示:
图片可以说明:
Spring
容器100%已经存在了MarketConfig
的Bean
对象;
但是MarketConfig
在该Controller
里面100%没有注入进来 ;
this
显示出了一个代理的路径;
于是,我在同样的Controller
去请求另外一个Method
,但可以正常访问。
Spring
容器100%已经存在了MarketConfig
的Bean
对象;MarketConfig
是有注入进来的(没有展示图,不太方便)this
直接是显示的自己的地址由于我就比较了一下方法的区别,最主要的区别点就是在于一个是private
,另外一个是public
。
其实,这里我就很快明白了。因为我们这边在Controller
层做了一些AOP来做监控与埋点。现阶段主要用的是CAT
(现在做了很多的一些封装,后面可以分享下,如何做到无浸入性以及与Apollo
的打通)。
AOP
里面用的是反射机制,用private
修饰的类是注入失败的,因为拿不到,只能用public
或者protected
修饰。
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e
,他会拉你们进群。
MySQL
的默认事务隔离级别为 Repeatable Read
。而 ORACLE
、SQLServer
等的默认隔离级别使用的是 Read Committed
模式,为什么呢?
开始我们的内容,相信大家一定遇到过下面的一个面试场景
面试官:“讲讲 mysql 有几个事务隔离级别?”
你:“读未提交,读已提交,可重复读,串行化四个!默认是可重复读”
面试官:“为什么 mysql 选可重复读作为默认的隔离级别?”
(你面露苦色,不知如何回答!)
面试官:“你们项目中选了哪个隔离级别?为什么?”
你:“当然是默认的可重复读,至于原因…呃…”
(然后你就可以回去等通知了!)
为了避免上述尴尬的场景,请继续往下阅读!Mysql
默认的事务隔离级别是可重复读 (Repeatable Read
),那互联网项目中 Mysql
也是用默认隔离级别,不做修改么?OK
,不是的,我们在项目中一般用读已提交 (Read Commited
) 这个隔离级别!what
!居然是读已提交,网上不是说这个隔离级别存在不可重复读和幻读问题么?不用管么?好,带着我们的疑问开始本文!
我们先来思考一个问题,在 Oracle
,SqlServer
中都是选择读已提交 (Read Commited) 作为默认的隔离级别,为什么 Mysql
不选择读已提交 (Read Commited
) 作为默认隔离级别,而选择可重复读 (Repeatable Read
) 作为默认的隔离级别呢?
这个是有历史原因的,当然要从我们的主从复制开始讲起了!
主从复制,是基于什么复制的?
是基于 binlog
复制的!这里不想去搬 binlog
的概念了,就简单理解为 binlog
是一个记录数据库更改的文件吧~binlog
有几种格式?
OK,三种,分别是:
statement
: 记录的是修改 SQL
语句mixed
:statement 和 row
模式的混合那 Mysql
在 5.0 这个版本以前,binlog
只支持 STATEMENT
这种格式!而这种格式在读已提交 (Read Commited
) 这个隔离级别下主从复制是有 bug
的,因此 Mysql
将可重复读 (Repeatable Read
) 作为默认的隔离级别!
接下来,就要说说当 binlog
为 STATEMENT
格式,且隔离级别为读已提交 (Read Commited
) 时,有什么 bug
呢?如下图所示,在主 (master) 上执行如下事务
此时在主 (master
) 上执行下列语句
1 | select * from test; |
输出如下
1 | ±–+ |
但是,你在此时在从 (slave
) 上执行该语句,得出输出如下
1 | Empty set |
这样,你就出现了主从不一致性的问题!原因其实很简单,就是在 master
上执行的顺序为先删后插!而此时 binlog
为 STATEMENT 格式,它记录的顺序为先插后删!从 (slave
) 同步的是 binglog
,因此从机执行的顺序和主机不一致!就会出现主从不一致!
解决方案有两种!
Repeatable Read
), 在该隔离级别下引入间隙锁。当 Session 1
执行 delete
语句时,会锁住间隙。那么,Ssession 2
执行插入语句就会阻塞住!row
格式,此时是基于行的复制,自然就不会出现 sql 执行顺序不一样的问题!奈何这个格式在 mysql5.1
版本开始才引入。因此由于历史原因,mysql
将默认的隔离级别设为可重复读 (Repeatable Read
),保证主从复制不出问题!那么,当我们了解完 mysql
选可重复读 (Repeatable Read
) 作为默认隔离级别的原因后,接下来我们将其和读已提交 (Read Commited
) 进行对比,来说明为什么在互联网项目为什么将隔离级别设为读已提交(Read Commited
)!
OK,我们先明白一点!项目中是不用读未提交 (Read UnCommitted
) 和串行化 (Serializable) 两个隔离级别,原因有二
采用读未提交 (Read UnCommitted
), 一个事务读到另一个事务未提交读数据,这个不用多说吧,从逻辑上都说不过去!
采用串行化 (Serializable
),每个次读操作都会加锁,快照读失效,一般是使用 mysql
自带分布式事务功能时才使用该隔离级别!(笔者从未用过 mysql
自带的这个功能,因为这是 XA
事务,是强一致性事务,性能不佳!互联网的分布式方案,多采用最终一致性的事务解决方案!)
也就是说,我们该纠结都只有一个问题,究竟隔离级别是用读已经提交呢还是可重复读?
接下来对这两种级别进行对比,讲讲我们为什么选读已提交 (Read Commited
) 作为事务隔离级别!
假设表结构如下
1 | CREATE TABLE `test` ( |
数据如下
1 | ±—±------+ |
为了便于描述,下面将
Repeatable Read
),简称为 RR
;RC
;缘由一
在 RR
隔离级别下,存在间隙锁,导致出现死锁的几率比 RC
大的多!
此时执行语句
1 | select * from test where id = 2 for update; |
在 RR
隔离级别下,存在间隙锁,可以锁住 (2,5)
这个间隙,防止其他事务插入数据!而在 RC
隔离级别下,不存在间隙锁,其他事务是可以插入数据!
在 RC
隔离级别下并不是不会出现死锁,只是出现几率比 RR
低而已!
缘由二
在 RR
隔离级别下,条件列未命中索引会锁表!而在 RC
隔离级别下,只锁行
此时执行语句
1 | update test set color = 'blue' where color = 'red'; |
在 RC
隔离级别下,其先走聚簇索引,进行全部扫描。加锁如下:
但在实际中,MySQL
做了优化,在 MySQL Server
过滤条件,发现不满足后,会调用 unlock_row
方法,把不满足条件的记录放锁。
实际加锁如下
然而,在 RR
隔离级别下,走聚簇索引,进行全部扫描,最后会将整个表锁上,如下所示
缘由三
在 RC
隔离级别下,半一致性读 (semi-consistent
) 特性增加了 update
操作的并发性!
在 5.1.15
的时候,innodb
引入了一个概念叫做 “semi-consistent
”,减少了更新同一行记录时的冲突,减少锁等待。
所谓半一致性读就是,一个 update
语句,如果读到一行已经加锁的记录,此时 InnoDB
返回记录最近提交的版本,由 MySQL
上层判断此版本是否满足 update
的 where
条件。若满足 (需要更新),则 MySQL
会重新发起一次读操作,此时会读取行的最新版本 (并加锁)!
具体表现如下:
此时有两个 Session
:Session1
和 Session2
!Session1
执行
1 | update test set color = 'blue' where color = 'red'; |
先不 Commit
事务!
与此同时 Ssession2
执行
1 | update test set color = 'blue' where color = 'white'; |
Session2
尝试加锁的时候,发现行上已经存在锁,InnoDB
会开启 semi-consistent read
,返回最新的 committed
版本 (1,red),(2,white),(5,red),(7,white)。MySQL
会重新发起一次读操作,此时会读取行的最新版本 (并加锁)!
而在 RR
隔离级别下,Session2
只能等待!
在 RC
级别下,不可重复读问题需要解决么?
不用解决,这个问题是可以接受的!毕竟你数据都已经提交了,读出来本身就没有太大问题!Oracle
的默认隔离级别就是 RC
,你们改过 Oracle 的默认隔离级别么?
在 RC
级别下,主从复制用什么 binlog
格式?
OK, 在该隔离级别下,用的 binlog
为 row
格式,是基于行的复制!Innodb
的创始人也是建议 binlog
使用该格式!
mysql —repeatable、oracle,sql server —read commited
mysql binlog
的格式三种:statement
、row
、mixed
mysql
用的是 repeatable
而不是 read committed:
在 5.0
之前只有 statement
一种格式,而主从复制存在了大量的不一致(bug
),故选用 repeatable
。read commited
原因有二:repeatable
存在间隙锁会使死锁的概率增大,在 RR
隔离级别下,条件列未命中索引会锁表!而在 RC
隔离级别下,只锁行RC
级用别下,主从复制用什么 binlog
格式:row
格式,是基于行的复制!如果使用 statement
格式,会导致主从不一致。如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e,他会拉你们进群。
]]>服务注册
在微服务启动时,首先,服务提供者需要将自己的服务注册到服务注册中心,服务提供者在启动的时候会发送REST
请求将自己注册到服务注册中心上,并带上一些元信息。服务注册中心接收到REST
请求,会将元信息存储在一个双层Map
中,第一层key
是服务名,第二层key
是具体服务的实例名。
注意:在服务注册时,需要确认一下eureka.client.register-with-eureka=true
是否正确,如果为false是禁止向服务注册中心注册的。
服务同步
当服务成功的注册到了注册中心之后,由于注册中心可能是高可用的集群,那么我们的服务可能只注册到了一个集群中的一个注册中心上,被一个注册中心所维护,而不被另外一个注册中心所维护,那么这个时候,我们就需要将这个注册中心的信息同步给集群中其他的注册中心,这就叫服务同步。那么他是如何实现的呢?
由于在集群中,一个注册中心互为其他注册中心的服务,当服务提供者请求到一个服务注册中心后,它会将请求转发到其他服务注册中心,实现注册中心之间的服务同步。
通过服务同步,服务提供者的服务信息可以通过集群中的任何一个服务注册中心获取。
服务续约
在注册完成后。服务提供者会维护一个心跳告诉注册中心服务,心跳间隔大约是30S
,防止注册中心剔除服务, 正常情况下,如果Eureka Server
在90秒
没有收到Eureka
客户的续约,它会将实例从其注册表中删除。这个过程称为服务续约。
服务获取
当一切的注册相关工作完成后,我们自然要获取服务清单,那么如何获取服务呢? 启动服务消费者后,消费者会发送一个REST
请求给服务注册中心,来获取上面注册的服务清单。 而服务注册中心会维护一份只读清单返回给消费者客户端,该缓存清单30s
更新一次。
服务调用
消费者获取服务清单后,可以通过服务名获取到具体服务实例与实例的元数据信息。这个时候,我们可以通过Ribbon
调用我们的目标服务,默认采用轮询的方式,从而实现负载均衡。
服务下线
当我们需要对服务实例进行正常的关闭操作时,它会触发一个服务下线的REST
请求给服务端。注册中心接收到请求后,将该服务状态置为DOWN
,并把下线时间传播出去。
失效剔除
有的时候,我们的服务意外挂掉了,那么Eureka
如何识别出我们异常的服务,并将其剔除呢?
服务注册中心启动时会创建定时任务,默认60s
一次,将当前清单中超时(90s
)没有续约的服务剔除。
自我保护
当失效剔除机制引入的时候,就会有一个问题,如果一个地区网络特别不稳定,那么服务可能不会续约,但我们还需要这个服务存在。这个时候,我们怎么解决呢?
还好,Eureka
拥有自我保护机制,可以很好的解决这个问题。Eureka Server
在运行期间,会统计心跳失败的比例在15分钟
之内是否低于85%
,如果低于,就会将当前实例注册信息保护起来,同时提示一个警告,一旦进入保护模式,Eureka Server
将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据。也就是不会注销任何微服务。 但是保护机制也有可能会出现问题,导致服务实例不能够被正确剔除。比如在保护期间,实例出现问题,那么客户端很容易拿到实际已经不存在的服务实例,会出现调用失败。
1 | protected void postInit() { |
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e,他会拉你们进群。
]]>