ziggle

Hail Hydra


  • Home

  • Archives

  • Search

MySQL开发规范

Posted on 2021-04-14

MySQL开发规范

一 表设计

1.1 命名

库名、表名、字段名必须使用英文单词(单数)表示表的意义,一个单词不足以表示时两者之间使用”_”(表与字段)分割。 原则上不能超过20个字符

  • table命名:(单数,多个单词,”_”分隔)

    Bad Example: csw_creditcard_bills

    Good Example: csw_creditcard_bill

  • index命名:

一般索引(Btree)命名:

Bad Example: index_uid_accid_type(uid, type, acid)

Good Example: idx_uid_type_acid(uid, type, acid)(索引表包含索引的所有信息,顺序一致,准确无误)。

唯一键索引命名:

Bad Example: bill_no (bill_no)

Good Example: uk_bill_no (bill_no)。

  • 字段的命名:

定义意义完整的单词或组合单词作为字段名,”_”分隔不同意义单词

Bad Example: 字段名:name

Good Example: 字段名:xxx_name 因为名称可以有多个,昵称,登录名等等。

1.2 字段类型使用

VARCHAR类型( 0-65535 Bytes)

适用于动态长度字符串,尽量根据业务需求定义合适的字段长度,不要为了图省事,直接定义为varchar(1024)或更长等等。

Char类型(字符数0-255)

适合所有固定字符串,如UUID char(36).不要使用Varchar类型。

Text 类型

