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

