tidb源码学习之auto_increment

自增ID

TiDB 的自增 ID (Auto Increment ID) 只保证自增且唯一,并不保证连续分配。 TiDB 目前采用批量分配的方式,所以如果在多台TiDB上同时插入数据,分配的自增 ID 会不连续。

https://github.com/pingcap/tidb/tree/master/meta/autoid/autoid.go

基本接口

// Allocator is an auto increment id generator.
// Just keep id unique actually.
type Allocator interface {
	// Alloc allocs the next autoID for table with tableID.
	// It gets a batch of autoIDs at a time. So it does not need to access storage for each call.
	Alloc(tableID int64) (int64, error)
	// Rebase rebases the autoID base for table with tableID and the new base value.
	// If allocIDs is true, it will allocate some IDs and save to the cache.
	// If allocIDs is false, it will not allocate IDs.
	Rebase(tableID, newBase int64, allocIDs bool) error
}

接口很简单,一个是根据tableID分配一个自增ID,而且分配是批量进行的,所以还有另一个是用来重新设置批量获取的起始点。

type allocator struct {
	mu    sync.Mutex
	base  int64
	end   int64
	store kv.Storage
	dbID  int64
}

从实现类的结构可以看出实现的基本思路。mu是用于并发分配操作的锁,base表示最后一个已分配的ID,end表示最后一个未分配的ID。 当base和end相等的时候,就表示当前自增ID已经使用完,需要重新发起一次批量申请了。store表示后端的存储引擎,dbID表示表所在的数据库。

		err := kv.RunInNewTxn(alloc.store, true, func(txn kv.Transaction) error {
			m := meta.NewMeta(txn)
			var err1 error
			newBase, err1 = m.GetAutoTableID(alloc.dbID, tableID)
			if err1 != nil {
				return errors.Trace(err1)
			}
			newEnd, err1 = m.GenAutoTableID(alloc.dbID, tableID, step)
			if err1 != nil {
				return errors.Trace(err1)
			}
			return nil
		})

至于如何从store获取到当前的未分配的自增ID,首先通过GetAutoTableID获取到当前最后一个已分配的ID,然后通过GenAutoTableID获取一个批次的ID。

tidb源码学习之ast包

基本结构 ast.go

基本结构

基本实现 base.go

和上图很类似,只是属于内部实现,提供了基本的能力,用于更细化的类型实现。 基本实现

函数调用 functions.go

分别为聚合函数,普通函数,转换函数

函数调用

可视化AST

为了方便理解AST,需要有个简单的可视化工具。

type astViewer struct {
	level int
}

// Enter implements ast.Visitor interface.
func (av *astViewer) Enter(inNode ast.Node) (outNode ast.Node, skipChildren bool) {
	fmt.Println(strings.Repeat("  ", av.level), reflect.TypeOf(inNode))
	av.level++
	return inNode, false
}

// Leave implements ast.Visitor interface.
func (av *astViewer) Leave(inNode ast.Node) (node ast.Node, ok bool) {
	av.level--
	return inNode, true
}

// How to use it.
node.Accept(&astViewer{})

DML语句 dml.go

dml语句类型

dml语句类型

dml语句还有不少内部结构,简单分类一下:

字段相关部分,其中WildCardField是一种特殊的SelectField,FieldList包括多个SelectField

字段

表相关部分,其中从外到里关系是TableRefsClause - Join - TableSource - TableName

表

条件相关部分,就不解释了

条件

DDL语句 ddl.go

ddl

表达式语句 expression.go

函数 functions.go

系统管理语句 misc.go

mysql类型转换之str_to_datetime分析

  • 源码路径 https://github.com/mysql/mysql-server/blob/23032807537d8dd8ee4ec1c4d40f0633cd4e12f9/sql-common/my_time.c
  • 方法 str_to_datetime

解析算法步骤

预处理判断

  1. 跳过开头的空格
  2. 如果剩下为空或者以非数字开头,返回失败
  3. 计算第一部分的内容(包括数字或字符T)
  4. 如果只有一部分,或后面接字符. ,那么属于内部格式