仅当字符数量可能超过 20000 个的时候,才可以使用 TEXT 类型来存放字符类数据 (原因在于varchar最多可存65535Bytes,utf8字符集下每字符占用3个字节,65535/3=20000+)所有使用 TEXT 类型的字段必须和原表进行分拆,与原表主键单独组成另外一个表进行存放.(原因在于,数据库按行扫描并获取数据,如果存在text字段,将大大增加行检索成本,分出去以后可按原表主键再获取text值。

禁用VARBINARY、BLOB存储图片、文件等

Enum类型

用 0, 1 ,2 等数字tinyint类型来代替Enum

Date类型

所有需要精确到时间(时分秒)的字段均使用TIMESTAMP

所有只需要精确到天的字段全部使用 DATE 类型,而不应该使用 TIMESTAMP 或者DATETIME 类型。

Integer 类型

所有整数类型的字段尽量使用合适的大小,且明确标识出为无符号型(UNSIGNED),除非确实会出现负数,仅仅当该字段数字取值会超过22亿,才使用 BIGINT 类型

1
2
3
4
5
6
7
8
9
10
11
12
类型   	字节	最小值  	最大值
    (带符号的/无符号的) (带符号的/无符号的)
TINYINT 1 -128 127
    0 255
SMALLINT 2 -32768 32767
    0 65535
MEDIUMINT 3 -8388608 8388607
    0 16777215
INT 4 -2147483648 2147483647
  0  4294967295
BIGINT 8 -9223372036854770000 9223372036854770000
    0 18446744073709500000

浮点数类型

存储精确浮点数必须使用DECIMAL替代FLOAT和DOUBLE或转为整形(推荐)字段

1.3 设计

  • 所有表必须包含三列,自增ID主键,unsigned数据类型,created_at(记录创建时间),updated_at(记录更新时间)
1
2
3
4
5
6
7
Example:
create table t1(
id int unsigned NOT NULL AUTO_INCREMENT,
created_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB comment '表描述';
  • 尽量做好适当的字段冗余(例:uid)
1
2
3
4
5
6
7
8
9

Example:
create table t1 (
id int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
uid int unsigned NOT NULL COMMENT '用户ID',
created_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB comment 'table描述';

MySQL5.6之后timestamp可以支持多列字段拥有自动插入时间和自动更新时间:DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

MySQL5.6之之前timestamp仅支持一列字段拥有自动插入时间和自动更新时间:DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

  • 索引字段属性设置为not null default 值(不能索引Null值)

  • 添加字段时不可带after,before,新加字段在表末尾添加

  • 表字符集采用utf8,不区分大小写(不使用utf8-bin)

  • 表存储引擎Innodb

  • 表及字段必须使用中文注释,简洁明了

  • 线上涉及表结构变更需至少提前24小时通知dba,大表提前1周。(在开发阶段与dba讨论db设计)

  • 状态型及类型列使用tinyint即可,原则上不能建索引(如status,type)

  • 禁止使用force index 等加hint方式使用索引

二 索引设计

  • 索引名称必须使用小写

  • 非唯一索引必须按照“idx_字段名”进行命名

  • 唯一索引必须按照“uk_字段名”进行命名

  • 索引中的字段数建议不超过5个,单张表的索引数量控制在5个以内

  • ORDER BY,GROUP BY,DISTINCT的字段尽量使用索引

  • 使用EXPLAIN判断SQL语句是否合理使用索引,尽量避免extra列出现:Using File Sort,Using Temporary

  • 对长度过长的VARCHAR字段必须建立索引时,使用Hash字段建立索引或者使用前缀索引,例:(idx(clumn(8)))

  • 合理创建联合索引,一定注意重复率低及高频查询字段作为前缀索引

Bad Example:

1
2
3
4
5
6
7
8
9
10
create table t1(
id int unsigned NOT NULL AUTO_INCREMENT,
uid int unsigned NOT NULL comment '用户id'
status tinyint not null default 0 comment '状态描述',
type tinyint not null default 0 comment '字段类型',
created_time NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
unique key uk_status_type_uid(status,type,uid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 comment '表描述';

Good Example:

1
2
3
4
5
6
7
8
9
10
create table t1(
id int unsigned NOT NULL AUTO_INCREMENT,
uid int unsigned NOT NULL comment '用户id'
status tinyint not null default 0 comment '状态描述',
type tinyint not null default 0 comment '字段类型',
created_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),z
unique key uk_uid_status_type(uid,status,type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 comment '表描述';

  • innodb 所有表必须建ID主键

  • 索引设计尽可能够用就好,避免无效索引

三 SQL编写

  • WHERE条件中必须使用合适的数据类型,避免MySQL进行隐式类型转化

  • SELECT、INSERT语句必须显式的指明业务所需的字段名称,如果取值不是全表,希望指定列名,不允许select

  • 避免复杂sql,降低业务耦合,方便之后的scale out 和sharding(如之后查询多个数据库等并发进行)

  • 对于结果较多的分页查询

Bad Example:

1
SELECT * FROM relation where biz_type ='0' AND end_time >='2014-05-29' ORDER BY id asc LIMIT 149420 ,20;

Good Example:

1
SELECT a.* FROM relation a, (select id from relation where biz_type ='0' AND end_time >='2014-05-29' ORDER BY id asc LIMIT 149420 ,20 ) b where a.id=b.id;。

  • 避免在SQL语句进行数学运算或者函数运算(应在程序端完成)

  • 用in or join代替子查询,in包含的值不易超过2000(整形)

  • 表连接规范避免使用JOIN

所有非外连接的 SQL 不要使用 Join … On … 方式进行连接,而直接通过普通的 Where条件关联方式。外连接的 SQL 语句,可以使用 Left Join … On 的 Join 方式,且所有外连接一律写成 Left Join,而不要使用 Right Join。

Bad Example:

1
select a.id,b.id from a join b on a.id = b.a_id where

Good Example:

1
select a.id,b.id from a,b where a.id = b.a_id and ...
  • 禁止insert、update、delete中做跨库操作,例:insert … wac.tbl_user

  • 业务逻辑中禁止使用存储过程、触发器、函数、外键等

  • 禁止使用order by rand(),now()函数

  • 如要insert … on duplicate key update需通知dba审核

  • 禁止使用union,用union all代替

  • 用union all 代替 or

  • 以下查询不能索引(not,!=,<>,!<,!>,not exist,not in,not like)(%like)

  • 可用explain观察sql执行计划,show profile 观察sql的性能损耗情况,目前我们环境中 5.6版本可对select,update,delete语句均支持执行计划


gradle-生命周期

Posted on 2020-12-15

构建的生命周期

任何Gradle的构建过程都分为三部分:初始化阶段、配置阶段和执行阶段。

通过如下代码向Gradle的构建过程添加监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
gradle.addBuildListener(new BuildListener() {
void buildStarted(Gradle var1) {
println '开始构建'
}
void settingsEvaluated(Settings var1) {
println 'settings评估完成(settins.gradle中代码执行完毕)'
// var1.gradle.rootProject 这里访问Project对象时会报错,还未完成Project的初始化
}
void projectsLoaded(Gradle var1) {
println '项目结构加载完成(初始化阶段结束)'
println '初始化结束,可访问根项目:' + var1.gradle.rootProject
}
void projectsEvaluated(Gradle var1) {
println '所有项目评估完成(配置阶段结束)'
}
void buildFinished(BuildResult var1) {
println '构建结束 '
}
})

hook 点

Gradle在构建的各个阶段都提供了很多回调,我们在添加对应监听时要注意,监听器一定要在回调的生命周期之前添加,比如我们在根项目的build.gradle中添加下面的代码就是错误的:

1
2
3
4
5
6
7
8
9
gradle.settingsEvaluated { setting ->
// do something with setting
}

gradle.projectsLoaded {
gradle.rootProject.afterEvaluate {
println 'rootProject evaluated'
}
}

当构建走到build.gradle时说明初始化过程已经结束了,所以上面的回调都不会执行,把上述代码移动到settings.gradle中就正确了。

通过一些例子来解释如何Hook Gradle的构建过程。

为所有子项目添加公共代码

在根项目的build.gradle中添加如下代码:

1
2
3
4
 gradle.beforeProject { project ->
println 'apply plugin java for ' + project
project.apply plugin: 'java'
}

这段代码的作用是为所有子项目应用Java插件,因为代码是在根项目的配置阶段执行的,所以并不会应用到根项目中。
这里说明一下Gradle的beforeProject方法和Project的beforeEvaluate的执行时机是一样的,只是beforeProject应用于所有项目,而beforeEvaluate只应用于调用的Project,上面的代码等价于

1
2
3
4
5
6
allprojects {
beforeEvaluate { project ->
println 'apply plugin java for ' + project
project.apply plugin: 'java'
}
}

after*也是同理的,但afterProject还有一点不一样,无论Project的配置过程是否出错,afterProject都会收到回调

为指定Task动态添加Action

1
2
3
4
5
6
7
8
9
10
11
 gradle.taskGraph.beforeTask { task ->
task << {
println '动态添加的Action'
}
}

task Test {
doLast {
println '原始Action'
}
}
获取构建各阶段耗时情况
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
long beginOfSetting = System.currentTimeMillis()

gradle.projectsLoaded {
println '初始化阶段,耗时:' + (System.currentTimeMillis() - beginOfSetting) + 'ms'
}

def beginOfConfig
def configHasBegin = false
def beginOfProjectConfig = new HashMap()
gradle.beforeProject { project ->
if (!configHasBegin) {
configHasBegin = true
beginOfConfig = System.currentTimeMillis()
}
beginOfProjectConfig.put(project, System.currentTimeMillis())
}
gradle.afterProject { project ->
def begin = beginOfProjectConfig.get(project)
println '配置阶段,' + project + '耗时:' + (System.currentTimeMillis() - begin) + 'ms'
}
def beginOfProjectExcute
gradle.taskGraph.whenReady {
println '配置阶段,总共耗时:' + (System.currentTimeMillis() - beginOfConfig) + 'ms'
beginOfProjectExcute = System.currentTimeMillis()
}
gradle.taskGraph.beforeTask { task ->
task.doFirst {
task.ext.beginOfTask = System.currentTimeMillis()
}
task.doLast {
println '执行阶段,' + task + '耗时:' + (System.currentTimeMillis() - task.beginOfTask) + 'ms'
}
}
gradle.buildFinished {
println '执行阶段,耗时:' + (System.currentTimeMillis() - beginOfProjectExcute) + 'ms'
}

将上述代码段添加到settings.gradle脚本文件的开头,再执行任意构建任务,你就可以看到各阶段、各任务的耗时情况。

动态改变Task依赖关系

有时我们需要在一个已有的构建系统中插入我们自己的构建任务,比如在执行Java构建后我们想要删除构建过程中产生的临时文件,那么我们就可以自定义一个名叫cleanTemp的任务,让其依赖于build任务,然后调用cleanTemp任务即可。

  • 寻找插入点
    如果你对一个构建的任务依赖关系不熟悉的话,可以使用一个插件来查看,在根项目的build.gradle中添加如下代码:
    1
    2
    3
    plugins{
    id "com.dorongold.task-tree" version "1.5"
    }

然后执行gradle <任务名> taskTree --no-repeat,即可看到指定Task的依赖关系,比如在Java构建中查看build任务的依赖关系:

  • 动态插入自定义任务

我们先定义一个自定的任务cleanTemp,让其依赖于assemble。

1
2
3
4
5
6
7
8
9
task cleanTemp(dependsOn: assemble) {
doLast {
println '清除所有临时文件'
}
}

afterEvaluate {
build.dependsOn cleanTemp
}

pwsh-刷新环境变量

Posted on 2020-12-11

刷新环境变量

code $profile

1
2
3
4
function RefreshEnv {
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
Write-host('RefreshEnv')
}

jvm-cms垃圾回收

Posted on 2020-10-27

浅谈CMS原理

CMS(Concurrent Mark Sweep)收集器以获取最短回收停顿时间为目标,是HotSpot虚拟机中第一款真正意义上的并发收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

过程

  • 初始标记(CMS initial mark):标记一下GC Roots能直接关联到的对象。需要STW,速度很快。
  • 并发标记(CMS concurrent mark):进行GC Roots Tracing。不需要STW。会产生浮动垃圾。
  • 重新标记(CMS remark):找到并发标记期间产生的浮动垃圾。需要STW,停顿时间一般会比初始标记稍长,但远比并发标记短。
  • 并发清除(CMS concurrent sweep):清除已标记的垃圾。不需要STW。会产生浮动垃圾,只能等下一次GC清理。

重新标记的过程?为什么比并发标记的时间短?
不考虑标记对象年龄等操作,最容易想到的原因是:

对象数的区别:“并发标记”过程需要扫描所有对象,标记出不可达的对象(该清除)和可达的对象(不该清除);“重新标记”只需要扫描在“并发标记”过程中被标记为可达或新创建的对象,检查其是否在“并发标记”过程中被标记为可达之后,由于用户使用变的不可达了
并发环境的区别:“并发标记”是与用户线程一起工作的,并发瓶颈较窄(工作线程少+安全检查);“重新标记”需要stop the world,之后仅有“重新标记”的线程在工作,并发瓶颈宽的多
PS:关于对象数的区别要想清楚,在对象分配后,如果对象有一瞬间不可达,则该对象以后都将不可达,可对其清理。因此,重新标记时不需要检查这部分对象。

并发清除的过程?如何才能让用户边使用,边清除?
还是那句话:

在对象分配后,如果对象有一瞬间不可达,则该对象以后都将不可达,可对其清理。

所以,已经在“并发标记”和“重新标记”过程被标记为不可达的对象,以后都不会再被用户使用,清除这些对象对用户完全无影响。

唯一可能有影响的是整理内存的过程,不过也只需要同步使用对象和整理对象两个动作。

CMS的优点、缺点?
优点:

大部分时间可与用户线程并发工作
低停顿
缺点:

对CPU资源非常敏感。并发标记和并发清理与用户线程一起工作,如果用户线程也是CPU敏感的,那么必然影响用户线程。
无法处理浮动垃圾(Floating Garbage)。并发标记与并发清除过程会产生浮动垃圾,如果CMS之前预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将退化使用Serial Old收集器,重新进行老年代的垃圾收集,这样停顿时间就很长了。可使用XX:CMSInitiatingOccupancyFraction参数设置触发CMS时的老年代空间比例(剩余空间就是预留空间),在JDK1.6中默认为92%。
基于“标记-清除算法”,收集结束时会有大量空间碎片产生,导致明明剩余空间充足,却无法为大对象分配足够的连续内存。可打开-XX:+UseCMSCompactAtFullCollection开关参数(默认打开)在进行Full GC之前整理内存碎片(称为“压缩”);使用-XX:CMSFullGCsBeforeCompaction参数(默认0)设置多少次不带压缩的Full CG之后才进行一次带压缩的Full GC。内存整理无法并行,还需要STW,需要适当调整内存整理的频率,在GC性能与空间利用率之间平衡。

PS:

浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处 理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。在这期间用户可能创建新的对象。为了处理这部分浮动垃圾和对象,CMS在并发清理之前,需要预留出足够空间给并发清理期间的用户线程使用。一般会显示使用-XX:CMSInitiatingOccupancyFraction参数设置触发CMS时的老年代空间比例,如果老年代增长不是太快,可以适当提高比例,以减少Full GC的次数。
关于浮动垃圾和内存碎片的问题。HDFS namenode在堆内存达到100G规模时,通常设置75%触发Full GC,不开启压缩,优先考虑STW造成的延迟。

jvm-oom异常原因

Posted on 2020-10-27

java.lang.OutOfMemoryError:Java heap space

这是最常见的OOM原因。

堆中主要存放各种对象实例,还有常量池等结构。当JVM发现堆中没有足够的空间分配给新对象时,抛出该异常。具体来讲,在刚发现空间不足时,会先进行一次Full GC,如果GC后还是空间不足,再抛出异常。

引起空间不足的原因主要有:

业务高峰,创建对象过多
内存泄露
内存碎片严重,无法分配给大对象

java.lang.OutOfMemoryError:Metaspace

方法区主要存储类的元信息,实现在元数据区。当JVM发现元数据区没有足够的空间分配给加载的类时,抛出该异常。

引起元数据区空间不足的原因主要有:

加载的类太多,常见于Tomcat等容器中
但是元数据区被实现在堆外,主要受到进程本身的内存限制,这种实现下很难溢出。

java.lang.OutOfMemoryError:Unable to create new native thread

以Linux系统为例,JVM创建的线程与操作系统中的线程一一对应,受到以下限制:

进程和操作系统的内存资源限制。其中,一个JVM线程至少要占用OS的线程栈+JVM的虚拟机栈 = 8MB + 1MB = 9MB(当然JVM实现可以选择不使用这1MB的JVM虚拟机栈)。
进程和操作系统的线程数限制。
Linux中的线程被实现为轻量级进程,因此,还受到pid数量的限制。
当无法在操作系统中继续创建线程时,抛出上述异常。

解决办法从原因中找:

内存资源:调小OS的线程栈、JVM的虚拟机栈。
线程数:增大线程数限制。
pid:增大pid范围。

java.lang.OutOfMemoryError:GC overhead limit exceeded
默认配置下,如果GC花费了98%的时间,回收的内存都不足2%的话,抛出该异常。

java.lang.OutOfMemoryError:Out of swap space
如果JVM申请的内存大于可用物理内存,操作系统会将内存中的数据交换到磁盘上去(交换区)。如果交换区空间不足,抛出该异常。

12…22
ziggle

ziggle

Hail Hydra !

110 posts
45 tags
RSS
GitHub
© 2021 ziggle
Powered by Hexo
|
Hail Hydra—