背景
不知道有沒有讀者經歷過刪除線上數據庫記錄,反正這事對我來說是挺有壓力的。動地心,顫抖的手,萬一刪錯了數據就麻煩了,影響數量大,領導會批評,還有覆盤自己批評,想想就頭大。那麼就要在刪除數據之前做好計劃,避免刪數據時發生意料之外的事情。記錄一次刪除線上數據。
業務以半年爲一個週期,每一個週期體驗參與用戶在40w左右。新一期開始後過期體驗用戶的數據較大,會讓數據庫增刪改查速度變慢,但這一部分數據對業務已經沒有幫助了,所以要清理過期體驗用戶的數據。
一共要清理5張表:
A:數量 36w
B:數量 36w
C:數量 140w
D:數量 36w
E:數量 72w
要求
對於大量數據的查詢和刪除不能使用一條語句完成,會造成數據庫阻塞,特別是刪除數據時會鎖表或鎖行。要減少對線上正常業務的影響,在刪除數據的同時不能影響數據庫正常增刪改查,而且不能讓數據庫查詢等待過長而導致API返回超時。所以查詢和刪除都需要分批完成。
刪除數據底層流程
簡單的說明一下在刪除數據時MySQL數據底層流程。
MySQL數據庫分爲兩個部分:server層和存儲引擎,如下圖:
在刪除一條記錄時,流程分別是:
- 客戶端連接到server層
- 分析器對sql語句進行詞法分析和語法分析,知道是一條刪除語句
- 優化器選擇合適的索引去查找待刪除的記錄
- 執行器調用存儲引擎刪除所有記錄
- 存儲引擎將維護的數據內存裏的記錄標記爲已刪除
- 存儲引擎根據刷盤規則將內存的記錄寫到磁盤中
以上就是刪除一條語句的過程,因爲每一個sql語句是一個事務,每一個事務都涉及到加鎖,所以在以上的刪除流程中,鎖的使用是這樣的:
- 如果查詢待刪除記錄走的是索引,那麼會將所有刪除記錄行加鎖
- 如果查詢待刪除記錄沒有走索引,那麼將會將整張表都鎖起來,直到刪除完成
刪除實施
針對最多140w的數據,肯定不能一次性刪除,否則鎖表的時間會讓其他業務無法進行,同時要在刪除時使用行鎖而不是表鎖。所以刪除分爲兩步走:
- 使用分塊查詢,將所有記錄的主鍵查詢出來
- 根據主鍵分批刪除,同時每一次刪除間隔一定時間,異常捕獲不影響刪除流程
經過5張表的刪除,得出以下腳本刪除時對數據庫影響較小
# 分批查詢,只查詢id
def chunk_model(model, conditions, start_id=0, chunk_size=500):
while True:
models = list(
model.select(model.id)
.where(model.id > start_id, *conditions)
.order_by(model.id.asc())
.limit(chunk_size)
)
if not models:
break
yield models
start_id = models[-1].id
import time
from app.camp.models import UserPlan
plan_id_list = [33,34]
delete_id_list = []
for chunk_data in chunk_model(UserPlan, [UserPlan.plan_id.in_(plan_id_list)]):
print(chunk_data[0].id)
for data in chunk_data:
delete_id_list.append(data.id)
# 分批刪除
for id_index in range(0, len(delete_id_list), 1000):
print(id_index)
try:
delete_ids = delete_id_list[id_index:id_index+1000]
UserPlan.delete().where(UserPlan.id.in_(delete_ids)).execute()
time.sleep(0.5)
except Exception as e:
print(id_index, e)
最多的140w數據耗時在10分鐘左右,全程數據庫無波動,業務沒有影響。