Mysql遷移到TiDB雙寫數(shù)據(jù)庫兜底方案詳解
正文
TiDB 作為開源 NewSQL 數(shù)據(jù)庫的典型代表之一,同樣支持 SQL,支持事務(wù) ACID 特性。在通訊協(xié)議上,TiDB 選擇與 MySQL 完全兼容,并盡可能兼容 MySQL 的語法。因此,基于 MySQL 數(shù)據(jù)庫開發(fā)的系統(tǒng),大多數(shù)可以平滑遷移至 TiDB,而幾乎不用修改代碼。對用戶來說,遷移成本極低,過渡自然。
然而,仍有一些 MySQL 的特性和行為,TiDB 目前暫時(shí)不支持或表現(xiàn)與 MySQL 有差異。除此之外,TiDB 提供了一些擴(kuò)展語法和功能,為用戶提供更多的便利。
TiDB 仍處在快速發(fā)展的道路上,對 MySQL 功能和行為的支持方面,正按 路線圖 的規(guī)劃在前行。
兼容策略
先從總體上概括 TiDB 和 MySQL 兼容策略,如下表:
通訊協(xié)議 | SQL語法 | 功能和行為 |
---|---|---|
完全兼容 | 兼容絕大多數(shù) | 兼容大多數(shù) |
截至 4.0 版本,TiDB 與 MySQL 的區(qū)別總結(jié)如下表:
? | MySQL | TiDB |
---|---|---|
隔離級別 | 支持讀未提交、讀已提交、可重復(fù)讀、串行化,默認(rèn)為可重復(fù)讀 | 樂觀事務(wù)支持快照隔離,悲觀事務(wù)支持快照隔離和讀已提交 |
鎖機(jī)制 | 悲觀鎖 | 樂觀鎖、悲觀鎖 |
存儲過程 | 支持 | 不支持 |
觸發(fā)器 | 支持 | 不支持 |
事件 | 支持 | 不支持 |
自定義函數(shù) | 支持 | 不支持 |
窗口函數(shù) | 支持 | 部分支持 |
JSON | 支持 | 不支持部分 MySQL 8.0 新增的函數(shù) |
外鍵約束 | 支持 | 忽略外鍵約束 |
字符集 | ? | 只支持 ascii、latin1、binary、utf8、utf8mb4 |
增加/刪除主鍵 | 支持 | 通過 alter-primary-key 配置開關(guān)提供 |
CREATE TABLE tblName AS SELECT stmt | 支持 | 不支持 |
CREATE TEMPORARY TABLE | 支持 | TiDB 忽略 TEMPORARY 關(guān)鍵字,按照普通表創(chuàng)建 |
DML affected rows | 支持 | 不支持 |
AutoRandom 列屬性 | 不支持 | 支持 |
Sequence 序列生成器 | 不支持 | 支持 |
三種方案比較
雙寫方案:同時(shí)往mysql和tidb寫入數(shù)據(jù),兩個(gè)數(shù)據(jù)庫數(shù)據(jù)完全保持同步
•優(yōu)點(diǎn):此方案最安全,作為兜底方案不需擔(dān)心數(shù)據(jù)庫回滾問題,因?yàn)閿?shù)據(jù)完全一致,可以無縫回滾到mysql
•缺點(diǎn):新方案,調(diào)研方案實(shí)現(xiàn),成本較高
讀寫分離:數(shù)據(jù)寫入mysql,從tidb讀,具體方案是切換到線上以后,保持讀寫分離一周時(shí)間左右,這一周時(shí)間用來確定tidb數(shù)據(jù)庫沒有問題,再把寫操作也切換到tidb
•優(yōu)點(diǎn): 切換過程,mysql和tidb數(shù)據(jù)保持同步,滿足數(shù)據(jù)回滾到mysql方案
•缺點(diǎn):mysql和tidb數(shù)據(jù)庫同步存在延時(shí),對部分寫入數(shù)據(jù)要求實(shí)時(shí)查詢的會導(dǎo)致查詢失敗,同時(shí)一旦整體切換到tidb,無法回切到mysql
直接切換:直接一步切換到tidb
•優(yōu)點(diǎn):切換過程最簡單,成本最低
•缺點(diǎn):此方案沒有兜底方案,切換到tidb,無法再回切到mysql或者同步數(shù)據(jù)回mysql風(fēng)險(xiǎn)較大,無法保證數(shù)據(jù)是否可用
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', }, } # 多重寫入數(shù)據(jù)庫配置 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
上述配置完成以后,在每個(gè)應(yīng)用的models.py中引用新的BaseModel類作為模型基類即可實(shí)現(xiàn)雙寫目的
class DirectoryStructure(BaseModel): """ 目錄結(jié)構(gòu) """ view = models.CharField(max_length=128, db_index=True) # 視圖名稱 eg:部門視圖 項(xiàng)目視圖 sub_view = models.CharField(max_length=128, unique=True, db_index=True) # 子視圖名稱 sub_view_num = models.IntegerField() # 子視圖順序號
注:目前該方法尚不支持多對多模型的雙寫情景,如有業(yè)務(wù)需求,還需重寫ManyToManyField類,方法參考猴子補(bǔ)丁方式
遷移數(shù)據(jù)庫過程踩坑記錄
TIDB配置項(xiàng)差異:確認(rèn)數(shù)據(jù)庫配置:ONLY_FULL_GROUP_BY 禁用 (mysql默認(rèn)禁用)
TIDB不支持事務(wù)savepoint,代碼中需要顯式關(guān)閉savepoint=False
TIDB由于是分布式數(shù)據(jù)庫,對于自增主鍵字段的自增策略與mysq有差異,若業(yè)務(wù)代碼會與主鍵id關(guān)聯(lián),需要注意
以上就是Mysql遷移到TiDB雙寫數(shù)據(jù)庫兜底方案詳解的詳細(xì)內(nèi)容,更多關(guān)于Mysql遷移TiDB雙寫數(shù)據(jù)庫的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Mysql數(shù)據(jù)庫值的添加、修改、刪除及清空操作實(shí)例
這篇文章主要給大家介紹了關(guān)于Mysql數(shù)據(jù)庫值的添加、修改、刪除及清空操作的相關(guān)資料,文中通過示例代碼以及圖文介紹的非常詳細(xì),需要的朋友可以參考下2021-06-06MySQL中CONCAT和GROUP_CONCAT方法的區(qū)別詳解
本文主要介紹了MySQL中CONCAT和GROUP_CONCAT方法的區(qū)別詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01mysql 搜尋附近N公里內(nèi)數(shù)據(jù)的簡單實(shí)例
下面小編就為大家?guī)硪黄猰ysql 搜尋附近N公里內(nèi)數(shù)據(jù)的簡單實(shí)例。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-04-04MYSQL與SQLserver之間存儲過程的轉(zhuǎn)換方式
這篇文章主要介紹了MYSQL與SQLserver之間存儲過程的轉(zhuǎn)換方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-11-11