头图

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

作者:京东零售 石磊

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

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

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

兼容策略

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

通讯协议SQL语法功能和行为
完全兼容兼容绝大多数兼容大多数

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

MySQLTiDB
隔离级别支持读未提交、读已提交、可重复读、串行化,默认为可重复读乐观事务支持快照隔离,悲观事务支持快照隔离和读已提交
锁机制悲观锁乐观锁、悲观锁
存储过程支持不支持
触发器支持不支持
事件支持不支持
自定义函数支持不支持
窗口函数支持部分支持
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中新增配置

# Dev Database settings
DATABASES = {
    '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 copy
import logging
import traceback
from django.db import models, transaction, router
from django.db.models.deletion import Collector
from django.db.models import sql
from django.db.models.sql.constants import CURSOR
from jcdp.settings import MULTI_WRITE_DB, DATABASES

multi_write_db = MULTI_WRITE_DB


# 重写QuerySet
class 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关联,需要注意。


京东云技术新知
京东云最新产品信息、技术干货以及最新活动发布,拥抱技术,与开发者携手创造未来!

京东云开发者(Developer of JD Technology)是京东云旗下为AI、云计算、IoT等相关领域开发者提供技术分...

1.9k 声望
5.1k 粉丝
0 条评论
推荐阅读
深入浅出RPC服务 | 不同层的网络协议
本系列文章从RPC产生的历史背景开始讲解,涉及RPC核心原理、RPC实现、JSF的实现等,通过图文类比的方式剖析它的内部世界,让大家对RPC的设计思想有一个宏观的认识。作者:王禹展   京东健康

京东云开发者

封面图
万字详解,吃透 MongoDB!
MongoDB 是一个基于 分布式文件存储 的开源 NoSQL 数据库系统,由 C++ 编写的。MongoDB 提供了 面向文档 的存储方式,操作起来比较简单和容易,支持“无模式”的数据建模,可以存储比较复杂的数据类型,是一款非常...

JavaGuide8阅读 1.7k

封面图
万字长文~vue+express+mysql带你彻底搞懂项目中的权限控制(附所有源码)
所谓的权限,其实指的就是:用户是否能看到,以及是否允许其对数据进行增删改查的操作,因为现在开发项目的主流方式是前后端分离,所以整个项目的权限是后端权限控制搭配前端权限控制共同实现的

水冗水孚11阅读 1.5k

花了几个月时间把 MySQL 重新巩固了一遍,梳理了一篇几万字 “超硬核” 的保姆式学习教程!(持续更新中~)
MySQL 是最流行的关系型数据库管理系统,在 WEB 应用方面 MySQL 是最好的 RDBMS(Relational Database Management System:关系数据库管理系统)应用软件之一。

民工哥11阅读 1.1k

封面图
一次偶然机会发现的MySQL“负优化”
今天要讲的这件事和上述的两个sql有关,是数年前遇到的一个关于MySQL查询性能的问题。主要是最近刷到了一些关于MySQL查询性能的文章,大部分文章中讲到的都只是一些常见的索引失效场合,于是我回想起了当初被那个...

骑牛上青山8阅读 2.3k评论 2

程序员英语学习指南
动机为什么程序员要学习英语?工作:我们每天接触的代码都是英文的、包括很多技术文档也是英文的学习:最新最前沿的技术最开始都是只有English版本就业:学好英语让你的就业范围扩大到全球,而不只限于国内目标读...

九旬6阅读 649

又一款内存数据库横空出世,比 Redis 更强,性能直接飙升一倍!杀疯了
KeyDB是Redis的高性能分支,专注于多线程,内存效率和高吞吐量。除了多线程之外,KeyDB还具有仅在Redis Enterprise中可用的功能,例如Active Replication,FLASH存储支持以及一些根本不可用的功能,例如直接备份...

民工哥4阅读 796评论 1

封面图

京东云开发者(Developer of JD Technology)是京东云旗下为AI、云计算、IoT等相关领域开发者提供技术分...

1.9k 声望
5.1k 粉丝
宣传栏