Tallate

该吃吃该喝喝 啥事别往心里搁

TALLATE / PERSONAL KNOWLEDGE INTERFACE

探索 Tallate

一个从后端系统走向 AI Agent 的工程师,正在思考、构建和记录什么?

参考

  1. 数据库范式简单讲解(1NF、2NF、3NF、4NF、BCNF)
  2. 缓存与数据库一致性之二:高并发下的 key 重建(先淘汰 cache 再写 db)的问题

由来

公司比较看重这批新员工(基本来自 985 院校),做了比较多的培训,从公司文化到衣着讲究都专门开了课,很切合应届毕业生的需求(强制参加的…)。
规范类似设计模式,是对最佳实践的总结,有点收获,这里把还记得的部分总结一下。

Java 编码规范

基本没听(其实是因为不知道下午要考试),内容和阿里 Java 规范很接近,有时间考个证书。

用户体验分享

这个大佬比较惨了,大家都是程序员、还都是偏后台的那种,讲课时间还是刚吃完饭那会,大家不是特别能接受,反正我是没听进去。

数据库规范

公司一个架构师大佬讲的,大部分内容仍然来自阿里 Java 规范,掺杂了一些干货。

遵守规范有什么好处

  1. 约定大于配置(这个理念是 SpringBoot 的基础),规范属于一种约定。
  2. 提高沟通效率
  3. 提升后续管理效率

基础

设置

  1. 使用 UTF8MB4。
  2. 注释很重要。
  3. 数据库内大小写不敏感,像 MySQL、Oracle 都是大小写不敏感的,如果出现敏感的情况注意设置成不敏感的。

命名

  1. 尽量取有意义的名字
  2. 如果有多个单词使用下划线分割
  3. 长度不要过长,如果设置得过长了,后面创建该表的备份表、表名末尾带上一些特殊的后缀,可能会超出数据库对表名长度的限制。

设计

  1. 尽量达到 3NF
    达到 1NF 和 2NF 是理所当然的,BCNF 又太严格了,会把表结构拆得过碎了,由此引入的多表关联会严重影响 DCL 效率,4NF 更是会把表结构拆稀碎了,达到 3NF 是比较理想的,但是不排斥使用 NoSQL 后产生的冗余。
  2. 逻辑删除而不是物理删除
    好处是可以反悔,坏处是表里数据会多很多。
    但是实践表明删除是很少被使用的,换句话说,被删除的数据相对总数来说是很少的,比如说我们把商品加到购物车里一般都会买掉而不是删除了,因此我们更倾向于使用逻辑删除。当然也不能排除有些业务删除是常用的,那就另说了。
    补充:其实我觉得完全可以另开一个表来存被逻辑删除的数据,我在毕设中就是这么做的。
  3. 为数据加上时间戳
    加上时间戳的主要目的是实现数据的增量同步,比如将数据同步到搜索系统(一般是 Elasticsearch)中,全量同步是无法忍受的。
    补充:时间戳还可以当作乐观锁。
  4. 合理利用 char
    varchar 能满足大部分用到字符串的场景,但是 varchar 的效率是不如 char 的,对于手机号、身份证号这种固定长度的属性完全可以使用 char 类型保存。
  5. 复杂的处理逻辑放到应用层而不是持久化层,不要用触发器、存储过程、视图,因为在云计算环境下应用层的扩展成本要远低于持久化层。
    补充:视图其实还是可以用一下的,比如给另一个部门查点数据,又不会暴露数据库的真正结构。

补充:数据库设计理论

元组/行:一个持久化对象。
属性/字段/列:某类持久化对象的属性,属性集一般使用 U 表示。
超键:能唯一标识一条元组的属性集。
候选键:不包含多余属性的超键。
主键:被用户选作元组标识的候选键。
主属性:构成候选键的属性。
函数依赖(FD):由业务确定的属性之间的依赖关系,即由一组属性可以唯一确定另一组属性。
关系模式(R(U)):指在属性集中存在的所有函数依赖。
平凡函数依赖:在 R(U)中存在 X→Y,若 Y 包含于 X,则称 X→Y 为平凡函数依赖。
完全依赖:在 R(U)中,如果 X→Y,并且对于 X 的任何真子集 X’都有 X’-/->Y’,则称 Y 完全依赖于 X,记作 X→Y。比如确定一批书的印刷时间需要给出版次和印次,不管是单独给出版次或印次都无法确认这批书。
部分依赖:与完全依赖相反,如果 X→Y,且 X 中存在一个真子集 X’,使得 X’→Y 成立,则称 Y 部分依赖于 X。比如学号和姓名可以确定一个学生,但其实学号就可以确定一个学生了。
传递依赖:指的是形如 X→Y→Z 的函数依赖。
多值依赖(MVD):原始定义比较难理解,用一个特殊情况来说明,就是在同一个表里存在 X:Y、X:Z 分别是 1 对多关系,即有 Y→X、Z→X,比如[课程, 学生, 先修课]是一张表,其中课程对学生、课程对先修课都是 1 对多关系。
1NF:不能有复合字段(不可分的原子值)。
实现方法:如果有,则拆成多个字段。
例子:一个人可能有多个联系电话,比如手机号、固话等,应该被拆分到多列内,或者多行保存同一人的多个电话。

身份证号 姓名 手机号 固定电话
3303012313 18012341234 123-12341234
身份证号 姓名 手机号
3303012313 18012341234
3303012313 123-12341234

缺陷:
2NF:实体属性完全依赖于主键、不能仅依赖主键的一部分属性(严格地说,非主属性完全依赖于候选键)。
实现方法:如果存在对主键的部分依赖,则拆出来作为一个新实体。
例子:
缺陷:
3NF:非主键属性不能间接依赖于主键(严格地说,不能有非主属性对候选键的传递依赖)。
实现方法:如果存在上述传递依赖,则将依赖的两个非主属性拆出成为新表,原表和新表之间是 1 对多的关系。
例子:
缺陷:
BCNF:消除任何属性对候选键的传递依赖(在 3NF 的基础上,不允许候选键属性传递依赖于候选键的属性)。
实现方法:同 3NF。
例子:
缺陷:
4NF:消除非平凡且非 FD 的多值依赖。
实现方法:
例子:
缺陷

SQL 编写

  1. 明确指出所有列,不要省略或使用通配符,比如:
    1
    2
    select * from t;
    insert into t values('1', 2);
  2. 连接操作给出表的别名,否则容易混淆

性能优化

  1. 分库分表
    将数据从一张表拆到多张表,后续再将这些表转移到多个数据库实例内,一般情况下可以减少单个数据库的压力,提升整体性能。常见的拆库方法如:
    • 按业务纬度分表(垂直拆分):将负责不同业务的表拆到不同库中。
    • id hash(水平拆分):对业务数据 id 求模 hash(id) % n,hash 函数是自定义的,如果 id 是数值可以直接返回 id 本身,如果是字符串可以取前几位的和(遍历每一位太慢了),n 表示我们需要将数据拆到几张表里。
      日期 hash(水平拆分):将不同月甚至日的数据分散到不同的库中。
      特殊 hash(水平拆分):按照某个特定的字段求模,或者根据特定范围段分散到不同的库中。
    • 读写分离:将数据拷贝到多个读库和写库内,读库加上索引,而写库不需要,这样,当然写库还需要通过某种机制将数据增量更新到读库内。
    • 冷热分离:将冷数据分离出去,比如微信聊天中 3 天前的数据就可以算作冷数据,这部分数据比较少用到,分离出去后可以提高查询最近聊天数据的响应效率。
  2. NoSQL
    NoSQL 常用于冗余数据,可以避免复杂的关联查询。
  3. 索引
    索引可以避免全表扫描,正确使用索引可以大大提高查询效率,但要注意函数可能导致索引失效,分析 SQL 语句问题的主要工具是执行计划
  4. 分页
    • 数据库分页优于内存分页
    • 分页前最好先过滤
    • order by 子句中最好不要使用会重复的字段(比如 name 就有可能重名)
    • 海量数据下可以只现实前 500 条数据,而不显示总条数和页数(百科和 Google 搜索都是这么做的)。

补充:缓存命中率和缓存淘汰

