后端学习 - MySQL存储引擎、索引与事务
文章目錄
- 一 存儲(chǔ)引擎
- 1 MyISAM 與 InnoDB 的差異
- 2 表級(jí)鎖與行級(jí)鎖
- 二 索引
- 1 主鍵索引與二級(jí)索引
- 2 聚簇索引與非聚簇索引
- 3 數(shù)據(jù)結(jié)構(gòu):哈希表
- 4 數(shù)據(jù)結(jié)構(gòu):B樹
- 5 數(shù)據(jù)結(jié)構(gòu):B+樹
- 6 數(shù)據(jù)結(jié)構(gòu):跳表
- 7 為什么不使用紅黑樹**
- 8 為什么不使用B樹**
- 三 MySQL 事務(wù)
- 1 ACID 及其保證手段
- 2 事務(wù)的隔離級(jí)別
- 3 多版本并發(fā)控制 MVCC
- 4 MVCC 實(shí)現(xiàn)流程
- 5 RR 等級(jí)下使用 MVCC 防止幻讀
- 四 規(guī)范與優(yōu)化建議
- 1 索引、數(shù)據(jù)表設(shè)計(jì)
- 2 索引失效的情況(優(yōu)化查詢需要避免)
- 3 連接查詢優(yōu)化
- 4 分庫(kù)和分表
- 五 InnoDB 的鎖機(jī)制
- 1 表鎖:S、X、IS、IX
- 2 行鎖:S、X
- 3 行鎖的進(jìn)一步劃分
- 六 SQL 查詢
- 1 語(yǔ)法
- 2 執(zhí)行順序
- 3 常用函數(shù) / 關(guān)鍵字
一 存儲(chǔ)引擎
1 MyISAM 與 InnoDB 的差異
- MySQL 5.5 之前,MyISAM 引擎是 MySQL 的默認(rèn)存儲(chǔ)引擎,之后的默認(rèn)引擎是 InnoDB
- 相比之下,InnoDB 具有 支持事務(wù)、支持行級(jí)鎖、支持外鍵、支持?jǐn)?shù)據(jù)庫(kù)異常崩潰后的安全恢復(fù)(redo log) 的特性
2 表級(jí)鎖與行級(jí)鎖
- 表級(jí)鎖: MySQL 中鎖定 粒度最大 的一種鎖,對(duì)當(dāng)前操作的整張表加鎖,實(shí)現(xiàn)簡(jiǎn)單,資源消耗也比較少,加鎖快,不會(huì)出現(xiàn)死鎖。其鎖定粒度最大,觸發(fā)鎖沖突的概率最高,并發(fā)度最低,MyISAM 和 InnoDB 引擎都支持表級(jí)鎖
- 行級(jí)鎖: MySQL 中鎖定 粒度最小 的一種鎖,只針對(duì)當(dāng)前操作的行進(jìn)行加鎖。 行級(jí)鎖能大大減少數(shù)據(jù)庫(kù)操作的沖突。其加鎖粒度最小,并發(fā)度高,但加鎖的開銷也最大,加鎖慢,會(huì)出現(xiàn)死鎖
二 索引
使用索引不一定能提升查詢性能,在表比較小時(shí),全表掃描的速度可能比使用索引更快
1 主鍵索引與二級(jí)索引
- 主鍵索引存放的數(shù)據(jù)是表中的一條數(shù)據(jù),二級(jí)索引存放的數(shù)據(jù)是主鍵索引
- 一張數(shù)據(jù)表有只能有一個(gè)主鍵,并且主鍵不能為 null,不能重復(fù)
- 一般而言,不發(fā)生索引覆蓋時(shí),在根據(jù)主索引搜索時(shí),直接找到 key 所在的節(jié)點(diǎn)即可取出數(shù)據(jù);在根據(jù)輔助索引查找時(shí),則需要先取出主鍵的值,再使用主索引進(jìn)行數(shù)據(jù)查找
- 除非發(fā)生索引覆蓋(需要查詢的字段正好是索引的字段,無論主索引或二級(jí)索引)的情況,否則使用二級(jí)索引會(huì)導(dǎo)致回表(當(dāng)查到索引對(duì)應(yīng)的主鍵后,還需要根據(jù)主鍵再到數(shù)據(jù)文件或表中查詢)
- 如果沒有主鍵也沒有合適的唯一索引,那么 InnoDB 內(nèi)部會(huì)生成一個(gè)隱藏的主鍵作為聚集索引,這個(gè)隱藏的主鍵是一個(gè)6個(gè)字節(jié),類型為長(zhǎng)整型的列,該列的值會(huì)隨著數(shù)據(jù)的插入自增
- 二級(jí)索引的內(nèi)節(jié)點(diǎn),也保存了主鍵的值,避免相同的二級(jí)索引值造成歧義
- 聯(lián)合索引本質(zhì)上也是二級(jí)索引,多個(gè)屬性建立聯(lián)合索引只會(huì)建立一棵B+樹
2 聚簇索引與非聚簇索引
- 聚簇索引指的是,索引和數(shù)據(jù)存放在一起。對(duì)于 InnoDB 引擎表來說,該表的索引(B+樹)的每個(gè)非葉子節(jié)點(diǎn)存儲(chǔ)索引,葉子節(jié)點(diǎn)存儲(chǔ)索引和索引對(duì)應(yīng)的數(shù)據(jù)
- 主鍵索引是聚簇索引,二級(jí)索引是非聚簇索引
| 聚簇索引 | 查詢速度快,無需回表 | 需要依賴有序的數(shù)據(jù)(因?yàn)閿?shù)據(jù)結(jié)構(gòu)是B+樹,需要在插入時(shí)排序);更新代價(jià)大(如果對(duì)索引列的數(shù)據(jù)被修改時(shí),那么對(duì)應(yīng)的索引也將會(huì)被修改) |
| 非聚簇索引 | 更新代價(jià)比聚簇索引小 | 需要依賴有序的數(shù)據(jù);可能會(huì)回表 |
3 數(shù)據(jù)結(jié)構(gòu):哈希表
- 插入數(shù)據(jù)時(shí),根據(jù) key 進(jìn)行哈希運(yùn)算,得到 bucket 的位置,如果該位置無 value 則插入成功,如果有則發(fā)生了哈希沖突,使用拉鏈法或者紅黑樹解決(類似 Java 的 HashMap)
- 只支持全值的精確查詢,時(shí)間復(fù)雜度僅為O(1),但不支持順序或者范圍查詢
4 數(shù)據(jù)結(jié)構(gòu):B樹
- MongoDB 采用的索引類型,多路平衡查找樹
- B樹的中間節(jié)點(diǎn)存放的也是數(shù)據(jù);葉節(jié)點(diǎn)之間沒有連接
- B樹的檢索的過程相當(dāng)于對(duì)范圍內(nèi)的每個(gè)節(jié)點(diǎn)的關(guān)鍵字做二分查找,可能還沒有到達(dá)葉子節(jié)點(diǎn),檢索就結(jié)束了
5 數(shù)據(jù)結(jié)構(gòu):B+樹
- MyISAM 和 InnoDB 均采用的索引類型,多路平衡查找樹
- B+樹的中間節(jié)點(diǎn)只存放索引,數(shù)據(jù)全部都在葉節(jié)點(diǎn)上;葉節(jié)點(diǎn)之間有連接
- 相對(duì)于B樹而言,對(duì)范圍查找的支持更好
- 查詢效率更加穩(wěn)定,因?yàn)槊看尾樵儽囟〞?huì)訪問到葉節(jié)點(diǎn)
- 葉子節(jié)點(diǎn)構(gòu)成一個(gè)有序鏈表,而且葉子節(jié)點(diǎn)本身按照關(guān)鍵字的大小從小到大順序鏈接
- B+樹的階數(shù)并非越大越好:階數(shù)很大時(shí),一個(gè)節(jié)點(diǎn)的大小會(huì)超過一個(gè)頁(yè)的大小,讀取這樣一個(gè)節(jié)點(diǎn)會(huì)導(dǎo)致多次I/O操作,造成性能下降。要盡量讓每個(gè)節(jié)點(diǎn)的大小等于一個(gè)頁(yè)的大小
6 數(shù)據(jù)結(jié)構(gòu):跳表
- 在 Redis 中的 Zset 使用到的數(shù)據(jù)結(jié)構(gòu)
- 為什么在 MySQL 中選擇 B+ 樹而非跳表作為索引的數(shù)據(jù)結(jié)構(gòu)?
如果是查詢磁盤文件,B+ 樹會(huì)比跳表的性能好很多,因?yàn)榇疟P查詢性能比內(nèi)存差,所以要盡量減少查詢的次數(shù)
B+ 樹每個(gè)節(jié)點(diǎn)按頁(yè)存放數(shù)據(jù),每次查詢可以查詢一批數(shù)據(jù)到內(nèi)存中;而且 B+ 樹的層數(shù)低,可以減少訪問磁盤的次數(shù)
7 為什么不使用紅黑樹**
- MySQL 用 I/O 次數(shù)衡量查詢效率,磁盤查找存取的次數(shù)往往由樹的高度所決定
8 為什么不使用B樹**
三 MySQL 事務(wù)
1 ACID 及其保證手段
- 原子性:事務(wù)是最小的執(zhí)行單位,不允許分割(事務(wù)內(nèi)的一系列操作,要么全都做,要么全不做)
- 一致性:事務(wù)執(zhí)行前后,數(shù)據(jù)庫(kù)的一致性保持不變
- 隔離性:并發(fā)進(jìn)行的事務(wù)之間互不影響
- 持久性:事務(wù)提交后,對(duì)數(shù)據(jù)庫(kù)中數(shù)據(jù)的修改是永久的
MySQL InnoDB 引擎使用 redo log 保證事務(wù)的持久性,使用 undo log 來保證事務(wù)的原子性;
鎖機(jī)制、MVCC 等手段來保證事務(wù)的隔離性( 默認(rèn)隔離級(jí)別是可重復(fù)讀);
保證了事務(wù)的持久性、原子性、隔離性之后,一致性才能得到保障
2 事務(wù)的隔離級(jí)別
| 讀未提交 | √ | √ | √ |
| 讀提交 | × | √ | √ |
| 可重復(fù)讀 | × | × | √ |
| 串行化 | × | × | × |
- 可重復(fù)讀 使用 MVCC + Next-key Lock 也可以避免幻讀
3 多版本并發(fā)控制 MVCC
- 簡(jiǎn)言之,MVCC 處理的是 讀數(shù)據(jù) 的問題,寫數(shù)據(jù)必須依靠加鎖
- 普通的 SELECT 語(yǔ)句在 讀提交 和 可重復(fù)讀 隔離級(jí)別下會(huì)使用到 MVCC,主要針對(duì)的也是這兩種隔離級(jí)別(因?yàn)檫@兩種隔離級(jí)別要求讀到的是 已經(jīng)提交了的 事務(wù)修改過的記錄),另外兩種隔離級(jí)別不適用 MVCC
- MVCC 的讀指的是 快照讀(讀取的是快照數(shù)據(jù)) , 而非 當(dāng)前讀(當(dāng)前讀讀取的是記錄的最新版本,讀取時(shí)還要保證其他并發(fā)事務(wù)不能修改當(dāng)前記錄,會(huì)對(duì)讀取的記錄進(jìn)行加鎖)
- 當(dāng)前讀實(shí)際上是一種加鎖的操作,是悲觀鎖的實(shí)現(xiàn),而MVCC本質(zhì)是采用樂觀鎖思想的一種方式
4 MVCC 實(shí)現(xiàn)流程
- 其實(shí)現(xiàn)主要依賴于 記錄的隱藏字段 + UndoLog + ReadView
- 讀提交 和 可重復(fù)讀 關(guān)于 ReadView 的區(qū)別是,在每一次進(jìn)行普通 SELECT 操作前都會(huì)生成一個(gè)ReadView ;可重復(fù)讀只在第一次進(jìn)行普通 SELECT 操作前生成一個(gè) ReadView,之后的查詢操作都重復(fù)使用相同的 ReadView
- ReadView 包含主要結(jié)構(gòu)
| trx_ids | 生成 ReadView 時(shí),活躍的讀寫事務(wù)的事務(wù) ID 列表 |
| up_limit_id | 活躍的事務(wù)中最小的事務(wù) ID |
| low_limit_id | 生成 ReadView 時(shí),系統(tǒng)中應(yīng)該分配給下一個(gè)事務(wù)的 ID 值 |
注意:low_limit_id 并不是 trx_ids 中的最大值,事務(wù)id是遞增分配的。比如,現(xiàn)在有id為1,2,3這三個(gè)事務(wù),之后id為3的事務(wù)提交了。那么一個(gè)新的讀事務(wù)在生成 ReadView 時(shí),trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是4
-
ReadView 規(guī)則:
- 如果被訪問版本的 trx_id 屬性值與 ReadView 中的 creator_trx_id 值相同,說明該記錄的修改是由當(dāng)前事務(wù)創(chuàng)建的,允許訪問
- 如果被訪問版本的 trx_id 屬性值小于 ReadView 中的 up_limit_id 值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成 ReadView 之前已經(jīng)提交,允許訪問
- 如果被訪問版本的 trx_id 屬性值大于或等于 ReadView 中的 low_limit_id 值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成 ReadView 后才開啟,不允許訪問
- 如果被訪問版本的 trx_id 屬性值在 ReadView 的 up_limit_id 和 low_limit_id 之間,那就需要判斷一下 trx_id 屬性值是不是在 trx_ids 列表中:如果在,說明生成該版本的事務(wù),在當(dāng)前事務(wù)開啟時(shí)尚未提交,不允許訪問;反之說明生成該版本的事務(wù),在當(dāng)前事務(wù)開啟時(shí)已經(jīng)提交,允許訪問
-
MVCC 整體流程:
- 首先獲取事務(wù)自己的版本號(hào),也就是事務(wù) ID;
- 獲取 ReadView;
- 查詢得到的數(shù)據(jù),然后與 ReadView 中的事務(wù)版本號(hào)進(jìn)行比較;
- 如果不符合 ReadView 規(guī)則,就需要從 Undo Log 中獲取歷史快照;
- 最后返回符合規(guī)則的數(shù)據(jù),如果無符合規(guī)則的數(shù)據(jù)則返回空
5 RR 等級(jí)下使用 MVCC 防止幻讀
- 執(zhí)行普通 SELECT 即快照讀時(shí),可重復(fù)讀只會(huì)在事務(wù)開啟后的第一次查詢生成 Read View ,并使用至事務(wù)提交。所以在生成 ReadView 之后其它事務(wù)所做的更新、插入記錄版本對(duì)當(dāng)前事務(wù)并不可見,實(shí)現(xiàn)了可重復(fù)讀和防止快照讀下的 幻讀
- 執(zhí)行當(dāng)前讀時(shí),InnoDB 使用 Next-key Lock 來防止這種情況。當(dāng)執(zhí)行當(dāng)前讀時(shí),會(huì)鎖定讀取到的記錄的同時(shí),鎖定它們的間隙,防止其它事務(wù)在查詢范圍內(nèi)插入數(shù)據(jù)
四 規(guī)范與優(yōu)化建議
1 索引、數(shù)據(jù)表設(shè)計(jì)
讓主鍵具有 AUTO_INCREMENT ,讓存儲(chǔ)引擎自己生成主鍵,而不是手動(dòng)插入,讓插入的記錄的主鍵值依次遞增,避免頁(yè)面分裂帶來的性能損耗
字段的數(shù)據(jù)類型定義準(zhǔn)確
設(shè)計(jì)數(shù)據(jù)表時(shí)遵循范式規(guī)則 1/2/3NF
1NF:數(shù)據(jù)表的每個(gè)字段不可拆分
2NF:數(shù)據(jù)表中非主屬性完全依賴于主屬性
3NF:數(shù)據(jù)表中的非主屬性不傳遞依賴于主屬性
對(duì)數(shù)據(jù)表進(jìn)行適當(dāng)?shù)男小⒘胁鸱?#xff08;分表)
數(shù)據(jù)庫(kù)和表的字符集統(tǒng)一使用 utf8mb4
表屬性盡量設(shè)置為 NOT NULL,因?yàn)?IS NOT NULL 不能使用索引
將聯(lián)合索引中,把過濾性最好(區(qū)分度最大)的屬性放在前面;把需要范圍查詢的屬性放在后面
2 索引失效的情況(優(yōu)化查詢需要避免)
對(duì)于列 (age, class, name) 建立了聯(lián)合索引,如果查詢條件是 class=5 and name='zy' 等違背左前綴,則無法使用該索引
優(yōu)化器可以決定 where 后面條件的順序,所以 class=5 and name='zy' and age=10 可以使用索引
WHERE name=123 不能使用索引;WHERE name='123' 可以使用索引
對(duì)于列 (age, class, name) 建立了聯(lián)合索引,WHERE student.age=30 AND student.name = 'abc' AND student.classId>20 會(huì)導(dǎo)致 name 索引失效
3 連接查詢優(yōu)化
- 驅(qū)動(dòng)表:無法避免全表掃描的表稱為驅(qū)動(dòng)表
- 在決定哪個(gè)表做驅(qū)動(dòng)表的時(shí)候,兩個(gè)表按照各自的條件過濾,過濾后計(jì)算參與 join 的各個(gè)字段的總數(shù)據(jù)量,數(shù)據(jù)量小的那個(gè)表,就是“小表”,應(yīng)該作為驅(qū)動(dòng)表
- 為了提高連接查詢效率,需要將小表作為驅(qū)動(dòng)表,大表作為被驅(qū)動(dòng)表,減少外層循環(huán)的次數(shù)
- LEFT JOIN 保證左表的每個(gè)記錄都會(huì)出現(xiàn),即左表為驅(qū)動(dòng)表,所以右表是關(guān)鍵,一定需要建立索引(右外連接反之)
- INNER JOIN 會(huì)自動(dòng)決定驅(qū)動(dòng)表、被驅(qū)動(dòng)表
對(duì)于連接屬性,如果兩表只有一個(gè)具有索引,則它作為被驅(qū)動(dòng)表
如果兩個(gè)表都在該屬性有索引,小表作為驅(qū)動(dòng)表,大表作為被驅(qū)動(dòng)表 - 需要JOIN 的字段,數(shù)據(jù)類型保持絕對(duì)一致,避免類型轉(zhuǎn)換導(dǎo)致索引失效
4 分庫(kù)和分表
| 分庫(kù) | 數(shù)據(jù)庫(kù)中的數(shù)據(jù)分散到不同的數(shù)據(jù)庫(kù)上 | 應(yīng)用的并發(fā)量太大;數(shù)據(jù)庫(kù)中的數(shù)據(jù)占用的空間越來越大,備份時(shí)間越來越長(zhǎng) |
| 分表 | 對(duì)單表的數(shù)據(jù)進(jìn)行拆分,可以是垂直拆分,也可以是水平拆分 | 單表的數(shù)據(jù)達(dá)到千萬(wàn)級(jí)別以上,數(shù)據(jù)庫(kù)讀寫速度比較緩慢 |
分庫(kù)帶來的問題:
- join 操作 : 同一個(gè)數(shù)據(jù)庫(kù)中的表分布在了不同的數(shù)據(jù)庫(kù)中,導(dǎo)致無法使用 join 操作,需要手動(dòng)進(jìn)行數(shù)據(jù)的封裝,比如在一個(gè)數(shù)據(jù)庫(kù)中查詢到一個(gè)數(shù)據(jù)之后,再根據(jù)這個(gè)數(shù)據(jù)去另外一個(gè)數(shù)據(jù)庫(kù)中找對(duì)應(yīng)的數(shù)據(jù)
- 事務(wù)問題 :同一個(gè)數(shù)據(jù)庫(kù)中的表分布在了不同的數(shù)據(jù)庫(kù)中,如果單個(gè)操作涉及到多個(gè)數(shù)據(jù)庫(kù),那么數(shù)據(jù)庫(kù)自帶的事務(wù)就無法滿足要求
- 分布式 id :分庫(kù)之后, 數(shù)據(jù)遍布在不同服務(wù)器上的數(shù)據(jù)庫(kù),數(shù)據(jù)庫(kù)的自增主鍵已經(jīng)沒辦法滿足生成的主鍵唯一了。需要為系統(tǒng)引入分布式 id
五 InnoDB 的鎖機(jī)制
該部分的參考
1 表鎖:S、X、IS、IX
- 意向鎖:
當(dāng)事務(wù)要在記錄上加上行鎖時(shí),要首先在表上加上意向鎖,使得判斷表中是否有記錄正在加鎖更簡(jiǎn)單
意向鎖不與行級(jí)鎖發(fā)生沖突,因?yàn)樗鼘儆诒砑?jí)鎖
意向鎖之間不發(fā)生沖突 - 表級(jí)鎖的兼容矩陣:
2 行鎖:S、X
行鎖是加在索引上的,如果查詢語(yǔ)句不使用索引的話,那么它就會(huì)升級(jí)到表鎖,最終造成效率低下
- 共享鎖(S):加了鎖的記錄,所有事務(wù)都能去讀取但不能修改,同時(shí)阻止其他事務(wù)獲得相同數(shù)據(jù)集的排他鎖
- 排他鎖(X):允許已經(jīng)獲得排他鎖的事務(wù)去更新數(shù)據(jù),阻止其他事務(wù)取得相同數(shù)據(jù)集的共享讀鎖和排他寫鎖
3 行鎖的進(jìn)一步劃分
根據(jù)行鎖的位置不同進(jìn)行劃分
- 本質(zhì)上是 Gap Lock + Record Lock,左開右閉
- MySQL 默認(rèn)隔離級(jí)別是RR,在這種級(jí)別下,如果使用 select in share mode 或者 select for update 語(yǔ)句(即當(dāng)前讀),那么 InnoDB 會(huì)使用 Next-Key Lock,防止幻讀,在上面已經(jīng)討論過
- 使用范圍條件而不是相等條件去檢索,并請(qǐng)求鎖時(shí),InnoDB就會(huì)給符合條件的記錄的索引項(xiàng)加上鎖;而對(duì)于鍵值在條件范圍內(nèi)但并不存在的記錄,就叫做間隙,InnoDB此時(shí)也會(huì)對(duì)間隙加鎖
- 在 RU 和 RC 兩種隔離級(jí)別下,即使你使用 select in share mode 或 select for update,也無法防止幻讀。因?yàn)檫@兩種隔離級(jí)別下只會(huì)有行鎖,而不會(huì)有間隙鎖;而如果是 RR 隔離級(jí)別的話,就會(huì)在間隙上加上間隙鎖
- 最簡(jiǎn)單的行鎖
- 插入意圖鎖是一種間隙鎖,在行執(zhí)行 INSERT 之前的插入操作設(shè)置。如果多個(gè)事務(wù) INSERT 到同一個(gè)索引間隙之間,但沒有在同一位置上插入,則不會(huì)產(chǎn)生任何的沖突
- 假設(shè)有值為4和7的索引記錄,現(xiàn)在有兩事務(wù)分別嘗試插入值為 5 和 6 的記錄,在獲得插入行的排他鎖之前,都使用插入意向鎖鎖住 4 和 7 之間的間隙,但兩者之間并不會(huì)相互阻塞,因?yàn)檫@兩行并不沖突
六 SQL 查詢
1 語(yǔ)法
- select 的屬性(不包括函數(shù)使用的)必須在 group by 后面出現(xiàn)
2 執(zhí)行順序
1. from # 首先進(jìn)入from字句 2. on # 根據(jù)條件選擇需要連接的行 3. join # 執(zhí)行連接 4. where # 對(duì)連接結(jié)果過濾 5. group by # 分組(在分組之前執(zhí)行了 select 后面的 if、substring_index... 等非運(yùn)算操作) 6. avg(), sum(), count()... behind select # 執(zhí)行函數(shù) 7. having # 對(duì)各個(gè)分組分別進(jìn)行過濾 8. select # 選擇結(jié)果 9. distinct # 結(jié)果去重 10. union # 將當(dāng)前結(jié)果并上其它結(jié)果 11. order by [asc, desc] # 排序 12. limit # 選擇指定行作為結(jié)果3 常用函數(shù) / 關(guān)鍵字
union [all]:不加 all 會(huì)自動(dòng)去重
substring(field, separator, index):如果 index > 0 則取前綴,否則取后綴
upper(str):轉(zhuǎn)為大寫
concat(str1, str2):拼接字符串
substring(str, start, end):子串,索引從1開始
總結(jié)
以上是生活随笔為你收集整理的后端学习 - MySQL存储引擎、索引与事务的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 辐射对孕妈和宝宝有哪些伤害辐射对宝宝有哪
- 下一篇: ios realm 文件_iOS开发中使