TiDB 入门

前言

基于 MySQL 构建的传统关系型数据库服务已经难于支撑公司业务的爆发式增长,另一方面是要进行云原生的推进,项目部门决定使用分布式数据库,虑到技术架构发展潜力、稳定性、以及与 MySQL 的兼容性,最终敲定了基于 TiDB 数据库进行二次开发的整体方案。

一、TiDB 快速上手

关于 TiDB 的简介以及优点,私以为如果没有实践过是很难体会的,我们先进行快速入门,再进行深入探讨。

使用 Docker Compose 快速构建 TiDB 集群

使用 Docker 部署 TiDB 是因为只适用于进行学习以及体验,或者用于开发环境,对于生产环境,不要使用 Docker 进行部署。

在部署之前,我已经假定你已经学习并安装过了 Docker 和 docker-compose ,如果你想学习 Docker ,可以参考我的另一篇博文 6w字教程入门 Docker 。

  1. 下载 tidb-docker-compose

    [aspire@localhost apps]$ git clone https://github.com/pingcap/tidb-docker-compose.git
    [aspire@localhost apps]$ ll
    drwxrwxr-x. 14 aspire aspire 4096 4月  28 12:27 tidb-docker-compose
    
  2. 启动 TiDB 集群。

    从 docker-compose.yml 文件看出,默认情况下会启动 3个 PD,3个 TiKV,1个 TiDB 和监控组件Prometheus,Pushgateway,Grafana 以及 tidb-vision,这意味着你可能需要足够的内存。

    关于什么是 PD ,这些概念我们之后会讲到,这里只需要知道 TiDB 的运行依赖于 PD、TiKV、TiDB 三个部分即可。

    [aspire@localhost tidb-docker-compose]$ cd tidb-docker-compose
    [aspire@localhost tidb-docker-compose]$ docker-compose up -d
    
  3. 访问集群。

    你可能已经注意到了,新建的连接类型为 MySQL,这是因为 TiDB 兼容 MySQL,这也就意味着数据库迁移后你几乎无需改动代码。

    本人在虚拟机上的 docker 里安装的 TiDB ,输入 IP 地址和端口,这里的端口指的是 TiDB 的端口,可以在 docker-compose.yml 文件中查看,默认端口为4000,用户名为 root ,密码无。
    TiDB 入门_第1张图片

  4. 你可以像操作 MySQL 一样操作 TiDB。
    TiDB 入门_第2张图片

  5. 访问集群 Grafana 监控页面:http://localhost:3000 默认用户名和密码均为 admin。
    TiDB 入门_第3张图片

  6. http://localhost:9090 访问 TiDB 的 Prometheus 管理界面。
    TiDB 入门_第4张图片

  7. http://localhost:8010/查看 tidb-vision
    TiDB 入门_第5张图片

MySQL 迁移 TiDB

这部分以 Springboot-2.1.9 + Mybatis + Druid 为例,演示数据库部分的改造。改造前项目 Demo 的部分代码以及配置文件如下:

spring.datasource.url=jdbc:mysql://localhost:3306/mmststdb?characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
spring.datasource.maxWait=60000 
spring.datasource.timeBetweenEvictionRunsMillis=60000 #每60秒运行一次空闲连接回收器(独立线程)
spring.datasource.minEvictableIdleTimeMillis=300000 #池中的连接空闲300秒后被回收
spring.datasource.validationQuery=SELECT 1 FROM DUAL #验证连接是否可用使用的SQL语句
spring.datasource.testWhileIdle=true #检验连接是否被空闲连接回收器回收,如果检测失败,则连接将被从池中去除.
spring.datasource.testOnBorrow=true #借出连接时检验是否有效,做了这个配置会降低性能。
spring.datasource.testOnReturn=true #归还连接时检验是否有效,做了这个配置会降低性能。
spring.datasource.poolPreparedStatements=true #是否缓存preparedStatement
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20 #缓存preparedStatement数量
spring.datasource.filters=stat,wall #通过别名的方式配置扩展插件,stat用于监控,wall防止SQL注入
spring.datasource.connectionProperties=druid.stat.mergeSql=true; druid.stat.slowSqlMillis=5000 #合并执行的相同sql,监控慢SQL