缓存命中率 = 查询命中数 / 总查询数
缓存命中率是度量缓存有效性的常用参数,为了提高缓存命中率,最好对不变数据进行缓存。
缓存淘汰是在服务器内存不够用时触发的,将一些价值较小的数据从缓存中淘汰,减少不必要的 swap,提升内存的有效利用率。
缓存淘汰还有一个问题:是先写入还是先淘汰呢?
首先看先写入再淘汰的情况,如下图所示。
X
我们交换操作 3 和 4 的顺序,第一步数据库操作成功,第二步如果淘汰缓存失败,则会出现 DB 中是新数据,Cache 中是旧数据的情况,即产生了数据不一致的问题。
然后是先淘汰再写入的情况,同样如上图所示。
写和读操作是并发执行的,应用实例 1 首先发起对缓存的淘汰,应用实例 2 读缓存 miss,于是从数据库读,而应用实例 1 这才将新值写入数据库,此时实例 2 得到的数就是脏数据了,注意在分布式环境下这种执行顺序完全是有可能的。

补充:执行计划

操作规范

  1. 变更需要明确范围
  2. 为上线预留时间和空间
  3. 给最专业的人进行评估
  4. 错峰操作

考试

考试题

一、单选题(每道题 2 分,共 12 分)
1、现有一张系统用户操作日志表,合理命名应该是( )
A、XTYHRZB
B、SysUserOpLog
C、Sys_User_Oplog
D、SYS_T_USER_OPLOG

2、下面数据库设计不符合规范的是( )
A、大数据量表进行分库分表
B、为了方便查看,表中使用明文存储用户密码
C、使用数字格式存储时间
D、将 json 格式数据存储到 Nosql 数据库

3、下面 SQL 编写不符合规范的是( )
A、select column1, column2 from table1
B、select column1, b.columna from table1, table2 b where column1 = b.columna
C、insert into table1(column1,column2) values(?,?)
D、update table1 set column1 = ‘aaa’ where id = ‘001’

4、下面关于性能优化不正确做法的是( )
A、尽量避免一条 SQL 从大于 4 张表中取数
B、如果一条 SQL 有三层以上的嵌套查询,在设计上考虑解决
C、为了提升代码执行效率,大量的公式运算交给数据库后端运行
D、为常用的分组排序字段建立索引

5、下面不正确的索引建立方法的是( )
A、业务表唯一标识字段必须制定唯一索引
B、只要是查询可能用到的字段,都要建立索引
C、尽量使用数据量少的索引
D、索引列上不使用函数计算

6、下面数据库操作符合规范的是( )
A、为了方便,应用程序使用超级管理员账号
B、为了快速解决问题,直接在后端数据库中修改业务数据
C、修改表结构告知影响人员,并预留一定时间评估、审核
D、程序测试完成后直接上线新功能
二、多选题(每道题 6 分,共 60 分)
1、关于领域模型命名,下列哪些说法符合规范:( )
A、数据对象命名:xxxDO,xxx 即为数据表名,例如:ResellerAccountDO。
B、数据传输对象:xxxDTO,xxx 为业务领域相关的名称,例如 ProductDTO。
C、展示层对象:xxxVO,xxx 一般为网页名称,例如 RecommendProductVO。
D、POJO 是 DO/DTO/BO/VO 的统称,命名成 xxxPOJO。

2、关于代码注释,下列哪些说法符合规范:( )
A、所有的抽象方法(包括接口中的方法)必须要用 javadoc 注释。
B、所有的方法,包括私有方法,最好都增加注释,有总比没有强。
C、过多过滥的注释,代码的逻辑一旦修改,修改注释是相当大的负担。
D、我的命名和代码结构非常好,可以减少注释的内容。

3、关于类命名,下列哪些说法符合规范:( )
A、抽象类命名使用 Abstract 或 Base 开头。
B、异常类命名使用 Exception 结尾。
C、测试类命名以它要测试的类的名称开始,以 Test 结尾。
D、如果使用到了设计模式,建议在类名中体现出具体模式。例如代理模式的类命名:LoginProxy;观察者模式命名:ResourceObserver。

4、关于加锁,下列哪些说法符合规范:( )
A、可以只锁代码区块的情况下,就不要锁整个方法体。
B、高并发的业务场景下,要考虑加锁及同步处理带来的性能损耗,能用无锁数据结构,就不要用锁。
C、能用对象锁的情况下,就不要用类锁。
D、加锁时需要保持一致的加锁顺序,否则可能会造成死锁。

5、关于方法的返回值是否可以为 null,下列哪些说法符合规范:( )
A、方法的返回值可以为 null,如果是集合,必须返回空集合。
B、方法的返回值可以为 null,不强制返回空集合,或者空对象等。
C、方法实现者必须添加注释,充分说明什么情况下会返回 null 值。
D、防止 NPE 是调用者的责任。

6、关于控制语句,下列哪些说法符合规范:( )
A、推荐 if-else 的方式可以改写成卫语句的形式。
B、尽量减少 try-catch 块内的逻辑,定义对象、变量、获取数据库连接等操作可以移到 try-catch 块外处理。
C、if ( condition) statements; 单行语句不需要使用大括号。
D、在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有。

7、关于捕获异常和抛异常,下列哪些说法符合规范:( )
A、如果需要捕获不同类型异常,为了方便处理,可以使用 catch(Exception e){…}。
B、不要捕获异常后不处理,丢弃异常信息。
C、捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。
D、异常定义时区分 unchecked / checked 异常,避免直接使用 RuntimeException 抛出。

8、关于变量和常量定义,下列哪些符合规范:( )
A、Long a=2L;//大写的 L。
B、Long a=2l; //小写的 l。
C、常量只定义一次,不再赋值,所以不需要命名规范。
D、不要使用一个常量类维护所有常量,应该按常量功能进行归类,分开维护。

9、数组使用 Arrays.asList 转化为集合,下列说法哪些正确的:( )
A、数组元素的修改,会影响到转化过来的集合。
B、数组元素的修改,不会影响到转化过来的集合。
C、对于转换过来的集合,它的 add/remove/clear 方法会抛出: UnsupportedOperationException。
D、Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。

10、关于并发处理,下列哪些说法符合规范:( )
A、线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
B、同步处理时,能锁部分代码区块的情况下不要锁整个方法;高并发时,同步调用应该考虑到性能损耗。
C、创建线程或线程池时,推荐给线程指定一个有意义的名称,方便出错时回溯。
D、推荐使用 Executors.newFixedThreadPool(int x)生成指定大小的线程池。

三、问答题(4 道题,共 28 分)
1、人力云(HR)业务需要建立人员表(含姓名、年龄、性别)和人员工作情况表(含工作开始时间、工作结束时间、工作单位、工作简介),请写出具体的建表语句。(8 分)

2、请列举你所了解的分库分表方式。(6 分)

3、现有业务需求要修改表结构,开发库已经修改测试完成,需要在生产库同步修改并发布新程序,请问你具体的步骤。(5 分)

4、现在有一张表 tablea,主键 key1,只有主键含有唯一索引,字段 columna,columnb,columnc,数量级 100 万,现在按照主键升序,每页 100 条分页,请写去取出第 900000 到 900100 条数据的 SQL 语句。(9 分)

答案&分析

一、单项选择
1.D 数据库大小写不敏感,应该全部用大写
2.B 选项 C 时间用数字格式存储,可以方便最后的国际化时区转化
3.B 连接语句要对每个表给出别名,对每个字段的使用都要带上表别名,否则有子查询的情况下,语句可能出现执行计划错误,甚至结果集错误。
4.C 选项 B 建议使用 NoSQL 缓存或其他可以在架构上优化的措施来减少复杂的数据库操作;选项 C 公式运算容易使得索引失效因此不建议在数据库中使用;选项 D 中的建议是因为 order by 和 group by 会使用到索引。
5.B 选项 B 索引是有代价的,对于一些值重复性较大的列最好不建索引;选项 C 也是考虑在写操作时更新索引会拉低性能。
6.C 操作一般是越谨慎越好,数据库操作最好交给专业人员来做(DBA),选项 B 是为了避免数据库出问题时互相甩锅。

二、多项选择
1.ABC 选项 C 我似乎没选,因为把 VO 当成 DO 来用了,而且现在开发都是前后台分离,很少用到页面模板;选项 D 中 POJO 确实是 DO/DTO/BO/VO 的统称,但不能把所有都命名成 XxxPOJO,会分不清该类的职责。
2.ACD 一般接口和抽象类里的抽象方法需要加上注释,而类里面的私有方法没有这个要求,也要避免注释过多的情况。
3.ABCD
4.ABCD 使用锁时要考虑锁的粒度、性能等特性,同时应该避免死锁等问题。
5.BCD 我和大部分人一样非常厌恶方法返回 null,尽量返回空集合或空对象能减少很多烦恼,但是手册中认为方法可以返回 null,防止 NPE 是调用者的责任,我觉得原因很大程度上是因为难以避免远程调用失败、序列化失败、运行时异常等场景返回 null 的情况。
6.ABD 选项 A 卫语句指的是处理完一个分支后直接返回;选项 B 强调我们分清稳定代码和非稳定代码,不要将不会出错的稳定代码也加到 try 中,这是不负责任的。
7.BCD 异常最好有业务含义,能提高后期排错效率。
8.AD 选项 B 不规范是因为 l 和 1 容易混淆。
9.ACD 最好看一下 Arrays.asList()源码。
10.ABC FixedThreadPool 不推荐使用,因为其允许的请求队列长度为 Integer.MAX_VALUE ,可能会堆积大量的请求,从而导致 OOM 。

