作者:京东零售 石磊
TiDB 作为开源 NewSQL 数据库的典型代表之一,同样支持 SQL,支持事务 ACID 特性。在通讯协议上,TiDB 选择与 MySQL 完全兼容,并尽可能兼容 MySQL 的语法。因此,基于 MySQL 数据库开发的系统,大多数可以平滑迁移至 TiDB,而几乎不用修改代码。对用户来说,迁移成本极低,过渡自然。
然而,仍有一些 MySQL 的特性和行为,TiDB 目前暂时不支持或表现与 MySQL 有差异。除此之外,TiDB 提供了一些扩展语法和功能,为用户提供更多的便利。
TiDB 仍处在快速发展的道路上,对 MySQL 功能和行为的支持方面,正按 路线图 的规划在前行。
兼容策略
先从总体上概括 TiDB 和 MySQL 兼容策略,如下表:
| 通讯协议 | SQL语法 | 功能和行为 |
|---|---|---|
| 完全兼容 | 兼容绝大多数 | 兼容大多数 |
截至 4.0 版本,TiDB 与 MySQL 的区别总结如下表:
| | MySQL | TiDB |
|---|---|---|
| 隔离级别 | 支持读未提交、读已提交、可重复读、串行化,默认为可重复读 | 乐观事务支持快照隔离,悲观事务支持快照隔离和读已提交 |
| 锁机制 | 悲观锁 | 乐观锁、悲观锁 |
| 存储过程 | 支持 | 不支持 |
| 触发器 | 支持 | 不支持 |
| 事件 | 支持 | 不支持 |
| 自定义函数 | 支持 | 不支持 |
| 窗口函数 | 支持 | 部分支持 |
| JSON | 支持 | 不支持部分 MySQL 8.0 新增的函数 |
| 外键约束 | 支持 | 忽略外键约束 |
| 字符集 | | 只支持 ascii、latin1、binary、utf8、utf8mb4 |
| 增加/删除主键 | 支持 | 通过 alter-primary-key 配置开关提供 |
| CREATE TABLE tblName AS SELECT stmt | 支持 | 不支持 |
| CREATE TEMPORARY TABLE | 支持 | TiDB 忽略 TEMPORARY 关键字,按照普通表创建 |
| DML affected rows | 支持 | 不支持 |
| AutoRandom 列属性 | 不支持 | 支持 |
| Sequence 序列生成器 | 不支持 | 支持 |
三种方案比较
双写方案:同时往mysql和tidb写入数据,两个数据库数据完全保持同步
•优点:此方案最安全,作为兜底方案不需担心数据库回滚问题,因为数据完全一致,可以无缝回滚到mysql
•缺点:新方案,调研方案实现,成本较高
读写分离:数据写入mysql,从tidb读,具体方案是切换到线上以后,保持读写分离一周时间左右,这一周时间用来确定tidb数据库没有问题,再把写操作也切换到tidb
•优点: 切换过程,mysql和tidb数据保持同步,满足数据回滚到mysql方案
•缺点:mysql和tidb数据库同步存在延时,对部分写入数据要求实时查询的会导致查询失败,同时一旦整体切换到tidb,无法回切到mysql
直接切换:直接一步切换到tidb
•优点:切换过程最简单,成本最低
•缺点:此方案没有兜底方案,切换到tidb,无法再回切到mysql或者同步数据回mysql风险较大,无法保证数据是否可用
Django双写mysql与tidb策略
settings.py中新增配置
1 2# Dev Database settings 3DATABASES = { 4 'default': { 5 'ENGINE': 'django.db.backends.mysql', 6 'NAME': 'name', 7 'USER': 'root', 8 'PASSWORD': '123456', 9 'HOST': 'db', 10 }, 11 'replica': { 12 'ENGINE': 'django.db.backends.mysql', 13 'NAME': 'name', 14 'USER': 'root', 15 'PASSWORD': '123456', 16 'HOST': 'db', 17 }, 18 'bak': { 19 'ENGINE': 'django.db.backends.mysql', 20 'NAME': 'name', 21 'USER': 'root', 22 'PASSWORD': '123456', 23 'HOST': 'db', 24 }, 25} 26# 多重写入数据库配置 27MULTI_WRITE_DB = "bak"
双写中间件 basemodel.py
1import copy 2import logging 3import traceback 4from django.db import models, transaction, router 5from django.db.models.deletion import Collector 6from django.db.models import sql 7from django.db.models.sql.constants import CURSOR 8from jcdp.settings import MULTI_WRITE_DB, DATABASES 9 10multi_write_db = MULTI_WRITE_DB 11 12 13# 重写QuerySet 14class BaseQuerySet(models.QuerySet): 15 16 def create(self, **kwargs): 17 return super().create(**kwargs) 18 19 def update(self, **kwargs): 20 try: 21 rows = super().update(**kwargs) 22 if multi_write_db in DATABASES: 23 self._for_write = True 24 query = self.query.chain(sql.UpdateQuery) 25 query.add_update_values(kwargs) 26 with transaction.mark_for_rollback_on_error(using=multi_write_db): 27 query.get_compiler(multi_write_db).execute_sql(CURSOR) 28 except Exception: 29 logging.error(traceback.format_exc()) 30 raise 31 return rows 32 33 def delete(self): 34 try: 35 deleted, _rows_count = super().delete() 36 if multi_write_db in DATABASES: 37 del_query = self._chain() 38 del_query._for_write = True 39 del_query.query.select_for_update = False 40 del_query.query.select_related = False 41 collector = Collector(using=multi_write_db) 42 collector.collect(del_query) 43 collector.delete() 44 except Exception: 45 logging.error(traceback.format_exc()) 46 raise 47 return deleted, _rows_count 48 49 def raw(self, raw_query, params=None, translations=None, using=None): 50 try: 51 qs = super().raw(raw_query, params=params, translations=translations, using=using) 52 if multi_write_db in DATABASES: 53 super().raw(raw_query, params=params, translations=translations, using=multi_write_db) 54 except Exception: 55 logging.error(traceback.format_exc()) 56 raise 57 return qs 58 59 def bulk_create(self, objs, batch_size=None, ignore_conflicts=False): 60 try: 61 for obj in objs: 62 obj.save() 63 except Exception: 64 logging.error(traceback.format_exc()) 65 raise 66 # objs = super().bulk_create(objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts) 67 # if multi_write_db in DATABASES: 68 # self._db = multi_write_db 69 # super().bulk_create(objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts) 70 return objs 71 72 def bulk_update(self, objs, fields, batch_size=None): 73 try: 74 super().bulk_update(objs, fields, batch_size=batch_size) 75 if multi_write_db in DATABASES: 76 self._db = multi_write_db 77 super().bulk_update(objs, fields, batch_size=batch_size) 78 except Exception: 79 logging.error(traceback.format_exc()) 80 raise 81 82 83class BaseManager(models.Manager): 84 _queryset_class = BaseQuerySet 85 86 87class BaseModel(models.Model): 88 objects = BaseManager() 89 90 class Meta: 91 abstract = True 92 93 def delete( 94 self, using=None, *args, **kwargs 95 ): 96 try: 97 instance = copy.deepcopy(self) 98 super().delete(using=using, *args, **kwargs) 99 if multi_write_db in DATABASES: 100 super(BaseModel, instance).delete(using=multi_write_db, *args, **kwargs) 101 except Exception: 102 logging.error(traceback.format_exc()) 103 raise 104 105 def save_base(self, raw=False, force_insert=False, 106 force_update=False, using=None, update_fields=None): 107 try: 108 using = using or router.db_for_write(self.__class__, instance=self) 109 assert not (force_insert and (force_update or update_fields)) 110 assert update_fields is None or update_fields 111 cls = self.__class__ 112 # Skip proxies, but keep the origin as the proxy model. 113 if cls._meta.proxy: 114 cls = cls._meta.concrete_model 115 meta = cls._meta 116 # A transaction isn't needed if one query is issued. 117 if meta.parents: 118 context_manager = transaction.atomic(using=using, savepoint=False) 119 else: 120 context_manager = transaction.mark_for_rollback_on_error(using=using) 121 with context_manager: 122 parent_inserted = False 123 if not raw: 124 parent_inserted = self._save_parents(cls, using, update_fields) 125 self._save_table( 126 raw, cls, force_insert or parent_inserted, 127 force_update, using, update_fields, 128 ) 129 if multi_write_db in DATABASES: 130 super().save_base(raw=raw, 131 force_insert=raw, 132 force_update=force_update, 133 using=multi_write_db, 134 update_fields=update_fields) 135 # Store the database on which the object was saved 136 self._state.db = using 137 # Once saved, this is no longer a to-be-added instance. 138 self._state.adding = False 139 except Exception: 140 logging.error(traceback.format_exc()) 141 raise
上述配置完成以后,在每个应用的models.py中引用新的BaseModel类作为模型基类即可实现双写目的
1class DirectoryStructure(BaseModel): 2 """ 3 目录结构 4 """ 5 view = models.CharField(max_length=128, db_index=True) # 视图名称 eg:部门视图 项目视图 6 sub_view = models.CharField(max_length=128, unique=True, db_index=True) # 子视图名称 7 sub_view_num = models.IntegerField() # 子视图顺序号
注:目前该方法尚不支持多对多模型的双写情景,如有业务需求,还需重写ManyToManyField类,方法参考猴子补丁方式
迁移数据库过程踩坑记录
TIDB配置项差异:确认数据库配置:ONLY_FULL_GROUP_BY 禁用 (mysql默认禁用)
TIDB不支持事务savepoint,代码中需要显式关闭savepoint=False
TIDB由于是分布式数据库,对于自增主键字段的自增策略与mysq有差异,若业务代码会与主键id关联,需要注意
