本系列文章主要是本人在游戲服務(wù)端開發(fā)過程中,遇到的一些不那么為人熟知但我又覺得比較重要的MySQL知識的介紹。希望里面淺薄的文字能為了提供一點(diǎn)點(diǎn)的幫助。
本章的目標(biāo)是介紹我使用數(shù)據(jù)庫過程中一些零碎的知識點(diǎn),單獨(dú)拎出來寫不了太多的東西所以整合成一章。
在不影響線上數(shù)據(jù)庫性能的情況下,如何知道一張表大概有多少行
有時(shí)候我們可能會通過一張表的數(shù)據(jù)量大致預(yù)估下一個(gè)業(yè)務(wù)的參與人數(shù)或者這個(gè)系統(tǒng)的使用程度。又或者將一個(gè)庫下所有表的數(shù)據(jù)量進(jìn)行分析,看看MySQL數(shù)據(jù)分布的情況。比如我想查詢test
庫下所有表的行數(shù)(大概行數(shù))可以用這條SQL:
SELECT `TABLE_NAME`,`TABLE_ROWS` FROM `information_schema`.`tables` WHERE `TABLE_SCHEMA`='test'
關(guān)于information_schema
.tables
表包含了MySQL所有表的一些基本情況,比如:大概行數(shù)、平均長度、最大長度等等。詳細(xì)介紹請戳這里。該表的數(shù)據(jù)會定期進(jìn)行統(tǒng)計(jì)。行數(shù)的統(tǒng)計(jì)方式也很有趣,是通過采樣統(tǒng)計(jì)的方式來統(tǒng)計(jì)的 —— 從InnoDB表中隨機(jī)選擇N個(gè)數(shù)據(jù)頁,算出這些頁包含數(shù)據(jù)行數(shù)的平均值,然后用總頁數(shù)乘以這個(gè)值就是了。當(dāng)然如果需要準(zhǔn)確確定一張表有多少行數(shù)據(jù)可以用SELECT COUNT(*) FROM TABLE_NAME
這條命令進(jìn)行,但是這樣會對數(shù)據(jù)庫產(chǎn)生性能影響(因?yàn)闀闅v這個(gè)表的整個(gè)主鍵索引樹)。
磁盤相關(guān)的確認(rèn)(大小、類型)
確認(rèn)磁盤空間和類型(機(jī)械硬盤、SSD)這事大家基本都會做。這里我先吐槽下某個(gè)云廠商 —— 有一次我們在壓測的時(shí)候發(fā)現(xiàn)寫入速率離SSD的寫入速率差一大截。后面發(fā)現(xiàn)binlog日志刷盤是瓶頸,仔細(xì)詢問才知道云廠商把binlog日志存在機(jī)械硬盤上。他們產(chǎn)品介紹的時(shí)候就說他們的MySQL用的是SSD磁盤,問他們binlog(binlog_format=ROW
;binlog_row_image=Full
)為什么這么慢,還想含糊其辭糊弄過去。這一點(diǎn)讓我覺得很無恥。如果選擇云廠商的MySQL產(chǎn)品,在使用之前要確認(rèn)好binlog日志所在磁盤的大小和類型。
除了binlog磁盤的問題,還要額外確認(rèn)的是MySQL使用的存儲方式:本地SSD、云SSD(阿里還有一個(gè)ESSD),對于數(shù)據(jù)量和壓力都比較大的項(xiàng)目組,這一點(diǎn)也是很重要的。
云服務(wù)對于冷備數(shù)據(jù)都會有免費(fèi)空間贈(zèng)送,要根據(jù)項(xiàng)目自己的實(shí)際情況規(guī)劃下如何使用。比如數(shù)據(jù)量比較大的項(xiàng)目是不是考慮只冷備一天的數(shù)據(jù),然后在過期前將冷備數(shù)據(jù)拉取出來放在本地服務(wù)器以避免購買云空間。
sys
庫的相關(guān)視圖:
這里主要想介紹下sys
庫中比較實(shí)用的監(jiān)控視圖。
- metrics:數(shù)據(jù)庫各種監(jiān)控和統(tǒng)計(jì)
- schema_index_statistics:各個(gè)表索引使用情況
- schema_table_statistics:各個(gè)表的使用情況
自增主鍵的使用
下面這個(gè)建表語句,rank
就是一個(gè)自增主鍵。
CREATE TABLE `TestTable` (
`rank` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`playerId` int(11) NOT NULL ,
`playInfoStr` varchar(255) DEFAULT NULL,
PRIMARY KEY (`rank`),
UNIQUE KEY `playerId` (`playerId`)
);
曾經(jīng)有一位同事,就想通過自增主鍵來實(shí)現(xiàn)玩家排名的功能。他的猜想是:誰先首次寫入TestTable
表,誰的rank就靠前(即使后面更新玩家數(shù)據(jù)rank也是不變),且rank是連續(xù)增長的(每次加1)。寫入和更新玩家的數(shù)據(jù)SQL是同一條,形如:replace into TestTable(name,playerId,playInfoStr) values('playerName',1,'78#12#13')
。想法很美好,但實(shí)際有很的大問題。對于同一個(gè)玩家,可能會有多次更新數(shù)據(jù)的可能(playInfoStr字段)。但每一次用上面SQL更新,rank就會變化,這樣和需求完全不一致。同時(shí)這里說明一點(diǎn):對于自增主鍵,并不是每一次自增都是加1。在一些事務(wù)失敗(比如死鎖造成的回滾)情況下,自增主鍵可能加2。所以rank是連續(xù)增長的(每次加1)
這一點(diǎn)自增主鍵也是實(shí)現(xiàn)不了的。
PS:自增主鍵每次的自增值(默認(rèn)是1)是可以設(shè)置的。
MySQL原生分表:
一張表數(shù)據(jù)量過大要怎么處理 —— 這是游戲服務(wù)端經(jīng)常要面臨的問題。當(dāng)某個(gè)游戲業(yè)務(wù),一個(gè)玩家在一張表有上千條數(shù)據(jù),我們就需要考慮分表(這里假設(shè)有100W玩家,這種表在游戲服務(wù)端很常見 - 比如任務(wù)、成就、物品等等)。MySQL只支持水平方向上的分表 —— 也就是不同的行可能分配在不同的物理分區(qū)中(不支持不同列在不同物理分區(qū)的垂直方向上的分表)。MySQL的分表實(shí)現(xiàn)方式是用戶通過分區(qū)函數(shù)確認(rèn)一行數(shù)據(jù)所在的分區(qū),然后對這行數(shù)據(jù)進(jìn)行操作(insert、update、select...)。分區(qū)函數(shù)是根據(jù)一行數(shù)據(jù)中一個(gè)或者多個(gè)字段來確認(rèn)分區(qū)的,下面介紹下最常用的幾個(gè)分表方式:
- 范圍分表:
CREATE TABLE `employees` (
`id` int(11) NOT NULL,
`name` varchar(30) COMMENT '雇員名字',
`hired` date NOT NULL DEFAULT '1970-01-01' COMMENT '入職日期',
`job_code` int(11) NOT NULL COMMENT '工號'
)
PARTITION BY RANGE (`job_code`) (
PARTITION p0 VALUES LESS THAN (10000),
PARTITION p1 VALUES LESS THAN (20000),
PARTITION p2 VALUES LESS THAN (30000)
);
上面是一個(gè)公司的雇員表(這里只是做舉例用,以這種數(shù)據(jù)量級完全沒有必要使用分區(qū)),這張表就是根據(jù)job_code
來進(jìn)行分區(qū)的。當(dāng)我們插入(1,'張三','2021-09-12',99)這條數(shù)據(jù)時(shí),就會被分到p0這個(gè)分區(qū)(這個(gè)對于客戶端來說是無感知的)。
我們可以在建表之后添加新的分區(qū)(30000<=job_code
<40000的數(shù)據(jù)在p3分區(qū)),語句如下:
ALTER TABLE `employees` ADD PARTITION (PARTITION p3 VALUES LESS THAN (40000));
通過范圍分區(qū)有一些缺點(diǎn):首先就是數(shù)據(jù)分布不均的問題。比如這張雇員表,對于大多數(shù)公司只會在p0分區(qū)才有數(shù)據(jù)(公司人數(shù)不超過10000人);其次如果分區(qū)字段超過定義最大區(qū)間會報(bào)錯(cuò),比如插入(1,'張三','2021-09-12',99999)這條數(shù)據(jù)時(shí)就會報(bào):1526 - Table has no partition for value 99999
的錯(cuò)誤,當(dāng)然這個(gè)問題可以通過添加一個(gè)無限大值分區(qū)來解決:ALTER TABLE
employeesADD PARTITION (PARTITION p4 VALUES LESS THAN MAXVALUE);
但是這又回到了第一個(gè)問題上面,有可能導(dǎo)致大部分?jǐn)?shù)據(jù)集中在p4分區(qū)上了。
范圍分區(qū)還可以挑date類型進(jìn)行,比如下面這樣(具體到天也是可以的):
CREATE TABLE `employees` (
`id` int(11) NOT NULL,
`name` varchar(30) COMMENT '雇員名字',
`hired` date NOT NULL DEFAULT '1970-01-01' COMMENT '入職日期',
`job_code` int(11) NOT NULL COMMENT '工號'
)
PARTITION BY RANGE( YEAR(hired) ) (
PARTITION d0 VALUES LESS THAN (2000),
PARTITION d1 VALUES LESS THAN (2010),
PARTITION d2 VALUES LESS THAN (2020),
PARTITION d7 VALUES LESS THAN MAXVALUE
);
- 列表分表:
CREATE TABLE `employees` (
`id` int(11) NOT NULL,
`name` varchar(30) COMMENT '雇員名字',
`hired` date NOT NULL DEFAULT '1970-01-01' COMMENT '入職日期',
`job_code` int(11) NOT NULL COMMENT '工號',
`sex` int(11) NOT NULL COMMENT '性別'
)
PARTITION BY LIST(`sex`) (
PARTITION pMan VALUES IN (1),
PARTITION pWoman VALUES IN (2),
PARTITION pUnkonw VALUES IN (3),
);
上面就是按照性別分區(qū)。sex
=1就在pMan區(qū);sex
=2就在pWoman區(qū);sex
=3就在pUnkonw區(qū)。一般這種適用于字段是有限數(shù)值的,比如按照省份分區(qū)(東南西北各個(gè)地理位置的省份)。同樣如果插入(1,'張三','2021-09-12',99,4)也是會報(bào)找不到分區(qū)的報(bào)錯(cuò)(sex
=4不在任何一個(gè)定義的分區(qū)上)。
CREATE TABLE `employees` (
`id` int(11) NOT NULL,
`name` varchar(30) COMMENT '雇員名字',
`hired` date NOT NULL DEFAULT '1970-01-01' COMMENT '入職日期',
`job_code` int(11) NOT NULL COMMENT '工號'
)
PARTITION BY HASH(`job_code`) PARTITIONS 4;
上面就是根據(jù)job_code
的值決定一行數(shù)據(jù)所在的分區(qū)(總分區(qū)數(shù)有4個(gè)),HASH分表很好解決了范圍分表數(shù)據(jù)分布不均的問題。
學(xué)習(xí)資料推薦:
MySQL官方文檔:https://dev.mysql.com/doc/refman/8.0/en/
阿里mysql月報(bào):http://mysql.taobao.org/monthly/
一位大牛的MySQL資料:https://github.com/hedengcheng/tech/tree/master/database/MySQL
阿里云MySQL文檔:https://help.aliyun.com/document_detail/95798.html
騰訊云MySQL文檔:https://cloud.tencent.com/document/product/236
極客時(shí)間的《MySQL 45講》(這個(gè)是付費(fèi)的):https://time.geekbang.org/column/article/67888