三、简答题
第一题:表名和字段名按规范,表名前缀 HR,使用有意义的单词,单词间用_分割,有中文注释。
第二题:读写分离,冷热分离,按照特定业务纬度分表,使用 ID Hash 取余平均分表等。
第三题:
1、明确影响范围
2、预留应变空间时间
3、交由专家评估合理性和性能
4、脚本交由专人在生产库执行,记录;
5、错峰操作。
第四题:参考规范 3 性能优化 22 ,或者先查询 id,在根据 id 查询数据等都可。
补充:后一个方法为什么可行呢?因为规范要求所有删除都是逻辑删除,能保证库里的数据 id 是从 1 到 1000000 排列的。

反思

有的题目比较抠细节,但是上课好好听应该不难。
大部分规范都取自阿里 Java 编码规范,但很多数据库优化策略很有见地,在日后多作总结。

之前一直在用的老 SSD 只有 128GB,上边装了 Ubuntu 作为宿主 OS,另外常备一个 Windows7 的虚拟机,时常面临空间不够的问题,一般是用 Vmware 中 Hard Disk 的Compact Disk功能来挤出一些空间来,但是这样频繁的硬盘读写对硬盘寿命多少有些影响。
后来换新机时自带的 SSD 是 256GB 的,当时出于方便不想重装环境就用老的 SSD 替换了下来,现在抽出一块时间把数据都转移到新硬盘上,尽量不影响平时的工作。

阅读全文 »

前言

该项目是 2017 年参加学院组织的实训项目期间同小组成员开发的,是一个简化版的电商项目。

涉及到的技术

  1. 掌握高并发站点的系统架构方法。
  2. 熟悉 linux 开发环境,掌握 tomcat、mysql 服务器集群的安装与配置。
  3. 理解 web 服务器的负载均衡原理,掌握配置方法。
  4. 掌握数据库主从复制,读写分离技术。
  5. 理解缓存原理,掌握缓存在程序开发的原理。
  6. 掌握 web 站点的动静分离技术。
阅读全文 »

概念

Git 中有 4 个区域:

  • 工作区( Working Area )
  • 暂存区( Stage )
  • 本地仓库( Local Repository )
  • 远程仓库( Remote Repository )

文件有 5 种状态:

  • 未修改( Origin )

  • 已修改( Modified )

  • 已暂存( Staged )

  • 已提交( Committed )

  • 已推送( Pushed )

  • 文件刚开始处于工作区,编辑文件后可以使用git diff查看文件进行了哪些修改

  • add 命令可以添加到暂存区,可以使用git diff --cached查看文件在暂存区和本地仓库之间的区别

  • commit 提交到本地仓库,可以使用git diff master origin/master查看本地仓库分支和远程仓库分支之间的区别
    注意,Git 保存的不是文件差异或者变化量,而只是一系列文件快照,并使用一个 commit 来指向它们。

  • push 将本地仓库的代码提交到远程仓库

索引

分支

命令

status

1
$ git status -s # 短版status

diff(比较文件)

1
2
3
4
git diff --cached # ,同--staged
git diff branch1 branch2 --stat //显示出所有有差异的文件列表
git diff branch1 branch2 文件名(带路径) //显示指定文件的详细差异
git diff branch1 branch2 //显示出所有有差异的文件的详细差异

log(显示提交历史)

对比分支差异
列出最近两次提交引入的更改(-p 相当于 diff 命令)

1
$ git log -p -2

其他选项

1
2
3
4
5
6
$ git log --stat # 列出每次提交的state(相当于加state命令)
$ git log --pretty=??? # 以某种格式化输出log
$ git log --since=2.weeks # 打印最近2周的log
$ git log -S function_name # 过滤出对function_name字符串有修改的commit,另外在log最后加--file,可以限制只打印对file有修改的commit
--decorate:在结果中显示当前所处的branch
--graph:以更直观的方式区别不同的branch

打印出炫酷的效果:

1
$ git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative

ignore

.gitignore 文件中的规则匹配的文件会被 git 忽略,但是文件事先要从索引中删除。

config

1
2
3
$ git config --global credential.helper cache # 设置凭证缓存,以后就不用每次都输入密码了,但默认只保存15分钟
$ git config credential.helper 'cache --timeout=3600' # 设置凭证保存一个小时
$ git config --global credential.helper store # 长期存储

add(建立索引)

1
git add .

unmodifying(已修改->未修改)

1
2
3
git checkout .
$ git checkout -- <file>... # 若已tracked的文件被修改,可以使用这条命令恢复到未修改时的情况
git reset --hard

commit(提交)

1
2
$ git commit -a # 自动add并提交追踪过的所有文件
$ git commit --amend # 覆盖上一次的commit,比如可以修改commit message,也可以删除一些文件的索引后再次进行提交,在误提交一些配置文件后可以这样挽救

unstage(撤销 add,已暂存->已修改)

1
2
3
$ git reset HEAD <file>... # 撤销上一次的add
$ git rm -rf --cached [文件] # 仅删除索引,不删除,-r递归,-f强行移除
$ git rm -rf [文件] # 从工作目录和跟踪列表中删除

tag

1
2
3
4
5
6
7
8
$ git tag # 显示所有标签
$ git tag -a v1.4 -m "my version 1.4" # 给最近一次的commit加一个注释标签(相当于一个节点)
$ git show <tag-name> # 显示tag和被tag的commit的信息
$ git tag v1.0-lw # 加一个轻量标签,仅仅在文件中记录最近一次commit的checksum
$ git tag -a v1.2 <checksum> # 在后面指定某个commit的完整(或部分)checksum也可以加一个标签,checksum可以使用$ git log --pretty=oneline得到
$ git push origin [tagname] # 将某个标签推送到远程服务器中
$ git push origin --tags # 将所有标签推送到远程服务器
$ git checkout -b [branchname] [tagname] # 转换到另一branch

aliase

1
2
3
4
5
6
$ git config --global alias.co checkout
$ git config --global alias.br branch
$ git config --global alias.ci commit
$ git config --global alias.st status
$ git config --global alias.unstage 'reset HEAD --'
$ git config --global alias.last 'log -1 HEAD'

以后就可以使用 git ci 来 commit 了

1
$ git config --global alias.visual '!gitk'

为自己写的工具设置别名

push

1
2
3
4
$ git push origin serverfix:awesomebranch # 将serverfix代表的分支推到远程origin的awesomebranch分支上
$ git push origin --delete serverfix # 删除服务器上的一个分支
$ git push --set-upstream origin master # 将当前分支的提交上传到远程仓库的某个分支并与其建立跟踪
$ git push -u origin master # 同上

remote

1
2
3
4
5
6
7
8
9
$ git remote # 显示远程服务器名,默认赋名origin
$ git remote -v # 显示需要从该远程主机读/写时可以使用的URL
$ git remote add <shortname> <url> # 添加一个可用的远程仓库
$ git fetch [remote-name] # 获取远程仓库有而本地仓库没有的信息,但不merge
$ git pull # fetch并merge
$ git push [remote-name] [branch-name] # 向有写权限的主机push数据
$ git remote show [remote-name] # 显示远程仓库的详细信息
$ git remote rename [old] [new] # 重命名仓库的shortname
$ git remote rm [shortname] # 移除远程仓库

branch

1
2
3
4
5
6
$ git branch -v # 显示每个branch的最后一个commit
$ git branch --merged # 过滤branch,只显示并入到当前branch的branches,同样的还有--no-merged
$ git branch -u origin/serverfix # 改变当前分支track的上流分支
$ git branch -vv # 查看每个本地分支track的服务器分支(只和最近一次fetch比较)
$ git branch -r # 查看远程分支列表
$ git branch -D [branch] # 强行删除分支,不管分支有没有被并进来

checkout

