创业公司技术选型
MySQL主从复制架构实践
浅析高并发下MySQL的数据一致性
Base64编码原理解析
深入浅出——理解进程、线程和协程
计算机基础
Go mod使用指南
一、启用go mod
1 | go env -w GO111MODULE=on #开启 MODULE |
GO111MODULE可以设置为:off、on、auto(默认值),从GO111MODULE变量名可以看出,是Go1.11版本之后才出来有依赖包管理办法。
- 为
off
时,则不使用go mod,查找依赖包的顺序是:当前项目根目录/vendor
,其次是$GOPATH/src
(这是Golang1.11版本之前的用法)。 - 为
on
时,则开启go mod,查找依赖包是以当前项目根目录的go.mod
文件为基准,会忽略$GOPATH
和vendor
文件夹,只根据go.mod
下载依赖。 - 为
auto
或未设置,则go命令根据当前目录启用或禁用模块支持。仅当当前目录位于$GOPATH/src
之外且其本身包含go.mod
文件或位于包含go.mod
文件的目录下时,才启用模块支持。
GOPROXY为依赖包代理地址,由于像golang.org/x
这种依赖包需要翻墙才能下载,所以建议设置成国内镜像地址:
https://goproxy.cn 为国内七牛云维护的GO的镜像地址。
https://goproxy.io 为国内另一个镜像地址。
二、初始化go.mod
进入项目根目录(假如目录名为project1),初始化一个moudle,模块名为你的项目名,必须为英文名称,允许字母数字下划线和/
,但是不能以/
开头。
1 | go mod init [模块名] |
如果我们的项目根目录在$GOPATH/src/
中,模块名可以不填写,将自动生成,一般是与项目根目录名称同名,如project1
或github.com/project1
;如果项目根目录不在$GOPATH/src/
中,则模块名必须填写,模块名同样可以命名如project1
或github.com/project1
,也就是说模块名不一定与路径对应起来,但如果我们使用了路径,如github.com/project1
,后续也可以把项目搬到$GOPATH/src/github.com
目录下去。
执行完后,会在当前项目根目录下创建一个go.mod
文件,内容如:
1 | module project1 |
此时还没有任何依赖包信息。
三、整理依赖包
1 | go mod tidy |
命令执行后,go mod会去项目文件中发现依赖包,将依赖包名单添加到go.mod
文件中,自动删除那些有错误或者没有使用的依赖包。
四、将依赖包拷贝到项目vendor
1 | go mod vendor |
在项目完工时,我们也可以把依赖包复制到项目根目录/vendor/
,以便存档,其实也可以不做这个操作,有go.mod
文档就够了。
项目根目录下有了vendor后,就可以这样编译:
1 | go build -mod vendor -o ./project1 //project1为编译后的包名,或如下面,加上文件名 |
五、其他常用命令
下载依赖包到本地缓存
1 | go mod download |
若之前拉过一次代码,会把该次go.mod
里面的版本信息缓存到$GOPATH/pkg/mod/cache
里面,
下载时如果cache有该版本信息,就用cache里面的版本信息去下载,否则重新下载。
注意:如果你已经拉取了一个tag版本下的包,若这个tag包含的信息被修改了,需要清除cache才能重新拉取这个这个更新后tag的信息。
清理本地依赖包
1 | go clean -modcache |
以上命令是go mod download
的反操作,执行后会删除放置在$GOPATH/pkg/mod/cache
下的依赖包缓存,有时依赖包出现错误,可以先清理后再重新下载到本地缓存。
添加单个依赖包
1 | go mod edit -require=golang.org/x/text |
移除单个依赖包
如果我们不再使用某个包了,可以单独移除,如:
1 | go mod edit -droprequire=golang.org/x/text |
验证依赖是否正确
1 | go mod verify |
解释为什么需要依赖
1 | go mod why |
校验所有依赖包的正确性
1 | go mod verify |
六、go.mod文档说明
go.mod
文件可以通过require,replace,exclude
语句来说明依赖包的管理规则:require
语句用于指定必不可少的依赖包replace
语句用于指定需要替换依赖包的地址,比如golang.org/x/text
包替换成github.com/golang/text
exclude
语句用于指定可以忽略依赖项模块
例如:
1 | replace ( |
主要包括:golang.org google.golang.org gopkg.in go.uber.org cloud.google.com
在下载包时会有timeout 导致编译失败,以上是对应的github库的替换。
七、go get的使用
使用go mod
之后,go get
拉取依赖的方式就发生了变化:
1、老的go get取包过程类似:git clone + go install
, 开启Go Module功能后go get
就只有git clone
或者 download过程了。
2、新老实现还有一个不同是,两者存包的位置不同。前者,存放在$GOPATH/src
目录下;后者,存放在$GOPATH/pkg/mod
目录下。
3、老的go get
取完主包后,会对其repo下的submodule
进行循环拉取。新的go get
不再支持submodule
子模块拉取。
查看指定包可以下载的版本
1 | go list -m -versions github.com/gogf/gf |
下载项目依赖
1 | go get ./... |
拉取最新的版本(优先择取 tag)
1 | go get golang.org/x/text@latest |
拉取 master 分支的最新 commit
1 | go get golang.org/x/text@master |
拉取 tag 为 v0.3.2 的 commit
1 | go get golang.org/x/text@v0.3.2 |
拉取 hash 为 342b231 的 commit,最终会被转换为 v0.3.2:
1 | go get golang.org/x/text@342b2e |
指定版本拉取,拉取v3版本
1 | go get github.com/smartwalle/alipay/v3 |
更新
1 | go get -u |
八、go mod与原GOPATH的区别
1、环境变量GOPATH
不再用于解析imports包路径,即原有的$GOPATH/src/
下的包,通过import是找不到了
2、Go Module功能开启后,下载的包将存放与$GOPATH/pkg/mod
路径
3、$GOPATH/bin
路径的功能依旧保持
总结
在Go1.11版本之前,依赖包管理一直是GOPATH的包管理方式,不太灵活方便,1.11版本发布后,带来了全新的go mod
管理方式,用官方的话说,这是 GOPATH 的替代方案,集成了对版本控制和软件包分发的支持。可以看出借鉴了nodejs等包管理方式,项目的创建不再需要放在固定的$GOPATH/src/
路径中,配合着国内的依赖包镜像源,体验很顺滑,推荐!
基于Mycat中间件实现MySQL的分表分库与读写分离
一、安装Java
官网下载jdk1.8.0_212.tar.gz,解压到如下路径即可。
本次安装路径为:/usr/local/java/jdk1.8.0_212/
安装过程略。
二、安装MyCat
官网:http://www.mycat.io/
GitHub地址:https://github.com/MyCATApache/Mycat-Server
下载地址:http://dl.mycat.io/1.6.7.1/Mycat-server-1.6.7.1-release-20190627191042-linux.tar.gz
本次安装采用最新版本Mycat-server-1.6.7.4-release-20200105164103-linux.tar.gz,解压到如下路径即可。
本次安装路径为:/usr/local/mycat
1 | cd /usr/local/src/ |
MyCat 1.6.5-release和1.6.7.4-release都测试过了,在jdk1.8环境下都是没有问题的。
三、配置环境变量
1 | vim /etc/profile |
四、创建MyCat用户
1 | adduser mycat |
五、配置MyCat
本文以笔者之前开发的一个广告平台项目ArtarvaSSP为例。
1、schema.xml配置如下:
1 | <?xml version="1.0"?> |
这里主要是将ssp_ads_fail_log
表分离到一个独立的库db_ssp_log
中(放在dn2中),所以在schema中,单独声明了<table name="ssp_ads_fail_log" primaryKey="id" autoIncrement="true" subTables="ssp_ads_fail_log_2020_$1-52" rule="sharding-by-date" dataNode="dn2" />
,分片规则为按日期划分(见rule.xml文件),52周共计52张表($1-52)。由于db_ssp_log
库设置了独立的用户名密码,于是新建dn2。mycat_sequence
为全局自增表,主要用来接管数据库的自增字段,见文末附件。
2、server.xml配置如下:
1 | <?xml version="1.0" encoding="UTF-8"?> |
这里主要配置了连接MyCat的用户名密码,允许访问的数据库,多个时可以用逗号隔开。为了让之前的项目从MySQL连接无缝切换到MyCat,这里的用户名密码与MySQL保持一致,到时只需要在项目里修改一下连接端口为8066就可以了。MyCat的服务端口为8066,如设置<property name="serverPort">8066</property>
。
3、rule.xml配置如下:
1 | <?xml version="1.0" encoding="UTF-8"?> |
这里主要是按日期(7日)来切分数据表,最近7日的数据放在一个数据表,2020年全年52周零2天。
注意:开始日期与结束日期的天数必须能整除7,否则MyCat启动不了。
4、log4j2.xml日志配置:conf/log4j2.xml
文件,配置如下:
1 | <?xml version="1.0" encoding="UTF-8"?> |
建议在上线前期将日志级别修改成debug
,可以看到详细的SQL执行日志(如调度到了哪台主机),观察一段时间,方便排查问题。Mycat运行稳定后将级别再调整为 info/ware
(生产环境建议设置)。
六、启动MyCat
1 | su - mycat #切换到mycat用户 |
如果启动失败,可以查看logs
目录下的日志文件。
常用命令:mycat { console | start | stop | restart | status | dump }
附录
mycat_sequence
结构如下:
1 | CREATE TABLE `mycat_sequence` ( |
在插入记录前,通过Go查询表获取一个自增数字,如下:
1 | id, err := getMycatSequence("ADS_FAIL_LOG") //获取自增值 |
遇到的问题
1、通过Navicat连接到Mycat时,提示too many connections
,将连接数修改成1000,一会又出现前面的提示,通过show processlist;
,发现有大量的线程处于Waiting for table level lock
,执行的操作是往日志表ssp_ads_fail_log表(MyISAM存储引擎)中insert数据,由于日志信息是实时的,每次只插入一条。
在查询一些资料后,将问题定位到存储引擎的问题上来,主要是因为MyISAM是表级锁,频繁插入,锁的开销很大,MyISAM更适合批量插入场景,于是将存储引擎又改回InnoDB。
查看磁盘I/O繁忙程度的命令:
iostat -x 2 10
,查看最后几列数据。
命令可参考:https://www.cnblogs.com/cynchanpin/p/6936977.html
2、将MyISAM修改成InnoDB时,又遇到了新的问题,磁盘满了(数据库日志、项目日志、MyCat debug日志都挺多的,再加上备份数据库的包也挺多的)
于是开始清理磁盘,先找出前10个大文件du -m / | sort -n -r | head -n 10
,清理多余的文件,然后将表修改成InnoDB:
1 | alter table ssp_ads_fail_log engine="InnoDB"; |
再查看一下表的存储引擎是否已经修改成功:
1 | show table status from db_ssp_log\G; |
参考:
https://segmentfault.com/a/1190000014767902 雪花算法
https://tech.meituan.com/2017/04/21/mt-leaf.html Leaf——美团点评分布式ID生成系统
深入理解RabbitMQ的原理
什么是队列(queue)?
队列是一种存储、组织数据的数据结构,最大的特点就是先进先出(FIFO)。
什么是消息队列?
服务之间最常见的通信方式是直接调用彼此来通信,消息从一端发出后立即就可以达到另一端,称为即时消息通讯(同步通信);
消息从某一端发出后,首先进入一个容器进行临时存储,当达到某种条件后,再由这个容器发送给另一端,称为延迟消息通讯 (异步通信)。
而容器的一个具体实现就是MQ(Message Queue)。
可以通过小红和小明读书的故事来理解一下消息队列。
什么是RabbitMQ?
RabbitMQ是一个实现了AMQP(Advanced Message Queuing Protocol)高级消息队列协议的消息队列服务,用Erlang语言编写,Erlang语言是为电话交换机开发的语言,天生自带高并发光环,和高可用特性。
理解GO指针
为什么需要指针?
简单地来说,就是保证软件功能实现的同时,尽量的节约内存资源,提高程序运行效率。
当给一个函数/方法传参的时候,我们要了解传进去的是值还是引用地址。当参数为基本类型时(如string, bool, int 及 float),传进去的大都是值,也就是另外复制了一份参数到当前的函数调用栈;当参数为高级类型时(如struct,array/slice,map,chan,func),传进去的基本都是引用地址,这个主要是因为虚拟机的内存管理导致的。
内存管理中的内存区域一般包括 heap(堆)和 stack(栈), stack 主要用来存储当前调用栈用到的简单类型数据:string,bool,int,float 等,这些类型的内存占用小,容易回收,基本上它们的值和指针占用的空间差不多,因此可以直接复制,GC也比较容易做针对性的优化。 复杂的高级类型占用的内存往往相对较大,存储在 heap 中,GC 回收频率相对较低,代价也较大,因此传引用/指针可以避免进行成本较高的复制操作,并且节省内存,提高程序运行效率。
关于堆和栈不太理解的童鞋,可以阅读:堆和栈的区别
因此,在下列情况可以考虑使用指针:
1、需要改变参数的值;
2、避免复制操作;
3、节省内存。
一、定义指针
var ptr1 *int64
刚定义的指针没有指向任何地址,为nil,所以也叫空指针。
如图:
我们假设图片上表示的是一个内存块,存放有变量名(户口)和值(住几口人),以及它的内存地址(门牌号),指针可以理解成一个特殊的变量,它也需要占用内存,它的特殊之处在于,它的值必须且只能存放内存地址,假设它在内存中的地址为:0xabcd
。
二、使用指针
1 | var a int64 = 100 |
如图:
我们先定义一个变量a
,系统为变量a
分配了一个内存地址0x1234
,
通过 & 符可以将变量 a
的内存地址0x1234
取出来,并交给 ptr1
保存,于是ptr1
的值由空(nil
)变成了0x1234
,通过*ptr1
就可以展示刚才存入的内存地址了。
三、指向指针的指针
归根结底还是指针类型,但定义时多了个*
,例如:var ptr2 **int64
1 | var ptr2 **int64 |
如图:
接着前面的定义,我们定义了一个指向ptr1
的指针ptr2
,此时ptr2
的值为ptr1
的内存地址,通过&a, ptr1, *ptr2
都可以拿到内存地址0x1234
,通过a, *ptr1, **ptr2
都可以拿到值100
。
四、将指针作为函数参数
例如:我们用函数来实现交换a和b的值:
1 | var a,b int64 = 100,200 |
如图:
在swap
函数的形参中,我们定义了两个指针ptr1
、ptr2
分别用来接收a和b的地址,通过*ptr1
、*ptr2
分别拿到值进行交换。在Go语言中,交换值还有更简洁的操作:*ptr1,*ptr2 = *ptr2,*ptr1
,直接交换。
五、指针数组
说白了还是数组,只不过这个数组仅用来存放内存地址,也就有了一个书面术语——指针数组。
来看代码:
1 | var arr = [3]int64 {1,2,3} |
上面代码定义了一个指针数组ptrarr
,用于存放数组arr
中的每一个元素的内存地址,然后通过ptrarr
可以直接查看到值都是内存地址,通过*ptrarr[k]
就可以获取到arr
每一个元素的值。
有了指针,为什么还要有指针数组,因为数组(指针数组)在内存中的地址一般情况下都是连续分配的,比零散存放的变量(指针)查找存取更有效率,速度更快。
六、结构体指针
6.1 说白了还是指针,只不过这个指针是指向了一个结构体,
例如:
1 | type Person struct { |
从以上代码可以得出,定义结构体指针分四步:
1、定义结构体类型
2、定义指针
3、实例化结构体
4、指针指向结构体实例
与指向变量或数组的指针不一样的地方在于,多了一种读取方式:Go提供了一种隐式解引用特性,可以直接用
指针名.结构字段
的形式访问值,如上面代码:(*ptr1).name
或ptr1.name
都可以获取到值。
6.2 也可以将结构体指针作为函数参数
1 | func main() { |
总结
在学校学习C语言的时候,指针没学好,所以有些阴影,本文试着用简单的代码来理解GO的指针,希望给自己加深印象。不同于 C 语言,Golang 的指针是单独的类型,而不是 C 语言中的 int 类型,而且也不能对指针做整数运算。传递指针给函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力。