写点什么

Mysql 到 TiDB 迁移,双写数据库兜底方案

  • 2022-12-27
    北京
  • 本文字数:4049 字

    阅读完需:约 13 分钟

Mysql到TiDB迁移,双写数据库兜底方案

作者:京东零售 石磊


TiDB 作为开源 NewSQL 数据库的典型代表之一,同样支持 SQL,支持事务 ACID 特性。在通讯协议上,TiDB 选择与 MySQL 完全兼容,并尽可能兼容 MySQL 的语法。因此,基于 MySQL 数据库开发的系统,大多数可以平滑迁移至 TiDB,而几乎不用修改代码。对用户来说,迁移成本极低,过渡自然。


然而,仍有一些 MySQL 的特性和行为,TiDB 目前暂时不支持或表现与 MySQL 有差异。除此之外,TiDB 提供了一些扩展语法和功能,为用户提供更多的便利。


TiDB 仍处在快速发展的道路上,对 MySQL 功能和行为的支持方面,正按 路线图 的规划在前行。

兼容策略

先从总体上概括 TiDB 和 MySQL 兼容策略,如下表:



截至 4.0 版本,TiDB 与 MySQL 的区别总结如下表:


三种方案比较

双写方案:同时往 mysql 和 tidb 写入数据,两个数据库数据完全保持同步


•优点:此方案最安全,作为兜底方案不需担心数据库回滚问题,因为数据完全一致,可以无缝回滚到 mysql


•缺点:新方案,调研方案实现,成本较高


读写分离:数据写入 mysql,从 tidb 读,具体方案是切换到线上以后,保持读写分离一周时间左右,这一周时间用来确定 tidb 数据库没有问题,再把写操作也切换到 tidb


•优点: 切换过程,mysql 和 tidb 数据保持同步,满足数据回滚到 mysql 方案


•缺点:mysql 和 tidb 数据库同步存在延时,对部分写入数据要求实时查询的会导致查询失败,同时一旦整体切换到 tidb,无法回切到 mysql


直接切换:直接一步切换到 tidb


•优点:切换过程最简单,成本最低


•缺点:此方案没有兜底方案,切换到 tidb,无法再回切到 mysql 或者同步数据回 mysql 风险较大,无法保证数据是否可用

Django 双写 mysql 与 tidb 策略

settings.py中新增配置
复制代码



# Dev Database settingsDATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'name', 'USER': 'root', 'PASSWORD': '123456', 'HOST': 'db', }, 'replica': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'name', 'USER': 'root', 'PASSWORD': '123456', 'HOST': 'db', }, 'bak': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'name', 'USER': 'root', 'PASSWORD': '123456', 'HOST': 'db', },}# 多重写入数据库配置MULTI_WRITE_DB = "bak"
复制代码


双写中间件 basemodel.py


import copyimport loggingimport tracebackfrom django.db import models, transaction, routerfrom django.db.models.deletion import Collectorfrom django.db.models import sqlfrom django.db.models.sql.constants import CURSORfrom jcdp.settings import MULTI_WRITE_DB, DATABASES
multi_write_db = MULTI_WRITE_DB

# 重写QuerySetclass BaseQuerySet(models.QuerySet):
def create(self, **kwargs): return super().create(**kwargs)
def update(self, **kwargs): try: rows = super().update(**kwargs) if multi_write_db in DATABASES: self._for_write = True query = self.query.chain(sql.UpdateQuery) query.add_update_values(kwargs) with transaction.mark_for_rollback_on_error(using=multi_write_db): query.get_compiler(multi_write_db).execute_sql(CURSOR) except Exception: logging.error(traceback.format_exc()) raise return rows
def delete(self): try: deleted, _rows_count = super().delete() if multi_write_db in DATABASES: del_query = self._chain() del_query._for_write = True del_query.query.select_for_update = False del_query.query.select_related = False collector = Collector(using=multi_write_db) collector.collect(del_query) collector.delete() except Exception: logging.error(traceback.format_exc()) raise return deleted, _rows_count
def raw(self, raw_query, params=None, translations=None, using=None): try: qs = super().raw(raw_query, params=params, translations=translations, using=using) if multi_write_db in DATABASES: super().raw(raw_query, params=params, translations=translations, using=multi_write_db) except Exception: logging.error(traceback.format_exc()) raise return qs
def bulk_create(self, objs, batch_size=None, ignore_conflicts=False): try: for obj in objs: obj.save() except Exception: logging.error(traceback.format_exc()) raise # objs = super().bulk_create(objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts) # if multi_write_db in DATABASES: # self._db = multi_write_db # super().bulk_create(objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts) return objs
def bulk_update(self, objs, fields, batch_size=None): try: super().bulk_update(objs, fields, batch_size=batch_size) if multi_write_db in DATABASES: self._db = multi_write_db super().bulk_update(objs, fields, batch_size=batch_size) except Exception: logging.error(traceback.format_exc()) raise

class BaseManager(models.Manager): _queryset_class = BaseQuerySet

class BaseModel(models.Model): objects = BaseManager()
class Meta: abstract = True
def delete( self, using=None, *args, **kwargs ): try: instance = copy.deepcopy(self) super().delete(using=using, *args, **kwargs) if multi_write_db in DATABASES: super(BaseModel, instance).delete(using=multi_write_db, *args, **kwargs) except Exception: logging.error(traceback.format_exc()) raise
def save_base(self, raw=False, force_insert=False, force_update=False, using=None, update_fields=None): try: using = using or router.db_for_write(self.__class__, instance=self) assert not (force_insert and (force_update or update_fields)) assert update_fields is None or update_fields cls = self.__class__ # Skip proxies, but keep the origin as the proxy model. if cls._meta.proxy: cls = cls._meta.concrete_model meta = cls._meta # A transaction isn't needed if one query is issued. if meta.parents: context_manager = transaction.atomic(using=using, savepoint=False) else: context_manager = transaction.mark_for_rollback_on_error(using=using) with context_manager: parent_inserted = False if not raw: parent_inserted = self._save_parents(cls, using, update_fields) self._save_table( raw, cls, force_insert or parent_inserted, force_update, using, update_fields, ) if multi_write_db in DATABASES: super().save_base(raw=raw, force_insert=raw, force_update=force_update, using=multi_write_db, update_fields=update_fields) # Store the database on which the object was saved self._state.db = using # Once saved, this is no longer a to-be-added instance. self._state.adding = False except Exception: logging.error(traceback.format_exc()) raise
复制代码


上述配置完成以后,在每个应用的 models.py 中引用新的 BaseModel 类作为模型基类即可实现双写目的


class DirectoryStructure(BaseModel):    """    目录结构    """    view = models.CharField(max_length=128, db_index=True)  # 视图名称 eg:部门视图 项目视图    sub_view = models.CharField(max_length=128, unique=True, db_index=True)  # 子视图名称    sub_view_num = models.IntegerField()  # 子视图顺序号
复制代码


注:目前该方法尚不支持多对多模型的双写情景,如有业务需求,还需重写 ManyToManyField 类,方法参考猴子补丁方式


迁移数据库过程踩坑记录


TIDB 配置项差异:确认数据库配置:ONLY_FULL_GROUP_BY 禁用 (mysql 默认禁用)


TIDB 不支持事务 savepoint,代码中需要显式关闭 savepoint=False


TIDB 由于是分布式数据库,对于自增主键字段的自增策略与 mysq 有差异,若业务代码会与主键 id 关联,需要注意

发布于: 刚刚阅读数: 4
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
Mysql到TiDB迁移,双写数据库兜底方案_数据库_京东科技开发者_InfoQ写作社区