1
2
3
4
5
$ git checkout -b [branch] [remotename]/[branch] # 在本地创建一个新分支,与远程的一个分支同步
$ git checkout --track origin/serverfix # 上面的缩写
$ git checkout serverfix # 上面的缩写
$ git checkout -b sf origin/serverfix # 如果将要创建的本地分支与所要track的远程分支不同名
撤销文件的修改(还未add的文件)

rebase(衍合)和 merge(合并)

  1. 实现方式:rebase 和 merge 都是用来在推送之前整合提交历史的,但是实现的方式却不同,rebase 先找出两个分支的公共祖先,然后将一个分支的从该节点之后的所有提交都当成补丁应用到另一个分支上;merge 同样是先找到分支的公共祖先,然后对两个分支的最新提交进行合并???,将合并中修改的内容生成一个新的 commit。
  2. 冲突解决:rebase 和 merge 在实施过程中都有可能会出现冲突的情况,merge 遇见冲突后会直接停止,等待手动解决冲突并重新提交 commit 后,才能再次 merge;而 rebase 遇见冲突后会暂停当前操作,开发者可以选择手动解决冲突、add 更新索引,然后 git rebase –continue 继续,或者 –skip 跳过(注意此操作中当前分支的修改会直接覆盖目标分支的冲突部分),亦或者 –abort 直接停止该次 rebase 操作。
  3. pull 命令其实相当于 fetch+merge,可以通过在后面加上 --rebase 选项来指定为 rebase 操作,或者先 fetch,之后再考虑使用 merge 还是 rebase。
  4. 不要 rebase 已经公开的提交对象,因为它会舍弃当前分支已经提交的 commit,对别人来说就像该分支被回滚了一样,如果 pull 了可能就会出问题。
    https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging#Basic-Merge-Conflicts
  5. 当有修改未 commit 时,不能进行 rebase 操作,此时可以考虑先用 git stash 命令暂存;但是 merge 可能会直接将未 commit 的内容覆盖掉。
    1
    2
    3
    4
    5
    6
    $ git rebase b # 嫁接过去,将当前分支里的补丁应用到目标分支的最后一个提交对象上,也就是在目标分支的最后一个commit的基础上重演一遍修改,最后将当前分支的???指过去
    $ git rebase --onto master server client # 将分支server上的另一分支client衍合到master上
    $ git rebase master server # 将server分支衍合到master中来
    $ git rebase -i b # 显示详细信息,包括各commit会被连接到目标分支的哪个commit后面,并且可以指定命令(策略pick、reword、edit、squash、fixup、exec)???????,默认为pick
    $ git merge b # 将目标分支合并进来
    $ git merge --ff-only # ff的意思是fast-forward,这种情况下要么当前分支已经是最新的了、要么合并是可以fast-forward的(只移动分支指针),默认情况下是--no-ff,也就是普通的合并

stash

1
2
$ git stash apply # 使用最近一次的stash记录来还原当前分支
$ git stash clear # 清空stash表

reset

统计代码量

统计每个人的代码量:

1
git log --format='%aN' | sort -u | while read name; do echo -en "$name\t"; git log --author="$name" --pretty=tformat: --numstat | awk '{ add += $1; subs += $2; loc += $1 - $2 } END { printf "added lines: %s, removed lines: %s, total lines: %s\n", add, subs, loc }' -; done

统计某个人某段时间内的代码量:

1
2
3
4
5
6
git log --since='2019-01-15' --until='2019-12-31' --format='%aN' | sort -u |
while read name; do echo -en "$name,";
git log --since='2019-01-15' --until='2019-12-31' --author="$name" --numstat
--pretty=tformat: --no-merges | awk '{ add += $1; subs += $2; loc += $1 - $2 }
END { printf "added lines, %s, removed lines, %s, total lines, %s\n", add, subs,
loc }' -; done >> XXX_19_12_31_code.csv;

回滚

Git 支持对本地代码库或远程代码库回滚。
参考:git 远程分支回滚

工作流

首先 master 分支即发布的版本,在此上分出一个 develop(huang)版本,每次发布任务时,在 develop 的基础上分出 liu、du、zhou、shen,完成各自的任务,完成后合并到 develop 上。
若 master 上出 bug 了就直接分出一个 hotfix 分支,修复后马上合并到 master 和 develop 上。
开发完成后(即 liu、du、zhou、shen 等的分支都已合并进 develop)在 develop 上分出一个 release,进行测试,最后合并到 master 上。

参考

  1. git-for-computer-scientists
  2. git - Documentation
  3. 常用.gitignore
  4. Git 少用 Pull 多用 Fetch 和 Merge
  5. 闲谈 git merge 与 git rebase 的区别
  6. 解决git误commit大文件导致不能push问题

一、参考

搭建博客

  1. 使用 Github 空间搭建 Hexo 技术博客–安装篇(基于 IntelliJ IDEA)
  2. GitHub Pages

hexo 使用

  1. hexo-指令
  2. NexT 常见问题
  3. hexo 搭建个人博客–基础篇
  4. hexo 搭建个人博客–NexT 主题优化
  5. hexo 搭建个人博客–SEO 和站点加速

在 GitHub 上写博客

  1. Writing on GitHub
  2. hexo 摸爬滚打之进阶教程
  3. hexo 分类与 tags 配置
  4. Emoji 表情
  5. 使用七牛云作为图床获取外链方式总结

添加持久链接功能

  1. hexo 链接持久化终极解决之道

添加评论功能

  1. Hexo NexT 主题中集成 gitalk 评论系统

