據(jù)結構與通用命令)
一、面試復習通信、鎖與代理這部分是前幾天面試題的展開講解也是寫高并發(fā)程序的基礎。1.1 什么是 IPC如何進行進程間通信IPC是 Inter-Process Communication進程間通信的縮寫。之所以需要進程間通信是因為進程之間數(shù)據(jù)是隔離的。兩種情況同一臺機器上的兩個進程通信如同一個程序開啟的多個進程Python多進程。不同機器上的兩個進程通信跨主機通信。如何通信方式說明共享內(nèi)存 / 共享變量線程間變量天然共享進程間需借助共享內(nèi)存隊列 / 管道如 Pythonmultiprocessing.Queue、Pipe消息隊列Redis、RabbitMQ、Kafka 等Socket 套接字服務與服務的接口調(diào)用HTTP、RPC、MySQL socket 自定協(xié)議文件方式通過文件交換數(shù)據(jù)1.2 線程間通信與鎖重點補充為什么需要鎖多線程共享同一個變量時多個線程同時讀寫就會產(chǎn)生數(shù)據(jù)競爭導致數(shù)據(jù)錯亂因此需要加鎖保證數(shù)據(jù)安全。互斥鎖LockfromthreadingimportThread,Lockdefwork():globaln lock.acquire()# 加鎖保證同一時刻只有一個線程修改共享數(shù)據(jù)tempn time.sleep(0.001)ntemp-1lock.release()join與鎖的區(qū)別在start()后立即join()會讓 100 個任務整體串行執(zhí)行雖然數(shù)據(jù)安全但效率很低。加鎖只是把“修改共享數(shù)據(jù)”的那部分代碼串行化不加鎖的部分仍能并發(fā)效率更高。死鎖兩個或兩個以上的進程/線程執(zhí)行時因爭奪資源而互相等待若無外力干預都無法繼續(xù)推進。典型如“科學家吃面”問題每個科學家需要同時拿兩把叉子才能吃面若各拿一把又互相等待另一把就會死鎖。遞歸鎖RLock/ 可重入鎖允許同一個線程多次acquire內(nèi)部維護一個計數(shù)器記錄獲取次數(shù)同一次數(shù)釋放后其他線程才能獲取。fromthreadingimportRLock lockRLock()lock.acquire()lock.acquire()# 同一線程可以再次獲取不會死鎖print(123)lock.release()lock.release()信號量Semaphore可以理解為“多把鎖”同時允許多個線程執(zhí)行某段代碼。例如Semaphore(5)表示最多 5 個線程同時進入。事件Event某些線程需要等其他線程執(zhí)行到某個節(jié)點后再繼續(xù)。例如女神者等待對方event.set()后多個備胎線程event.wait()再啟動。GIL 與普通互斥鎖的區(qū)別GIL 是解釋器級別的鎖保證同一時刻只有一個線程執(zhí)行 Python 字節(jié)碼。普通互斥鎖是應用層面的鎖用來保護共享數(shù)據(jù)。GIL 無法替代數(shù)據(jù)保護所以仍需互斥鎖。I/O 密集型與計算密集型的選型單核 CPU無論 I/O 密集還是計算密集都用線程或協(xié)程。多核 CPU計算密集型用多進程I/O 密集型用多線程I/O 等待不占用 CPU。1.3 正向代理與反向代理代理本質是一個中介原本 A 和 B 可以直連中間插入一個 CC 就是代理。正向代理 —— 代理的是客戶端訪問原本無法訪問的資源如 Google。做緩存加速訪問。對客戶端訪問授權、上網(wǎng)認證。記錄用戶訪問記錄上網(wǎng)行為管理對外隱藏用戶身份。典型如 VPN、爬蟲代理池。反向代理 —— 代理的是服務端保護內(nèi)網(wǎng)安全阻止 Web 攻擊。大型網(wǎng)站通常把反向代理作為公網(wǎng)訪問入口Web 服務器放在內(nèi)網(wǎng)。負載均衡通過反向代理服務器優(yōu)化網(wǎng)站負載。典型如 nginx、apache。項目上線升級常結合反向代理做灰度發(fā)布與平滑升級。1.4 什么是粘包粘包是TCP 流式協(xié)議的現(xiàn)象TCP 沒有消息邊界客戶端發(fā)送的多個數(shù)據(jù)包會像水流一樣流向服務端多個包“粘”在一起無法區(qū)分是幾個數(shù)據(jù)包。解決思路每個包設置結束標志如 HTTP 用\r\n\r\n。每個包設置固定大小的頭頭里包含包的大小再按長度讀取。二、補充表單表設計結合業(yè)務動態(tài)表單可以用“兩張表 一個提交內(nèi)容字段”來設計表單表定義一個表單id創(chuàng)建人創(chuàng)建時間1用戶 id時間表單詳情表定義表單字段id表單 id類型key是否必填值11單行文本申請人true21單選框用途true[測試, 生產(chǎn)]31單選框是否第一次true[是, 否]41多選框云廠商true[騰訊云, 阿里云, xx云]填寫表單用戶提交記錄id表單 id用戶 id內(nèi)容111{1:lqz, 2:測試, 3:是, 4:[騰訊云, 阿里云]}224{1:是, 2:測試, 3:是, 4:[騰訊云, 阿里云]}內(nèi)容字段直接用 JSON 存儲靈活但不易匯總統(tǒng)計適合字段不確定的動態(tài)表單場景。三、通用命令info# 查看內(nèi)存、CPU、主從信息可用于寫 Redis 監(jiān)控client list# 查看正在連接的會話clientkillip:port# 斷開某個客戶端dbsize# 統(tǒng)計總共有多少個 keyflushall# 清空所有數(shù)據(jù)庫危險慎用flushdb# 只清空當前數(shù)據(jù)庫select數(shù)字# 選擇某個庫默認共 16 個庫0~15monitor# 記錄/監(jiān)控操作日志會一直輸出注意會夯住終端生產(chǎn)環(huán)境請謹慎使用flushall、flushdb、monitor。其中monitor會持續(xù)打印所有命令影響性能且占用終端。管理 Redis 實例的 Web 項目可選參考https://gitee.com/bijingrui/repollhttps://gitee.com/dromara/mayfly-gohttps://gitee.com/careyjike_173/redis_web_clienthttps://gitee.com/Alcex/QMySQLAdminhttps://gitee.com/CloudWise/OMPhttps://gitee.com/lustlost/ubackup四、字符串String類型String 是 Redis 最基本也最常用的數(shù)據(jù)類型value 是二進制安全的可以存圖片、序列化對象等。4.1 基本使用 get / set / delsetname lqz# 設置 key 的 valueO(1)get name# 獲取 key 的 valueO(1)del name# 刪除 keyO(1)4.2 自增自減 incr / decr / incrby / decrbyincr age# value 自增 1decr age# value 自減 1incrby age10# value 增加 10decrby age10# value 減 10應用場景統(tǒng)計網(wǎng)站訪問量Redis 單線程執(zhí)行命令天然無競爭適合做計數(shù)器。分布式 ID 生成多臺機器并發(fā)生成也不會重復。說明INCR/DECR要求 value 是整數(shù)字符串否則會報錯。這類命令用于計數(shù)非常高效。4.3 setnx / setxxsetname lqz# 不管 key 是否存在都設置setnx name lqz# key 不存在時才設置新增setname lqz nx# 等價于 SETNXsetname lqz xx# key 存在時才設置更新分布式鎖用SET key value NX EX 秒數(shù)實現(xiàn)“不存在才設置 自動過期”是分布式鎖的常見做法。setlock_code1NX EX10# 加鎖最多持有 10 秒del lock_code# 釋放鎖注意SETNX與EXPIRE分開寫不是原子的可能出現(xiàn)“已 setnx 但還沒 expire 時進程崩潰鎖永不釋放”的問題因此更推薦SET key value NX EX。4.4 批量操作 mget / msetmget key1 key2 key3# 批量獲取O(n)mset key1 value1 key2 value2 key3 value3# 批量設置O(n)n次get與一次mget的差別n次get n 次命令時間 n 次網(wǎng)絡往返時間。一次mget 1 次網(wǎng)絡往返 n 次命令時間省去大量網(wǎng)絡開銷。4.5 getset / append / strlengetset name lqznb# 設置新值并返回舊值O(1)append name666# 把 value 追加到舊值后面O(1)strlen name# 計算字符串長度注意中文按 UTF-8 字節(jié)數(shù)計算O(1)4.6 incrbyfloat / getrange / setrangeincrbyfloat age3.5# value 自增 3.5傳負值表示自減O(1)getrange key03# 獲取指定下標范圍的子串O(1)setrange key2xyz# 從指定下標開始覆蓋寫入 valueO(1)4.7 String 的其他要點底層編碼String 底層為SDS簡單動態(tài)字符串根據(jù)長度有int、embstr、raw三種編碼embstr用于較短字符串raw用于較長字符串。常用場景緩存、計數(shù)器、分布式鎖、會話Session、短信驗證碼、限流等。五、哈希Hash類型Hash 類似字典一個 key 對應多個 field-value適合存對象或一行的字段。5.1 基本操作 hget / hset / hdelhset key field value# 設置 hash key 對應的 field 的 valueO(1)hget key field# 獲取 hash key 對應的 field 的 valueO(1)hdel key field# 刪除 hash key 對應的 fieldO(1)示例hset user:1:info age23hget user:1:info age hset user:1:info name lqz hgetall user:1:info hdel user:1:info age5.2 判斷與計數(shù) hexists / hlenhexists user:1:info name# 判斷 field 是否存在O(1)hlen user:1:info# 獲取 field 數(shù)量O(1)5.3 批量操作 hmget / hmsethmget key field1 field2... fieldN# 批量獲取O(n)hmset key field1 value1 field2 value2# 批量設置O(n)5.4 全量操作 hgetall / hvals / hkeyshgetall key# 返回所有 field 和 valueO(n)hvals key# 返回所有 field 的 valueO(n)hkeys key# 返回所有 fieldO(n)小心使用hgetall它返回全部字段當字段很多時會阻塞 Redis生產(chǎn)環(huán)境建議用hscan游標式代替。兩個典型應用統(tǒng)計網(wǎng)站每個用戶主頁的訪問量hincrby user:1:info pageview count。緩存 MySQL 的一條記錄把行字段作為 field 存入 hash如hmset user:1 name lqz age 23。5.5 hsetnx / hincrby / hincrbyfloathsetnx key field value# field 不存在才設置O(1)hincrby key field intCounter# field 的 value 自增O(1)hincrbyfloat key field floatCounter# field 的 value 自增浮點數(shù)O(1)小結Hash 適合存“一個對象的多個字段”比把整個對象序列化成字符串更省內(nèi)存、方便單字段更新如只更新 age。六、算法與數(shù)據(jù)結構復雜度6.1 為什么重要算法和數(shù)據(jù)結構是編程的基石算法代碼的執(zhí)行邏輯和流程如for、if例如排序。數(shù)據(jù)結構變量的組織結構如list、map、set。寫 Redis 時理解每條命令的時間復雜度能避免在數(shù)據(jù)量巨大時把服務“打停”。6.2 大 O 表示法記號含義例子O(1)常數(shù)時間與數(shù)據(jù)量無關GET、SET、HSET、LPUSH、SADDO(log n)對數(shù)時間ZSet的ZADD/ZRANGEBYSCORE底層跳表O(n)線性時間隨數(shù)據(jù)量線性增長KEYS、SMEMBERS、HGETALL、LRANGE全量O(n2)平方時間某些嵌套遍歷算法時間復雜度消耗的時間空間復雜度消耗的內(nèi)存。6.3 生產(chǎn)環(huán)境注意點keys、smembers、hgetall等全量命令是O(n)key 很多時會阻塞 Redis。用scan、hscan、sscan代替全量命令逐批游標式遍歷不阻塞。單條命令盡可能快批量操作mget、mset、pipeline減少網(wǎng)絡往返。想刷題可參考力扣LeetCode難度分簡單、中等、困難排序等算法可查閱 Python 常用排序教程。七、列表List類型List 是有序的字符串列表可用來實現(xiàn)隊列、棧、消息隊列等。7.1 插入操作rpush key value1 value2... valueN# 從右側插入O(1~n)lpush key value1 value2... valueN# 從左側插入O(1~n)linsert key before|after value newValue# 在 value 前/后插入O(n)需遍歷列表示例linsert listkey before bjavalinsert listkey after b php7.2 刪除操作lpop key# 從左側彈出一個 itemO(1)rpop key# 從右側彈出一個 itemO(1)lrem key count value# 按 count 刪除 valueO(n)ltrim key start end# 按索引范圍修剪列表O(n)lrem的count規(guī)則count 0從左到右刪除最多 count 個 value 相等的項。count 0從右到左刪除最多Math.abs(count)個 value 相等的項。count 0刪除所有 value 相等的項。示例lrem listkey0a# 刪除列表中所有值 alrem listkey-1c# 從右側刪除 1 個 cltrim listkey14# 只保留下標 1~4 的元素7.3 查詢操作lrange key start end# 獲取指定索引范圍所有 item包含 endO(n)lindex key index# 獲取指定索引的 itemO(n)llen key# 獲取列表長度O(1)示例lrange listkey02# 下標 0~2lrange listkey1-1# 從第 1 個到倒數(shù)第 1 個lindex listkey0lindex listkey-17.4 修改操作lset key index newValue# 設置指定索引的值O(n)lset listkey2ppp# 把第 2 個位置設為 ppp7.5 阻塞式操作 blpop / brpopblpop keytimeout# LPOP 的阻塞版本timeout 為超時時間0 表示一直阻塞brpop keytimeout# RPOP 的阻塞版本timeout 為超時時間0 表示一直阻塞7.6 實戰(zhàn)用 List 組合出常見結構# 實現(xiàn)棧先進后出lpush lpop# 實現(xiàn)隊列先進先出lpush rpop# 固定大小的列表只保留最近 N 條lpush ltrim# 阻塞式消息隊列生產(chǎn)者-消費者lpush brpop實現(xiàn)時間軸timeLine把關注的人的微博按時間順序lpush進列表即可按時間軸展示。八、集合Set類型Set 是無序、去重的字符串集合適合做標簽、點贊、抽獎、共同好友等。8.1 基礎命令sadd key element# 向集合添加元素重復添加失敗O(1)srem key element# 移除元素O(1)scard key# 計算集合大小sismember key element# 判斷元素是否在集合中srandmember key count# 隨機取出 count 個元素不破壞集合spop key# 隨機彈出一個元素并移除smembers key# 獲取所有元素無序O(n)小心使用會阻塞8.2 集合運算sdiffkey1 key2# 差集在 key1 中但不在 key2 中sinter key1 key2# 交集同時在兩個集合中sunion key1 key2# 并集兩個集合所有元素把結果保存到新集合sdiffstore dest key1 key2# 差集存入 destsinterstore dest key1 key2# 交集存入 destsunionstore dest key1 key2# 并集存入 dest8.3 集合典型應用抽獎系統(tǒng)spop彈出用戶 id活動取消直接刪除集合。點贊 / 點踩 / 喜歡用戶點了贊就把用戶 id 放到該條記錄的集合中。標簽給用戶/文章打標簽sadd user:1:tags 標簽1 標簽2也可反向保存“關注某標簽的人有哪些”。共同好友使用集合的交集 / 并集 / 差集計算。總結sadd標簽相關。spop/srandmember隨機數(shù)相關抽獎、隨機推薦。sadd/sinter社交相關共同好友、共同關注。九、面試題與參考答案1. HTTP 協(xié)議詳情、版本、請求頭HTTP 協(xié)議基于 TCP 的“請求-響應”式無狀態(tài)應用層協(xié)議客戶端發(fā)起請求服務端返回響應。版本HTTP/1.0短連接每次請求都要新建連接。HTTP/1.1持久連接keep-alive、管線化、chunked分塊傳輸解決隊頭阻塞能力有限。HTTP/2二進制分幀、多路復用、頭部壓縮HPACK、服務端推送解決隊頭阻塞。HTTP/3基于 QUIC底層 UDP進一步降低連接建立延時。常見請求頭Host # 請求主機 User-Agent # 客戶端標識 Accept # 可接受的響應類型 Accept-Encoding # 可接受的壓縮編碼 Content-Type # 請求體類型如 application/json Content-Length # 請求體長度 Authorization # 認證信息 / Token Cookie # 會話 Cookie Cache-Control # 緩存策略 Connection # 連接控制keep-alive / close Referer # 來源頁面 Origin # 跨域來源 X-Forwarded-For # 經(jīng)過代理的客戶端 IP常見響應頭Content-Type、Content-Length、Set-Cookie、Cache-Control、Location、Server、Date等。2. 如何實現(xiàn)服務器給客戶端發(fā)送消息WebSocket 是什么普通的 HTTP 是“客戶端主動請求、服務端被動響應”無法直接由服務端主動推消息。實現(xiàn)服務端推送的常見方式輪詢Polling客戶端定時請求浪費資源。長輪詢Long Polling服務端掛起請求直到有新消息減少請求次數(shù)。SSEServer-Sent Events服務端單向推送基于 HTTP。WebSocket推薦方案。WebSocket 是什么一種基于 TCP 的全雙工通信協(xié)議。客戶端通過 HTTP 發(fā)起握手Upgrade: websocket升級后建立一條持久連接雙方可隨時互相發(fā)消息適合聊天、實時通知、在線游戲、實時行情等場景。常見實現(xiàn)Django Channels、FastAPI WebSocket、Node.js 的 Socket.IO 等。3. 悲觀鎖和樂觀鎖如何實現(xiàn)悲觀鎖假設一定會發(fā)生沖突訪問數(shù)據(jù)前先加鎖其他線程/事務被阻塞。實現(xiàn)MySQLSELECT ... FOR UPDATE、Redis 分布式鎖SET NX。樂觀鎖假設很少沖突不加鎖提交時檢查版本號是否變化變了則重試。實現(xiàn)MySQL 版本號字段version、Redis 使用WATCHMULTIEXEC的樂觀事務。在 Redis 中的實現(xiàn)方式# 悲觀鎖分布式鎖key 不存在才寫入并設置過期時間SET lock:order1NX EX5DEL lock:order# 樂觀鎖WATCH 監(jiān)視 key在事務期間若被修改則 EXEC 返回失敗WATCH stock:1 MULTI DECR stock:1 EXEC UNWATCH