mybatis.mapper-locations=classpath:mapper/*.xml
<mapper namespace="com.example.demo.dao.EcMapper">
    <select id="getEcById" resultType="map">
    	select * from card_data_dict where id = #{id}
    </select>
</mapper>

接下来进行数据库的迁移,使用 navicat 将原有的表结构以及数据导出之后,在 TiDB 的 test 库中运行 SQL 文件,可以看到执行成功,表结构和数据完整的迁移过来。
TiDB 入门_第6张图片对于代码方面,你无需做什么修改,包括 maven 依赖,和数据库驱动都无需改动,你只需要配置迁移后的连接地址和数据库即可。

spring.datasource.url=jdbc:mysql://192.168.71.128:4000/test?characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=
TiDB 不支持的特性
  • 外键约束
  • 存储过程与函数
  • 触发器
  • 事件
  • 非 ASCII、latin1、binary、UTF8、UTF8MB4 的字符集
  • BINARY 之外的排序规则
  • 增加或删除主键
  • Savepoints
  • CREATE TABLE tableName AS SELECT statement 语法(但是支持 create table tableName1 LIKE tableName2
  • 创建临时表(CREATE TEMPORARY TABLE 语法)
TiDB 与 MySQL 兼容性对比

TiDB 支持 MySQL 传输协议及其绝大多数的语法,不过一些特性由于在分布式环境下没法很好的实现,目前暂时不支持或者是表现与 MySQL 有差异。

  • 默认的排序规则不同

    在上一部分中迁移的表结构中,可以查看到,对于 varchar 类型的字段,在 MySQL 中的排序规则为 utf8_general_ci ,但是在 TiDB 中的排序规则为 utf8_bin
    TiDB 入门_第7张图片TiDB 入门_第8张图片

  • EXPLAIN 查询计划

    TiDB 的查询计划(EXPLAIN/EXPLAIN FOR)输出格式与 MySQL 差别较大,同时 EXPLAIN FOR 的输出内容与权限设置与 MySQL 不一致。参考官方文档:理解 TiDB 执行计划
    TiDB 入门_第9张图片TiDB 入门_第10张图片

  • 内置函数

    TiDB 支持常用的 MySQL 内置函数,但是不是所有的函数都已经支持,具体请参考 语法文档。

  • DDL 语句

    在 TiDB 中,运行的 DDL 操作不会影响对表的读取或写入。但是,目前 DDL 变更有如下一些限制:
    Add Index
    1、不支持同时创建多个索引
    2、不支持通过 ALTER TABLE 在所生成的列上添加索引

    Add Column
    1、不支持同时创建多个列
    2、 不支持将新创建的列设为主键或唯一索引,也不支持将此列设成 auto_increment 属性

    Drop Column
    1、不支持删除主键列或索引列

    Change/Modify Column
    1、不支持有损变更,比如从 BIGINT 变为 INTEGER,或者从 VARCHAR(255) 变为 VARCHAR(10)
    2、不支持修改 DECIMAL 类型的精度
    3、不支持更改 UNSIGNED 属性
    4、只支持将 CHARACTER SET 属性从 utf8 更改为 utf8mb4

    Alter Database
    1、只支持将 CHARACTER SET 属性从 utf8 更改为 utf8mb4

  • 视图

    目前 TiDB 不支持对视图进行 UPDATE、INSERT、DELETE 等写入操作。

二、TiDB 简介

TiDB 是 PingCAP 公司设计的开源分布式 HTAP (Hybrid Transactional and Analytical Processing) 数据库,结合了传统的 RDBMS 和 NoSQL 的最佳特性。TiDB 兼容 MySQL,支持无限的水平扩展,具备强一致性和高可用性。TiDB 的目标是为 OLTP (Online Transactional Processing) 和 OLAP (Online Analytical Processing) 场景提供一站式的解决方案。

可以使用 TiDB-Wasm 一键体验 TiDB 数据库,打开网页之后会在内存中构建 TiDB 数据库。TiDB-Wasm 可直接进行 SQL 执行、兼容性验证等基本操作。

分布式数据库

分布式数据库是多个互连的数据库,他们通常位于多个服务器上,但彼此互相通信以实现共同目标。分布式数据库和数据库集群都能实现数据库的高可用,他们的区别在于:分布式是指将业务拆分然后分布在不同的地方,而集群指的是将几台服务器集中在一起,实现同一业务。分布式中的每一个节点,都可以做集群,而集群并不一定就是分布式的。 分布式是服务的拆分,每个部分都只有完整数据的一部分;而集群是服务器的克隆,集群中的每个部分的数据都和同一集群中的其他节点的数据一致。

RDBMS、DBMS

Relational Database Management System,关系数据库管理系统,与之相对应的是非关系型数据库(NoSQL),一个关系数据库管理系统(RDBMS)是数据库管理系统(DBMS)中的一种。

关系型数据库是依据"一对一、一对多、多对多"等关系模型来创建的数据库,包括数据结构(数据存储的问题,二维表)、操作指令集合(SQL语句)、完整性约束(表内数据约束、表与表之间的约束),常见的关系型数据库有Oracle、Microsoft SQL Server、MySQL。

非关系数据库主要采用列模型、键值对模型或文档类模型来存储数据,常见的有 Redis、MongoDB。

OLTP、OLAP

对数据处理大致可以分成两大类:联机事务处理OLTP(on-line transaction processing)、联机分析处理OLAP(On-Line Analytical Processing)。从功能角度来看,OLTP 负责基本业务的正常运转,而业务数据积累时所产生的价值信息则被 OLAP 不断呈现。OLTP 和 OLAP 解决的不是同一个问题,不存在竞争关系,而是相互补充的过程。

Raft 协议

Raft 有效的解决了分布式一致性的问题,Raft 协议可以使得一个集群的服务器组成复制状态机。一个分布式的复制状态机系统由多个复制单元组成,每个复制单元均是一个状态机,它的状态保存在一组状态变量中,状态机的变量只能通过外部命令来改变。简单理解的话,可以想象成是一组服务器,每个服务器是一个状态机,服务器的运行状态只能通过一行行的命令来改变。每一个状态机存储一个包含一系列指令的日志,严格按照顺序逐条执行日志中的指令,如果所有的状态机都能按照相同的日志执行指令,那么它们最终将达到相同的状态。因此,在复制状态机模型下,只要保证了操作日志的一致性,我们就能保证该分布式系统状态的一致性。

在 Raft 体系中,有一个强 leader,接受来自客户端的指令,并写入到自己的日志中,然后通过一致性模块和其他服务器交互,确保每一条日志都能以相同顺序写入到其他服务器的日志中,即便服务器宕机了一段时间。一旦日志命令都被正确的复制,每一台服务器就会顺序的处理命令,并向客户端返回结果。

Raft 协议在集群初始状态下是没有 Leader 的, 集群中所有成员均是 Follower,在选举开始期间所有 Follower 均可参与选举,这时所有 Follower 的角色均转变为 Condidate, Leader 由集群中所有的 Condidate 投票选出,最后获得投票最多的 Condidate 获胜,其角色转变为 Leader 并开始其任期,其余落败的 Condidate 角色转变为 Follower 开始服从 Leader 领导。这里有一种意外的情况会选不出 Leader 就是所有 Condidate 均投票给自己,这样无法决出票数多的一方,Raft 算法为了解决这个问题引入了北洋时期袁世凯获选大总统的谋略,即选不出 Leader 不罢休,直到选出为止,一轮选不出 Leader,便令所有 Condidate 随机 sleap(Raft 论文称为 timeout)一段时间,然后马上开始新一轮的选举,这里的随机 sleep 就起了很关键的因素,第一个从 sleap 状态恢复过来的 Condidate 会向所有 Condidate 发出投票给我的申请,这时还没有苏醒的 Condidate 就只能投票给已经苏醒的 Condidate ,因此可以有效解决 Condiadte 均投票给自己的故障,便可快速的决出 Leader。

选举出 Leader 后 Leader 会定期向所有 Follower 发送 heartbeat 来维护其 Leader 地位,如果 Follower 一段时间后未收到 Leader 的心跳则认为 Leader 已经挂掉,便转变自身角色为 Condidate,同时发起新一轮的选举,产生新的 Leader。

整体架构

TiDB 集群主要包括三个核心组件:TiDB Server,PD Server 和 TiKV Server。此外,还有用于解决用户复杂 OLAP 需求的 TiSpark 组件和简化云上部署管理的 TiDB Operator 组件。
TiDB 入门_第11张图片TiDB Server

TiDB Server 负责接收 SQL 请求,处理 SQL 相关的逻辑,并通过 PD 找到存储计算所需数据的 TiKV 地址,与 TiKV 交互获取数据,最终返回结果。TiDB Server 是无状态的,其本身并不存储数据,只负责计算,可以无限水平扩展,可以通过负载均衡组件(如LVS、HAProxy 或 F5)对外提供统一的接入地址。

PD Server

Placement Driver (简称 PD) 是整个集群的管理模块,其主要工作有三个:一是存储集群的元信息(某个 Key 存储在哪个 TiKV 节点);二是对 TiKV 集群进行调度和负载均衡(如数据的迁移、Raft group leader 的迁移等);三是分配全局唯一且递增的事务 ID。

PD 通过 Raft 协议保证数据的安全性。Raft 的 leader server 负责处理所有操作,其余的 PD server 仅用于保证高可用。建议部署奇数个 PD 节点。

TiKV Server

TiKV Server 负责存储数据,从外部看 TiKV 是一个分布式的提供事务的 Key-Value 存储引擎。存储数据的基本单位是 Region,每个 Region 负责存储一个 Key Range(从 StartKey 到 EndKey 的左闭右开区间)的数据,每个 TiKV 节点会负责多个 Region。TiKV 使用 Raft 协议做复制,保持数据的一致性和容灾。副本以 Region 为单位进行管理,不同节点上的多个 Region 构成一个 Raft Group,互为副本。数据在多个 TiKV 之间的负载均衡由 PD 调度,这里也是以 Region 为单位进行调度。

水平扩展

无限水平扩展是 TiDB 的一大特点,这里说的水平扩展包括两方面:计算能力和存储能力。TiDB Server 负责处理 SQL 请求,随着业务的增长,可以简单的添加 TiDB Server 节点,提高整体的处理能力,提供更高的吞吐。TiKV 负责存储数据,随着数据量的增长,可以部署更多的 TiKV Server 节点解决数据 Scale 的问题。PD 会在 TiKV 节点之间以 Region 为单位做调度,将部分数据迁移到新加的节点上。所以在业务的早期,可以只部署少量的服务实例(推荐至少部署 3 个 TiKV, 3 个 PD,2 个 TiDB),随着业务量的增长,按照需求添加 TiKV 或者 TiDB 实例。

高可用
  • TiDB
    TiDB 是无状态的,推荐至少部署两个实例,前端通过负载均衡组件对外提供服务。当单个实例失效时,会影响正在这个实例上进行的 Session,从应用的角度看,会出现单次请求失败的情况,重新连接后即可继续获得服务。单个实例失效后,可以重启这个实例或者部署一个新的实例。
  • PD
    PD 是一个集群,通过 Raft 协议保持数据的一致性,单个实例失效时,如果这个实例不是 Raft 的 leader,那么服务完全不受影响;如果这个实例是 Raft 的 leader,会重新选出新的 Raft leader,自动恢复服务。PD 在选举的过程中无法对外提供服务,这个时间大约是3秒钟。推荐至少部署三个 PD 实例,单个实例失效后,重启这个实例或者添加新的实例。
  • TiKV
    TiKV 是一个集群,通过 Raft 协议保持数据的一致性(副本数量可配置,默认保存三副本),并通过 PD 做负载均衡调度。单个节点失效时,会影响这个节点上存储的所有 Region。对于 Region 中的 Leader 节点,会中断服务,等待重新选举;对于 Region 中的 Follower 节点,不会影响服务。当某个 TiKV 节点失效,并且在一段时间内(默认 30 分钟)无法恢复,PD 会将其上的数据迁移到其他的 TiKV 节点上。

三、事务

事务的特性

事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性,一个事务中的一系列的操作要么全部成功,要么一个都不做。

一般来说,事务是必须满足4个条件(ACID):原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。

  • 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

  • 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。

  • 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。

    在并发事务没有进行隔离的情况下,会发生如下问题:

    1. 脏读,脏读指一个事务读取了另外一个事务未提交的数据。
    2. 不可重复读,不可重复读指在一个事务内多次读取表中的某一行数据,读取的结果却不同。然而,不可重复读在大多数应用场景中并不是什么问题。这里举一个极端的例子,A、B二人要进行财务对账,两人在同一个事务中进行查询,A先查到的结果为100元,在B查询之前,有一个并发过来修改了这个结果导致B查询的时候这个钱变成了200元,这就导致A、B两个人统计出来的钱不一样。
    3. 幻读,有叫虚读,指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。注意不可重复读是读到了别的事务的 update 语句的结果,而幻读是读到了别的事务的 insert 语句的结果。

    事务隔离分为不同级别,包括读未提交(Read uncommitted)、读已提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。

    1. 读未提交是隔离级别最低的一种事务级别。在这种隔离级别下,会引发脏读、不可重复读和幻读。
    2. 读已提交,读到的都是别人提交后的值。这种隔离级别下,会引发不可重复读和幻读,但避免了脏读。
    3. 可重复读,这种隔离级别下,会引发幻读,但避免了脏读、不可重复读。
    4. 串行化,以单线程的方式进行,所有事务按照次序依次执行。脏读、不可重复读、幻读都不会出现。。
  • 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

事务能够通过 AID 来保证这个 C 的过。C 是目的,AID 都是手段。事务的一致性决定了一个系统设计和实现的复杂度,也导致了事务的不同隔离级别。通常,在工程实践中,为了性能的考虑会对隔离性进行折中。

  • 强一致性:也称为原子一致性,更新过的数据能被后续所有的访问都能看到。也就是说,在任意时刻,所有节点中的数据是一样的。
  • 弱一致性:数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。最终一致性就属于弱一致性。
TiDB 事务

TiDB的事务隔离级别并没有完全支持ANSI标准中的所有隔离级别,它缺省的隔离级别是"Snapshot Isolation"(简称SI),这种隔离级别类似于 ANSI 标准中的 “可重复读”(简称RR),但是与它又不完全相同:RR会发生“幻读”,但是SI不会发生;RR不会发生“写偏斜(write skew)”,而SI会发生。请参考 TiDB事务隔离级别和并发控制的特点详述

自 v3.0.8 开始,TiDB 默认使用悲观事务模型。悲观事务的行为和 MySQL 基本一致。

  • SELECT FOR UPDATE 会读取已提交的最新数据,并对读取到的数据加悲观锁。
  • UPDATEDELETEINSERT 语句都会读取已提交的最新的数据来执行,并对修改的数据加悲观锁。
  • 当一行数据被加了悲观锁以后,其他尝试修改这一行的写事务会被阻塞,等待悲观锁的释放。
  • 当一行数据被加了悲观锁以后,其他尝试读取这一行的事务不会被阻塞,可以读到已提交的数据。
  • 事务提交或回滚的时候,会释放所有的锁。

更多详情参加官方文档。

参考资料

[1] TiDB官方文档
[2] TiDB 在爱奇艺的应用及实践
[3] 新一代数据库TiDB在美团的实践
[4] TiDB与 MySQL 兼容性对比
[5] OLTP与OLAP:在新IT环境下的相互结合
[6] Raft协议原理详解
[7] 说一说那些我也不太懂的 Raft 协议
[8] TiDB事务隔离级别和并发控制的特点详述

你可能感兴趣的:(数据库)