预计年份的长度

  1. 内部格式,长度是4,8,14+的时候,年份是4位,其他是2位
  2. 其他情况,年份是4位

扫描各个部分

  1. 需要重新扫描(第一部分),对每部分进行扫描
  2. 如果是内部格式,扫描固定位数,否则扫描直到非数字,得到这个部分
  3. 如果这部分数值大于999999,返回失败。
  4. 对于每个部分,如果是内部格式,扫描固定长度,否则扫描到出现非数字为止
  5. 每个部分记录得到的数值,原始长度记录长度(可能有前置0),需要记录扫描了几个部分,最后的位置
  6. 如上继续扫描,如果发现了年月日之后,后续可以接字符T,这是特殊情况
  7. 每部分扫描后,跳过中间的间隔,如空格或标点,不同的是,空格不是所有地方都能加,例如年月日后面可以加,年后面就不可以。
  8. 如果扫描到秒这部分,后面如果是符号.的话,还要扫描小数位。

调整结果

  1. 年份原始长度如果是2位,需要做转换处理。
  2. 小数位需要检验长度,对于保留小数位进行处理,可能还要四舍五入
  3. 小数位可能会导致overflow,需要对结果增加一秒
  4. 需要检验日期时间格式各个部分在一定的单位内,特别需要注意闰二月等特殊情况

错误处理

  1. 这个方法很多dirty code,除了返回值,还有flags和warning status
  2. flags用来标记各种解析的选项,会影响解析结果
  3. waring status收集警告信息,有警告可能返回成或失败

tidb源码学习之错误管理

golang错误机制

golang没有内置所谓异常的方式,通常是通过返回一个error参数来标识是否出现错误。不过也提供了一个简单粗暴的panic,recover机制,它只是在应用的框架上使用,绝不是用来控制错误处理流程的。

error是一个简单的接口,只要实现Error方法,就是一个error。自定义实现error,就可以通过判断不同的error类型,来做一些区别对待,例如是忽略并显示警告,还是显示错误。

tidb错误类型

type Error struct {
	class ErrClass
	code ErrCode
	message string
	args []interface{}
	file string
	line int
}

上面是tidb自定义错误的定义,包括了错误类型,错误码,错误消息,还有一些错误位置相关的信息。

它有一个方法ToSQLError,用来转换成对外的错误信息(主要是mysql错误类型和tidb专有的)

转换关系是这样的,可以看看types包中的errors.go的用法,了解实际的使用。

ErrClassToMySQLCodes map[ErrClass](map[ErrCode]uint16)
terrors, trace, sqlerror

从上面的结构可以看出,对于一个Error对象,根据ErrClass和ErrCode是可以对应到一个mysql的错误码的。

错误链的使用

类似java那样,可以包装成另外一个往外抛,最终形成一个错误链,不会丢掉原始的异常信息。原生的error机制太简单了,tidb采用了一个开源库帮忙,https://github.com/juju/errors 通过errors.Trace(err)返回一个包装过的错误或者nil,并且允许通过errors.Cause(err)得到原始的错误,非常方便。

tidb源码学习之测试框架

tidb项目对测试非常重视,采用了大量的测试用例来保证代码的质量,目前代码测试的整体覆盖率在70%左右。

golang自带了测试框架,但tidb实际上使用的是gocheck的测试框架,这个框架兼容官方测试框架的用法。

测试框架介绍路径 gocheck有一些额外的优点,可以在使用过程中慢慢体会。

  • 强大的assert功能
  • 按test suite来组织测试用例,并支持setUp,tearDown
  • 一些便捷功能,如临时文件

下面介绍tidb在测试中一些有趣的用法。在tidb的测试用例里边会用到testkit和testleak两个工具包。

testkit

主要是提供一些便捷的工具,例如查询并检查错误,比较查询结果等,对照使用即可。

testleak

主要是提供了一个检测goroutine泄露的工具。使用很方便,只需要在开头加一个defer语句就可以了。原理也不复杂,就是通过runtime.Stack()得到所有正在运行的goroutine的堆栈,排除一些已知的堆栈,如果还剩下一些未知的,那可能就是有问题了。详细的可以看看tidb的源代码。