添加网站统计功能

  1. 不蒜子
  2. Hexo+Next 主题 文章添加阅读次数,访问量等(需要额外添加不蒜子脚本
  3. Hexo 添加字数统计、阅读时长、友情链接

添加看板动画

  1. hexo 添加 live2d 看板动画

添加复选框样式

  1. 用 VPS+hexo 搭了个博客,如何让 hexo 支持复选框
  2. hexojs/hexo-renderer-marked

数学公式

  1. 在 Hexo 中渲染 MathJax 数学公式
  2. MathJax
  3. LaTEX:在线数学公式编辑器
  4. TEX Commands available in MathJax

markdown 格式化

  1. hustcc/lint-md

二、博客搭建

流程

  1. 创建 GitHub Pages 仓库
  2. 安装 hexo 博客框架
  3. 初始化 hexo 项目
  4. 提交 hexo
  5. 配置 hexo

创建 GitHub Pages 仓库

在 GitHub 上创建username.github.io 的一个项目,username 为 github 上的用户名,该仓库将成为 hexo 打包后的发布地址。

安装 hexo

1
2
3
4
5
6
7
8
9
10
11
12
# 更新node版本,需要在root下运行
npm install -g n
n stable
# 使用cnpm安装hexo
alias cnpm="npm --registry=https://registry.npm.taobao.org \
--cache=$HOME/.npm/.cache/cnpm \
--disturl=https://npm.taobao.org/dist \
--userconfig=$HOME/.cnpmrc"
cnpm install -g hexo-cli
# 遇到“Node Sass does not yet support your current environment:”的问题,需要重装node-sass模块
npm uninstall --save node-sass
npm install --save node-sass

初始化 hexo 项目

1
hexo init

创建测试页面。

1
hexo new test

本地测试。

1
2
3
hexo clean
hexo generate
hexo server

打包提交

1
2
3
hexo clean
hexo generate
hexo deploy

hexo升级

https://theme-next.js.org/docs/getting-started/upgrade.html

配置 hexo

主要是对项目下名为_config.yml的配置文件的修改。

博客中的图片显示

刚开始希望把图片直接放到 hexo 项目里,但hexo g时总是出现莫名其妙的错误,图片也没有被拷贝到 public 目录下面,部署后当然看不到。
后来希望使用图床来解决问题,需要有一个图床的客户端,比如极简图床,还需要一个存储后端,常用的如微博图床或七牛,申请七牛还需要验证身份这个过程有些麻烦,微博图床据说也不稳定,有一种图床是专业的图床既提供了前端又自带了后台存储,如sm.ms,但是怎么可能永远免费,想来想去还不如自己搭建一个简单的图片服务器。

  1. 先租一台服务器
  2. 将图片拷贝到服务器上
    1
    scp -r local_path user@address:/server_path
  3. 部署 web 服务器
    如果像我一样懒,可以直接跑一个容器当 web 服务器,nginx 作为静态 web 服务器有很高的性能,以后也可以根据需要做一些其他配置,在服务器上执行下面命令运行一个 nginx 服务器:
    1
    docker run -p 80:80 -v /path/to/resources:/usr/share/nginx/html -v /path/to/logs:/var/log/nginx nginx
  4. 备份
    以防万一,最后在 github 留了一份备份,这样一份图片就有三份备份了。
  5. 注意
    如果图片访问403了,很有可能是权限不够,因为默认ng配置里是通过nginx用户来读取本地路径下的图片的,

    默认ng配置可以通过docker exec <container ID> -it /bin/sh连到容器里查看/etc/nginx/nginx.conf,第一行可以看到配置user nginx;

上边的工作不需要太多时间,以后需要同步的话也比较方便。

HTTPS

https://tallate.github.io/73d751b6.html

三、博客内容构建

  1. categories 和 tags
    hexo 里一篇博客有 categories 和 tags 这两个属性用于对博客进行归档,有时候这两个东西的作用容易搞混,按我的理解, categories 更像是书前面目录的章节, tags 像是书后面对关键词的索引,比如目录中有一章专门讲 Java 技术栈的内容, category 就是 《 Java 》,同理可能还有《 Linux 》,这两章都讲了叫作 Semphore 的东西,所以他们都包含一个叫 Semphore 的 tag ,当然如何划分总归还是要靠个人意愿。
  2. 文章频率
    博客并不是新闻那样的实时内容平台,而是重在积累,两篇相邻的文章之间横跨好几个星期甚至几个月也很正常,如果非常频繁的发博客反而应该怀疑内容的可靠性。

内存池

buffer/cache/pool

cache(高速缓存)是一种介于寄存器和内存之间的存储器,访问速度也在他们二者之间,当用户程序请求存储空间时,会先尝试从 cache 获取,当找不到时再到内存中找,并且找到的页及其临近的几页都会被拿到 cache 中去以备下次访问。
而 buffer 出现的地方比较多,比如 java4 引入的 NIO 中就有一个 Buffer,它的主要作用是缓冲从 Channel 中取到的数据,服务器接收到网络请求时从准备到开始接收数据是有一定的时延的,当出现大量的小请求时,很容易造成资源的浪费,所以引入了缓冲区。
pool 常出现在一些框架中,常见的有数据库连接池、内存池,它和 buffer 有些区别,pool 中的对象往往可以重复利用。

malloc/free

通过调用 malloc 和 free 函数可以实现在堆上的对象分配,这是 C 语言标准库提供的最基本的手动管理内存分配的方法,但是 malloc/free 实现的其实是最基本的 指针碰撞(Bump the Pointer) 内存分配方式,每次分配时都会从空闲块链表表头开始搜索,找到足够大的一块就返回,释放时同样是在空闲块链表中遍历,找到它的分配位置,再和前后两块合并。

显然,这种方式存在一些缺陷:

  • 大量分配后空闲块链表变得很长(时间开销)
  • 产生大量内存碎片(空间开销)
  • 使用麻烦,容易造成内存泄露(申请后忘了释放)

下面是一种 malloc/free 的实现:

折叠/展开代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
typedef long Align; //最受限类型

union header{ //块头部
struct{
union header *ptr; //空闲块链表的下一块
unsigned size; //本块的大小
}s;
Align x; //强制块对齐,即强制header最小不小于Align
};
typedef union header Header;
/******************************************/
#define MAXBYTES (unsigned)10240

static Header base;//链表
static Header *freep = NULL;//空闲链表的初始指针

/* malloc函数:存储分配 */
void *malloc(unsigned nbytes){
Header *p, *prevp;
Header *morecore(unsigned);
unsigned nunits;

//超出可分配字节数上限
if(nbytes > MAXBYTES){
fprintf(stderr, "alloc: can't allocate more than %u bytes\n", MAXBYTES);
return NULL;
}

//需要分配的块数
nunits = (nbytes + sizeof(Header) - 1) / sizeof(Header) + 1;
//空闲链表不存在
if((prevp = freep) == NULL){
base.s.ptr = freep = prevp = &base;
base.s.size = 0;
}
for(p = prevp->s.ptr; ; prevp = p, p = p->s.ptr) {
//足够分配
if(p->s.size >= nunits){
if(p->s.size == nunits){
prevp->s.ptr = p->s.ptr;
}
else{
//如果找到的块太大,则分配其末尾部分
p->s.size -= nunits;
p += p->s.size;
p->s.size = nunits;
}
freep = prevp;
return (void*)(p + 1);
}
//闭环的空闲链表
if(p == freep){
if((p = morecore(nunits)) == NULL){
return NULL;//系统无剩余存储空间
}
}
}
}

#define NALLOC 1024 //最小申请单元数
static unsigned maxalloc; //当前已分配的最大内存块的长度
/* morecore函数:向系统申请更多的存储空间 */
Header *morecore(unsigned nu){
char *cp, *sbrk(int);
Header *up;

if(nu < NALLOC){
nu = NALLOC;
}
cp = sbrk(nu * sizeof(Header));
if(cp == (char*) - 1){ //没有空间
return NULL;
}
up = (Header*)cp;
up->s.size = nu;
maxalloc = (up->s.size > maxalloc) ? up->s.size : maxalloc;
//把多余的存储空间插入到空闲区域
free((void*)(up + 1));
return freep;
}

/* free函数:将块ap放入空闲块链表中 */
void free(void *ap){
Header *bp, *p;

bp = (Header*)ap - 1; //块头
//欲free的内存块长度不能等于0或者大于当前已分配的最大内存块长度
if(bp->s.size == 0 || bp->s.size > maxalloc){
fprintf(stderr, "free: can't free %u units\n", bp->s.size);
return ;
}
//扫描空闲块链表,找到bp所处位置,它可能在两个空闲块之间,也可能在链表末尾
for(p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr){
//被释放的块在链表的开头或末尾
if(p >= p->s.ptr && (bp > p || bp < p->s.ptr)){
break;
}
}
//与上一相邻(空闲)块合并
if(bp + bp->s.size == p->s.ptr){
bp->s.size += p->s.ptr->s.size;
bp->s.ptr = p->s.ptr->s.ptr;
}
else{//说明中间有已经分配的块
bp->s.ptr = p->s.ptr;
}
//与下一相邻(空闲)块合并
if(p + p->s.size == bp){
p->s.size += bp->s.size;
p->s.ptr = bp->s.ptr;
}
else{//说明中间有已经分配的块
p->s.ptr = bp;
}
}

简单的测试:

折叠/展开代码
1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct MyList {
int val;
struct MyList *next;
} MyList;

int main(int argc, char *argv[]) {
int n = 1000000;
while(n--) {
MyList *list = malloc(sizeof(MyList));
free(list);
}
return 0;
}

设计简单内存池

内存池和线程池或数据库连接池(各种池)的实现机制类似,都是事先申请一大块资源,当实际使用时,不必再去申请内存,而是直接从内存池中“拿”,如果内存池设计得好,完全可以避免直接使用 malloc/free 进行内存管理的问题。

下面的代码很大程度上参考了 这里 的实现

折叠/展开代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
mem_pool.h
#ifndef _MEM_POOL_H
#define _MEM_POOL_H

#define BLOCKHEAD_SIZE 24 // 两个地址值合为16位,再加上8位验证码
#define MAGIC_CODE 0x123456 // 验证码(用于free内存块的时候判断是否是合法的内存块)

#include <stdio.h>
#include <stdint.h>
#include <malloc.h>

/**
* 内存块
* 1.data包含head和body两部分
* head包括指向对应block的指针、对应list的指针及验证码(MAGIC_CODE)
* 2.双向链表
* 为了free时方便从used中拿出插入到free中
*/
typedef struct MemBlock {
void *data;
struct MemBlock *prev, *next;
} MemBlock;
static MemBlock *newMemBlock(int size) {
MemBlock *block = malloc(sizeof(MemBlock));
if(! block) {
perror("malloc MemBlock failed");
return NULL;
}
block->data = malloc(BLOCKHEAD_SIZE + size);
return block;
}
/**
* 内存表
* 1.每个list保存一种size的内存块
*/
typedef struct MemList {
MemBlock *free, *used;
int size;
struct MemList *next;
} MemList;
static MemList *newMemList(int size) {
MemList *list = malloc(sizeof(MemList));
if(! list) {
perror("malloc MemList failed");
return NULL;
}
list->size = size;
return list;
}
/**
* 内存池
* 1.单例(因为是C语言,所以还未实现)
* 1.蝇量
*/
typedef struct MemPool {
MemList *head;
MemList *last;
} MemPool;
static MemPool *newMemPool() {
MemPool *pool = malloc(sizeof(MemPool));
if(! pool) {
perror("malloc MemPool failed");
return NULL;
}
return pool;
}

void* getBuf(MemPool *pool, size_t bufSize);
int freeBuf(void *buf);

#endif // _MEM_POOL_H
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#include "mem_pool.h"

/**
* 把内存块想象成一个int64的数组,头三个元素(head)为元数据,
* 之后的部分为分配给用户实际可用的内存
*/
void setMemoryHead(void *buf, MemList *list, MemBlock *block) {
if(buf == NULL) {return ;}
int64_t *data = (int64_t *)buf;
// 设置头部,注意指针有64位
data[0] = (int64_t)list;
data[1] = (int64_t)block;
data[2] = (int64_t)MAGIC_CODE;
}
void* getMemoryBody(void *buf) {
if(buf == NULL) {return NULL;}
int64_t *data = (int64_t *)buf;
return &data[3];
}
/**
* 从释放的内存块中可以找到它所属的list和block节点位置
* @return 0表示失败,其他表示成功
*/
int getFromBuf(void *buf, MemList** list, MemBlock** block) {
// 先转换为字节数组再计算到头部的偏移量,因为
char* cbuf = buf;
int64_t *data = (int64_t *)(buf - BLOCKHEAD_SIZE);
if(data[2] != MAGIC_CODE) {
perror("error: not a valid block");
return 0;
}
// 还原
*list = data[0];
*block = data[1];
return 1;
}

/**
* 从内存池中获取一块内存
* 内存块的大小是固定的,链表的每个节点代表一种大小的内存块
*/
void* getBuf(MemPool *pool, size_t bufSize) {
if(! pool) {return NULL;}
// list为空,表示还未分配过,尝试分配并初始化
if(! pool->head) {
pool->head = newMemList(bufSize);
}
MemList *list = pool->head;

// 查找匹配大小的list
while(list && list->size != bufSize) {
list = list->next;
}

// 没有匹配的list,分配一个,插入到表头处
if(! list) {
MemList *tmp = newMemList(bufSize);
tmp->next = pool->head;
pool->head = tmp;
list = tmp;
}

// 查找已有list中是否存在空余内存块,若没有则新建
if(! list->free) {
list->free = newMemBlock(list->size);
}

// 取一块
MemBlock *block = list->free;
list->free = block->next;
if(list->free) {
list->free->prev = NULL;
}
// 压回已使用块表
if(list->used) {
list->used->prev = block;
}
block->next = list->used;
list->used = block;

// 设置块头部
setMemoryHead(block->data, list, block);
return getMemoryBody(block->data);
}

/**
* 释放一块内存
* @return 0表示释放失败,其他数字表示成功
*/
int freeBuf(void *buf) {
MemList *list = NULL;
MemBlock *block = NULL;
if(! getFromBuf(buf, &list, &block)) {
return 0;
}

// 从used块中取出
if(block->prev) {
block->prev->next = block->next;
}
if(block->next) {
block->next->prev = block->prev;
}
// 放到free块中
if(list->free) {
list->free->prev = block;
}
block->next = list->free;
block->prev = NULL;
list->free = block;

// 操作成功
return 1;
}

简单的测试:

折叠/展开代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
typedef struct List {
int val;
struct List *next;
} List;

int main() {
MemPool *pool = newMemPool();
time_t begin, end;
time(&begin);
srand(time(0));
List *list = NULL;
int n = 100000000;
while(n--) {
if(random() % 10 < 7) {
List *node = getBuf(pool, sizeof(List));
node->next = list;
list = node;
} else if(list) {
List *node = list;
list = list->next;
freeBuf(node);
}
}
time(&end);
printf("OK\n");
printf("costs %lf\n", difftime(end, begin));
return 0;
}

可改进的地方:

  • 线程安全,这里有个问题,是把整个池锁住还是把其中的某个表锁住?因为内存池是使用很频繁的,我更倾向于后者(锁住某个表的实现可以参考 Java 的分段锁机制)
  • 一种 size 一个 list,各 list 之间用链表串联起来,如果申请的内存块大小不固定也可能出现这个链表特别长的情况,那样就退化成了 malloc/free,我觉得解决办法是将分配的内存块大小固定成 1024B、2048B 这些量级,但这样又会多出很多内存碎片,又退化成了 malloc/free
  • 引入引用计数或可达性分析算法来管理内存,但是那样的话需要给所有类做一个公共基类,然后重载基类的’=’操作符用于修改引用数,非常麻烦,但是不这么做就退化成了 malloc/free
  • 老实说这个内存池并没有实用价值

一些框架中的内存管理

  1. Doug Lee 的 malloc/free
    ???

  2. Apache 内存池
    Apache 内存池的实现相对上面的来说有很多改进

  • 使用一个free数组来表示上面的MemList,下标每增加 1,内存节点大小增加 4K,这样可以很快定位到要分配的那个内存块链表,如果要分配的内存大小超出数组范围就直接分配到下标 0 处,所以不会有溢出的问题
    节点大小和下标间的转换用到了一个宏APR_ALIGN,使用该宏要求size为整数,boundary为 2 的幂,具体执行过程还是自己代入一组值算一算
    1
    2
    #define APR_ALIGN(size, boundary) \
    (((size)+ ((boundary) - 1)) &~((boundary) - 1))
  • 没有 used 表,分配时就从 free 中取出来返回给用户,找不到就 malloc 一个,释放时插回到 free 表中
  • 用 current_free_index 和 max_free_index 配合限制内存分配器总共能够分配的内存大小
  1. cocos2d-x 内存管理
    对 cocos2d-x 的使用经历比较短,不能很好体会它的设计理念,只列出我记得的一些点:
  • C++11 下本地存储(栈)比堆存储好,最好不要 new 对象
  • cocos2d-x 使用引用计数算法管理内存,大部分内置对象都继承自 cocos2d::Object 类
  1. MySQL 内存池
    MySQL 使用二级缓存机制管理分配的对象,第一级为缓冲池和通用内存池,他们都通过调用 malloc/free 来从操作系统分配/释放内存块,他们的区别下面再讲;第二级为内存堆,它为其他模块提供内存块分配功能
  • 缓冲池(innobase/buf/buf0buf.c、innobase/include/buf0buf.h)
    缓冲池的定义如下

  • 通用内存池(innobase/mem/mem0pool.c、innobase/include/mem0pool.h)
    内存池的定义如下

    折叠/展开代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct mem_pool_struct{
    byte* buf; /* memory pool */
    ulint size; /* memory common pool size */
    ulint reserved; /* amount of currently allocated
    memory */
    mutex_t mutex; /* mutex protecting this struct */
    UT_LIST_BASE_NODE_T(mem_area_t)
    free_list[64]; /* lists of free memory areas: an
    area is put to the list whose number
    is the 2-logarithm of the area size */
    };

内存池的创建函数如下所示

折叠/展开代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
mem_pool_t*
mem_pool_create(
/*============*/
/* out: memory pool */
ulint size) /* in: pool size in bytes */
{
mem_pool_t* pool;
mem_area_t* area;
ulint i;
ulint used;

ut_a(size > 10000);

pool = ut_malloc(sizeof(mem_pool_t));

/* We do not set the memory to zero (FALSE) in the pool,
but only when allocated at a higher level in mem0mem.c.
This is to avoid masking useful Purify warnings. */

pool->buf = ut_malloc_low(size, FALSE);
pool->size = size;

mutex_create(&(pool->mutex));
mutex_set_level(&(pool->mutex), SYNC_MEM_POOL);

/* Initialize the free lists */

for (i = 0; i < 64; i++) {

UT_LIST_INIT(pool->free_list[i]);
}

used = 0;

while (size - used >= MEM_AREA_MIN_SIZE) {

i = ut_2_log(size - used);

if (ut_2_exp(i) > size - used) {

/* ut_2_log rounds upward */

i--;
}

area = (mem_area_t*)(pool->buf + used);

mem_area_set_size(area, ut_2_exp(i));
mem_area_set_free(area, TRUE);

UT_LIST_ADD_FIRST(free_list, pool->free_list[i], area);

used = used + ut_2_exp(i);
}

ut_ad(size >= used);

pool->reserved = 0;

return(pool);
}

由此可以得出通用内存池和缓冲池之间的差别

1.

1.

  • 内存堆(innobase/mem/mem0mem.c、innobase/include/mem0mem.h、innobase/include/mem0mem.ic)
    内存堆的定义如下

    折叠/展开代码
    1
    2
    /* A memory heap is a nonempty linear list of memory blocks */
    typedef mem_block_t mem_heap_t;

    可见内存堆就是内存块的链表。

  • 内存块(memory block)的定义如下

    折叠/展开代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    struct mem_block_info_struct {
    ulint magic_n;/* 魔数用于debug(标志该块内存是从我这分出去的) *//* magic number for debugging */
    char file_name[8];/* 创建内存堆的代码文件名 *//* file name where the mem heap was created */
    ulint line; /* 创建内存堆的代码行 *//* line number where the mem heap was created */
    UT_LIST_BASE_NODE_T(mem_block_t) base; /* 在第一块(表头)中定义,保存内存块链表的一些元信息,
    包括长度、表头指针、表尾指针 */
    /* In the first block in the
    the list this is the base node of the list of blocks;
    in subsequent blocks this is undefined */
    UT_LIST_NODE_T(mem_block_t) list; /* 内存块的链表(双向链表) *//* This contains pointers to next
    and prev in the list. The first block allocated
    to the heap is also the first block in this list,
    though it also contains the base node of the list. */
    ulint len; /* 当前内存块大小,以字节为单位 *//* physical length of this block in bytes */
    ulint type; /* 该内存堆的类型,内存堆的主要分类依据是:从通用内存池还是缓冲池分配 */
    /* type of heap: MEM_HEAP_DYNAMIC, or
    MEM_HEAP_BUF possibly ORed to MEM_HEAP_BTR_SEARCH */
    ibool init_block; /* TRUE if this is the first block used in fast
    creation of a heap: the memory will be freed
    by the creator, not by mem_heap_free */
    ulint free; /* 当前空闲空间位置 */
    /* offset in bytes of the first free position for
    user data in the block */
    ulint start; /* the value of the struct field 'free' at the
    creation of the block */
    byte* free_block;
    /* if the MEM_HEAP_BTR_SEARCH bit is set in type,
    and this is the heap root, this can contain an
    allocated buffer frame, which can be appended as a
    free block to the heap, if we need more space;
    otherwise, this is NULL */
    #ifdef MEM_PERIODIC_CHECK
    UT_LIST_NODE_T(mem_block_t) mem_block_list;
    /* List of all mem blocks allocated; protected
    by the mem_comm_pool mutex */
    #endif
    };

内存块的创建函数如下所示

折叠/展开代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
UNIV_INLINE
mem_heap_t*
mem_heap_create_func(
/*=================*/
/* out, own: memory heap */
ulint n, /* in: desired start block size,
this means that a single user buffer
of size n will fit in the block,
0 creates a default size block;
if init_block is not NULL, n tells
its size in bytes */
void* init_block, /* in: if very fast creation is
wanted, the caller can reserve some
memory from its stack, for example,
and pass it as the the initial block
to the heap: then no OS call of malloc
is needed at the creation. CAUTION:
the caller must make sure the initial
block is not unintentionally erased
(if allocated in the stack), before
the memory heap is explicitly freed. */
ulint type, /* in: MEM_HEAP_DYNAMIC, or MEM_HEAP_BUFFER
possibly ORed to MEM_HEAP_BTR_SEARCH */
char* file_name, /* in: file name where created */
ulint line /* in: line where created */
)
{
mem_block_t* block;

if (n > 0) {
// 创建指定大小的内存块
block = mem_heap_create_block(NULL, n, init_block, type,
file_name, line);
} else {
// 以默认的大小创建,MEM_BLOCK_START_SIZE的值为64B
block = mem_heap_create_block(NULL, MEM_BLOCK_START_SIZE,
init_block, type, file_name, line);
}

ut_ad(block);

UT_LIST_INIT(block->base);

/* Add the created block itself as the first block in the list */
UT_LIST_ADD_FIRST(list, block->base, block);

#ifdef UNIV_MEM_DEBUG

if (block == NULL) {

return(block);
}

mem_hash_insert(block, file_name, line);

#endif

return(block);
}

当想要扩增内存堆时调用,它会将扩增的内存块插入内存堆的末尾(mem_block_info_struct 结构中的 base 成员变量包含了表尾指针),而且每次扩增的内存块大小为前一次的两倍大

  1. 内存池和 GC 的关系
    在阅读《垃圾回收的算法与实现》时,除了某人用 GC 交到了女朋友外,使我最印象深刻的应该是前言中一位教授对 GC 和虚拟内存关系的看法:

    既然话说到这里了,我就再介绍一下我的个人看法吧。实际上,GC 相当于虚拟内存。一般的虚拟内存技术是在较小的物理内存的基础上,利用辅助存储创造一片看上去很大的“虚拟”地址空间。也就是说,GC 是扩大内存空间的技术,因此我称其为空间性虚拟存储。这样一来,GC 就成了永久提供一次性存储空间的时间轴方向的时间性虚拟存储。神奇的是,比起称为“垃圾回收”,把 GC 称为“虚拟内存”令人感觉其重要了许多。当初人们根据计算机体系结构开发了许多关于空间性虚拟存储的支持,所以大部分的计算机都标配了空间性虚拟存储。只要硬件支持,GC 性能就能稳步提升,然而现实情况是几乎没有支持 GC 的硬件,这不能不令人感到遗憾。

什么时候使用内存池及一些要点

  1. 小心内存泄露
    如果应用从内存池中获取对象后没有释放回去就很有可能会发生内存泄露的情况,和 C 语言中 malloc 后忘了 free 的情况类似。一般内存池都是单例的,这意味着内存池和整个应用的生命周期是相同的,这块没被释放的内存将一直占用内存空间,而且因为内存池中总是有对这个对象的引用,所以就算有 GC 也没有办法进行回收。
    水平有限我仅谈谈在 Web 应用中的几个解决办法:
    • 缩短内存池的生命周期,比如在一次会话期间占用一个内存池,这个内存池将会被保存在一个 SessionContext 对象中,当会话结束时就释放它
    • 在一些对象价值不是特别高的情形下(比如),可以使用 弱引用对象池 来管理对象???

参考

  1. 《C 程序设计语言》
  2. 内存池的实现 http://www.cnblogs.com/bangerlee/archive/2011/08/31/2161421.html
  3. A memory pool written with C++ https://github.com/bangerlee/mempool
  4. MySQL 内核 InnoDB 存储引擎
  5. https://www.zhihu.com/question/26190832/answer/32387918
  6. 《垃圾回收的算法与实现》

姓名排序

题目描述:给定 30,000 个中国人的名字,选用一种线性排序方法有效排序,要求使用内存不超过 1G。
想法 1:计数排序,汉字组合显然是无穷多的,无法确定元素的范围,而且可能有重名的情况,所以不能使用;
想法 2:使用桶排序,不好确定每个桶的元素范围,一般情况下都是假定元素限制在(0,1];
想法 3:使用基数排序可以一次按名字的第 i 位的排序,如果每个桶的标志为一个汉字,那桶就太多了,如果每个桶的标志为汉字的首字母,桶够少了,但是每个桶都需要大量时间,既然是汉字,我们完全可以用汉字拼音;

获得测试组

一组有 3w 行,每一行随机 2-10 个 utf-8 中文字符

从这里[1]了解到 unicode 字符的基本汉字编码范围为 4E00-9FA5,我的想法是随机生成一个 19968-40869 的 unsigned int 型数,然后编码成 utf-8 字符;

utf-8 字符可以由 unicode 字符转换而来,转换算法[2]为:

1
2
3
4
×××××××× ×××××××× -> 1110×××× 10×××××× 10××××××
1110拼上前4位
10拼上中间6位
10拼上后6位

输入到文件中

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//中文字为3字节,第一步
void randomname(unsigned char *name){
int length = random() % 8 + 2;
int pos;
for(pos = 0; pos < length; pos++){
unsigned int a = (random() % 20902) + 19968;
name[3 * pos] = (a >> 12) + 224;
name[3 * pos + 1] = ((a >> 6) % 64) + 128;
name[3 * pos + 2] = (a % 64) + 128;
}
name[3 * pos] = '\0';
printf("%s", name);
}
void makefile(FILE *f){
if(f == NULL){
fprintf(stderr, "error1");
exit(1);
}
unsigned char name[35];
int count = 30000;
srand(time(0));
while(count--){
randomname(name);
if(fprintf(f, "%s%c", name, '\r') == 0){
fprintf(stderr, "error");
break;
}
}
fclose(f);
}
int main(void){
FILE *f = fopen("a.txt", "w");
makefile(f);
return 0;
}

转码

我们希望按照拼音顺序排序,所以得先将 UTF-8 字符转换为 GB2312 编码

这里遇到了难题,我打算使用 linux 下的 iconv 进行转码,但是转出来的都是乱码,可能是对转码的工作原理还不明白。

刚开始我打算编写 C 语言代码转换 utf-8 -> gb2312,但是失败了,一是因为有些字符在 gb2312 中不存在,报错:

1
2
C语言中出现 iconv(pattern, pin, &inlen, pout, &outlen) 返回-1
命令行下出现 iconv: 未知 0 处的非法输入序列

二是我直接输出到了终端,结果出现了乱码:

1
你好 -> ����

是因为终端默认使用 utf-8,这样就无法显示了。

后来使用 GBK,因为 GBK 包括 gb2312,将编码后的字符直接输出到了文件中,这样就能看到编码后的结果了

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <stdio.h>
#include <iconv.h>
#include <string.h>
#include <stdlib.h>
int code_convert(char *from_charset, char *to_charset, char *inbuf, size_t inlen, char *outbuf, size_t outlen){
iconv_t pattern;
char **pin = &inbuf;
char **pout = &outbuf;
pattern = iconv_open(to_charset, from_charset);
if(pattern == 0){
return -1;
}
memset(outbuf, 0, outlen);
if(iconv(pattern, pin, &inlen, pout, &outlen) == -1){
return -1;
}
iconv_close(pattern);
return 0;
}
#define MAXNUM 30000
#define MAXNAME 31
char names_utf8[MAXNUM][MAXNAME];
char names_gb2312[MAXNUM][MAXNAME];
void readFile(char *fileName){
FILE *f;
if((f = fopen(fileName, "r")) == NULL){
fprintf(stderr, "cant open file %s\n", fileName);
exit(1);
}

int pos;
for(pos = 0; pos < MAXNUM; pos++){
//fgets(names_utf8[pos], MAXNAME, f);fgets会保留行末尾的\r\n
fscanf(f, "%s", names_utf8[pos]);
}
}
void convert(){
FILE *f = fopen("b.txt", "w");
int pos;
for(pos = 0; pos < MAXNUM; pos++){
if(code_convert("utf-8", "GBK", names_utf8[pos], strlen(names_utf8[pos]), names_gb2312[pos], MAXNAME) == -1){
fprintf(stderr, "iconv error");
exit(2);
}
fprintf(f, "%s\r\n", names_gb2312[pos]);
}
fclose(f);
}

这部分代码等价于以下命令:

1
sudo iconv -f utf-8 -t GBK a.txt -o b.txt

起初认为像 GBK 这样的汉字编码标准会很地道地按拼音顺序来编码,但是还是太天真了,因为有多音字存在等原因,要按拼音编码是不大可能的;

最终上网找了一个汉字转拼音的程序[4],找拼音的大致思路是预先准备好拼音及该拼音的汉字,要找一个汉字的拼音就遍历一遍找,方法比较简单只覆盖了一部分汉字,而且是线性遍历效率不高,还有个缺点就是多音字只能找到第一个;

对姓名进行排序

最后就是使用基数排序了,因为使用 utf-8 编码,所以数组中 27-29 为第 10 个汉字,24-26 为第 9 个汉字,以此类推,姓名有长有短,比如“张三”的后 8 位都不存在,默认放入 0 号桶,其他情况如果能找到该位汉字的拼音,将姓名放到对应的桶中,如果找不到,默认该汉字未编码,放入 0 号桶,这里桶其实代表 a/an/ang…,最后为了保证排序的稳定性,桶使用了队列实现;

代码如下所示:
其中pinyin.h是,代码如这里所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
#include "pinyin.h" //汉字-拼音
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define P2HSIZE (sizeof(p2h) / sizeof(p2h[0]))
int getP2Hpos(char word[]){
if(word == NULL || word[0] == '\0'){ return -1; }
int pos;
char *curHzString;
for(pos = 0; pos < P2HSIZE; pos++){
curHzString = p2h[pos].hz;
if(strstr(curHzString, word)){
return pos;
}
}
return -1;
}
#define MAXNUM 30000 //文件中姓名的数量
#define MAXNAME 31 //姓名最多为10个汉字
char *names_utf8[MAXNUM];
/*******************************************/
void readFile(char *fileName){
FILE *f;
if((f = fopen(fileName, "r")) == NULL){
fprintf(stderr, "cant open file %s\n", fileName);
exit(1);
}

int pos;
for(pos = 0; pos < MAXNUM; pos++){
names_utf8[pos] = (char*)malloc(MAXNAME * sizeof(char));
fscanf(f, "%s", names_utf8[pos]);
}
}
void clearNames(){
int pos;
for(pos = 0; pos < MAXNUM; pos++){
free(names_utf8[pos]);
}
}
/****************************************/
typedef struct cell{
int pos;
struct cell *next;
}cell;
typedef struct bucket{
cell *front;
cell *rear;
}bucket;
int isEmpty(bucket *b){
return b->rear == NULL;
}
cell *makeCell(int pos){
cell *c = (cell*)malloc(sizeof(cell));
if(c == NULL){ fprintf(stderr, "error"); }
c->pos = pos, c->next = NULL;
return c;
}
bucket *makeBucket(){
bucket *b = (bucket*)malloc(sizeof(bucket));
b->front = b->rear = NULL;
return b;
}
int push(bucket *b, int pos){
if(b == NULL){printf("error");
return -1;
}
cell *c = makeCell(pos);
if(isEmpty(b)){
b->rear = b->front = c;
}
else{
b->front->next = c;
b->front = c;
}
return 0;
}
int pop(bucket *b){
if(isEmpty(b)){
return -1;
}
else{
cell *rear = b->rear;
int pos = rear->pos;
b->rear = rear->next;
free(rear);
return pos;
}
}
//桶的数量就是拼音的数量,0号桶用于装找不到拼音的汉字
#define BUCKETSNUM (P2HSIZE + 1)
bucket *buckets[BUCKETSNUM];
char tmp[4];
void pushBuckets(int radix){//1 <= radix <= 10,表示第几个字
int pos, bucketId;
for(pos = 0; pos < MAXNUM; pos++){//如果改成while(pos++ < MAXNUM)会出现段错误,为什么??
strncpy(tmp, names_utf8[pos] + 3 * (radix - 1), 3);
bucketId = getP2Hpos(tmp) + 1;
push(buckets[bucketId], pos);
}
}
//桶中保存的是对应姓名数组的下标
void popBuckets(){
char *names_tmp[MAXNUM];
int npos;
for(npos = 0; npos < MAXNUM; npos++){
names_tmp[npos] = names_utf8[npos];
}
npos = 0;
int bpos;
for(bpos = 0; bpos < BUCKETSNUM; bpos++){
while(!isEmpty(buckets[bpos])){
int pos = pop(buckets[bpos]);
names_utf8[npos++] = names_tmp[pos];
}
}
}
void initBuckets(){
int pos;
for(pos = 0; pos < BUCKETSNUM; pos++){
buckets[pos] = makeBucket();
}
}
void clearBuckets(){
int pos;
for(pos = 0; pos < BUCKETSNUM; pos++){
while(pop(buckets[pos]) != -1);
}
}
void radixSort(){
int radix;
for(radix = 10; radix >= 1; radix--){
pushBuckets(radix);
popBuckets();
}
}
void writeResult(){
FILE *f = fopen("result.dat", "w");
if(f == NULL){ fprintf(stderr, "open file failed"); exit(1);}

int pos;
for(pos = 0; pos < MAXNUM; pos++){
fprintf(f, "%s\r\n", names_utf8[pos]);
}
fclose(f);
}
void func(){
readFile("a.txt");
initBuckets();
radixSort();
writeResult();
clearNames();
}
int main(void){
func();
return 0;
}

拓展

当需要排序的数据量很大,例如 100G 个人按照年龄排序,如何基于外存对这些数据排序?使用 mapreduce 等工具?

参考

  1. 汉字 Unicode 编码范围
  2. Unicode 和 UTF-8 有何区别?
  3. linux 下 iconv()函数的用法
  4. Linux C 实现 简单的 汉字转拼音
  5. 字符编码详解
  6. C/C++段错误问题排查和解决方法

马拉松计划

训练

4-10 10 11 20 13 休息 30 休息
11-17 8 13 20 13 休息 20 休息
18-24 5 10 13 8 休息 休息 马拉松

PS1:后程如果跑不动,可以尝试跑 5 分钟走 1 分钟调整,喝水时也可以走一会。
PS2:目标是 400,平常用 11 的速度跑,马拉松用 10 的速度。

饮食

平时多喝牛奶。
马拉松时带点巧克力,不知道场地里的粮食多不。

装备

跑前似乎有发衣服,所以就不买专门的速干衣了,但是跑鞋和毛巾袜是必须的。

0%