面試系列-4 hash應用場景分析實踐

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"英國弗蘭明曾說過一句話:“不要等待運氣降臨,應該去努力掌握知識。”","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"1 前言","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大家好,我是阿沐!你的收穫便是我的喜歡,你的點贊便是對我的認可。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作爲一年開發經驗的畢業生,在上一個章節跟面試官聊了聊redis的基礎數據結構列表類型,我們憑藉日常知識積累跟面試官展開了","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"相愛相殺","attrs":{}},{"type":"text","text":"場景以及面試期間內心的活動狀況。通過結合項目在實際場景中的運用案例和知識點的細節,穩穩的對答如流。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼這一章節面試官會考驗我們對redis的hash數據結構的原理、場景、注意事項、實戰這些點進行考察。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好了,開始我們與面試官的博弈,這將是一個很長很長的面試過程,請大家!","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"2 數據結構hash的理解","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"面試官","attrs":{}},{"type":"text","text":":“小年輕,今天讓我考驗下你redis的hash數據結構知識,不是很厲害嘛,不給你搞個下馬威是不行了,我沒面子啊,我不要面子的嘛?”。休息完了,我們就繼續下一個話題吧,你是怎麼理解哈希類型的呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"面試者","attrs":{}},{"type":"text","text":":“嚯嚯嚯,看來是故意來找茬的,講完sting,講list,現在hash;告訴你我不怕”。非常非常地自信說道:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"redis中的哈希(hash或者","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"散列表","attrs":{}},{"type":"text","text":"),內部存儲很多鍵值對以key - [","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Field-Value","attrs":{}},{"type":"text","text":"]的形式存儲,也是一種","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"數組+鏈表","attrs":{}},{"type":"text","text":"的二維結構(本身又是一個 鍵值對結構)。正是因爲這樣,通常我們可以使用哈希存儲一個對象信息。下面是我對hash的關係圖如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/91/91a3d8916ec94e829749f4812c0aeed0.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"redis哈希關係鏈圖","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意點:從上圖我們可以看出,哈希的關係隱射實際上是field->value的映射,它們纔是一對。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"面試官","attrs":{}},{"type":"text","text":":“不要以爲知道一點點概念就洋洋得意,這是作爲一個開發最最最基礎的理念。”","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"3 常用的hash指令","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"面試官","attrs":{}},{"type":"text","text":":基於上面你對哈希的理解,是否可以簡單的介紹下hash的比較常見的指令呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"面試者","attrs":{}},{"type":"text","text":":“估計真是看我比較年輕,以爲的經驗是虛報的,這是要考驗我基礎是吧!那我可就不客氣了”。嗯嗯,那我說說我經常使用的一些操作指令吧。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"查找select","attrs":{}},{"type":"text","text":"指令操作:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"hget指令:hget key field 獲取哈希表key中給定字段的值,不存在返回nil;時間複雜度O(1)。\n\nhgetall指令:hgetall key 獲取哈希表key中的所有字段和值,不存在返回空列表;時間複雜度O(n),n是哈希表的大小。\n\nhlen指令:hlen key 獲取哈希表key中field的數量,不存在返回0;時間複雜度O(1)。\n\nhmget指令:hmget key [field ...] 獲取哈希表key中一個或多個給定字段的值,不存在返回nil;時間複雜度O(n),n爲給定字段的數量。\n\nhkeys指令:hkeys key 獲取哈希表key中所有字段,不存在返回空表;時間複雜度O(n),n爲哈希表的大小。\n\nhscan指令:hscan key cursor(遊標) [MATCH pattern(匹配的模式)] [COUNT count(指定從數據集裏返回多少元素,默認值爲 10 )] 獲取哈希表key中匹配元素。\n\nhvals指令:hlen key 獲取哈希表key中所有的字段的值,不存在返回空表;時間複雜度O(n),n是哈希表的大小。\n\nhexists指令:hexists key field 獲取哈希表key中field是否存在,存在返回1不存在返回0;時間複雜度O(1)。\n\nhstrlen指令:hstrlen key field 獲取哈希表key中字段長度,不存在返回0,否則返回長度整數;時間複雜度O(1)。\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"添加insert","attrs":{}},{"type":"text","text":"指令操作:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"hset指令:hset key value 將哈希表key中的字段的值設爲value,不存在則創建設置,否則將覆蓋舊值;時間複雜度O(1)。\n注意點:如果哈希表中字段已經存在且舊值已被新值覆蓋,返回0而不是1,不能搞錯。\n\nhmset指令:hmset field value [field value ...] 一次將多個field-value數據設置進哈希表中,表中已存在的字段會直接覆蓋;時間複雜度O(n),n爲field-value的數量。\n注意:不同於hset,若哈希表已存在字段值,重複設置將會返回OK,而不是0。\n\nhsetnx指令:hsetnx key field value 僅僅當哈希表中字段不存在時可設置,否則無效;時間複雜度O(1)。\n注意:跟setnx不同的是,若設置的字段已存在值,那麼當前操作將返回結果集爲0而不是OK。\n\nhincrby指令:hincrby key field increment 給哈希表中指定字段增加數值;時間複雜度O(1)。\n注意:執行hincrby命令後返回的是字段的最新值,而不是ok或者1。\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3、","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"刪除delete","attrs":{}},{"type":"text","text":"指令操作:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"hdel指令:hdel key field [field ...] 刪除哈希表中一個或多個字段,不存在則忽略;時間複雜度O(n),n爲要刪除字段的數量。\n注意:刪除操作返回值是刪除成功的數量,不存在的字段將被忽略。\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面是我整理哈希類型命令的時間複雜度,大家可以參考此表:","attrs":{}}]},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"
指令時間複雜度
hget key fieldO(1)
hgetall keyO(n),n是哈希表的大小
hlen keyO(1)
hmget key [field ...]O(n),n爲給定字段的數量
hkeys keyO(n),n爲哈希表的大小
hexists key fieldO(1)
hstrlen key fieldO(1)
hset key valueO(1)
hmset field value [field value ...]O(n),n爲field-value的數量
hsetnx key field valueO(1)
hincrby key field incrementO(1)
hdel key field [field ...]O(n),n爲要刪除字段的數量
"}}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"面試官","attrs":{}},{"type":"text","text":":“咦,年輕人善於整理功能劃分呀!可以可以,這樣做筆記也是一個不錯的選擇”。那麼我看你簡歷上你寫着熟練掌握redis的應用場景,可以簡單說下你是如何在項目中使用哈希數據表嘛?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"面試者","attrs":{}},{"type":"text","text":":“這不是 張飛喫豆芽,小菜一碟”。你好,面試官;沒問題的,下面我來闡述我具體的應該場景","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.1 哈希的使用場景","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"面試者","attrs":{}},{"type":"text","text":":其實hash的使用在項目中是最常見的一種數據結構,那麼我們通常會使用hash結構來存儲網站用戶的基礎信息;也可以用來定時統計指定的某些文章的閱讀總數等等。實際上我們都是根據自己的業務場景來決定怎麼用。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"面試官","attrs":{}},{"type":"text","text":":嗯嗯,那麼可以簡單的介紹下你是如何使用的?面試官還是一副嚴肅的表情,彷彿我欠了她幾萬塊錢一樣,搞的這麼嚴肅我都賴的面試了。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.1.1 用戶信息","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們首先創建一個關係型的用戶信息數據表,存儲用戶的基礎信息(如果存在","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"冷熱數據分離","attrs":{}},{"type":"text","text":",或者","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"分表分庫","attrs":{}},{"type":"text","text":"。做法都一樣):","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"sql"},"content":[{"type":"text","text":"CREATE TABLE `mumu_user` (\n  `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用戶ID',\n  `user_name` varchar(255) NOT NULL DEFAULT '' COMMENT '用戶暱稱',\n  `user_pwd` varchar(64) NOT NULL DEFAULT '' COMMENT '用戶密碼',\n  `user_email` varchar(125) NOT NULL DEFAULT '' COMMENT '用戶郵箱',\n  `user_gender` tinyint(2) NOT NULL DEFAULT '0' COMMENT '用戶性別 0-保密;1-男;2-女',\n  `user_desc` varchar(255) NOT NULL DEFAULT '' COMMENT '用戶描述',\n  `create_at` int(10) NOT NULL DEFAULT '0' COMMENT '註冊時間',\n  PRIMARY KEY (`user_id`),\n) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用戶信息基礎表';\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以添加幾條用戶數據:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"sql"},"content":[{"type":"text","text":"insert into `mumu_user` (`user_name`, `user_pwd`,`user_email`,`user_desc`,`create_at`) VALUES('李阿沐', '123456', '[email protected]', '我是阿沐', unix_timestamp());\n\ninsert into `mumu_user` (`user_name`, `user_pwd`,`user_email`,`user_desc`,`create_at`) VALUES('李阿沐1', '123456789', '[email protected]', '我是阿沐啊', unix_timestamp());\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼我們使用Redis哈希結構存儲用戶信息的示意圖如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/8b/8bf6bcd02732e07a5acc424d9fbbb370.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"hash存儲用戶基礎信息","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"php"},"content":[{"type":"text","text":"connect('127.0.0.1', 6379);\n//echo \"Server is running: \" . $redis->ping();\n\n$data = [\n    'user_id'    => 1001,\n    'user_name'  => '李阿沐',\n    'user_email' => '[email protected]',\n    'user_desc'  => '我是阿沐',\n];\n\n$key = sprintf('user:info:%u', $data['user_id']);\n\n//向 hash 表中批量添加數據:hMset\n$result = $redis->hMSet($key, $data);\n$redis->expire($key,120);\n\nif ($result) exit('批量設置用戶信息成功!');\n\nexit('批量設置用戶信息失敗!');\n\n-- 終端查詢\n127.0.0.1:6379> HGETALL user:info:1001\n1) \"user_id\"\n2) \"1001\"\n3) \"user_name\"\n4) \"\\xe6\\x9d\\x8e\\xe9\\x98\\xbf\\xe6\\xb2\\x90\"\n5) \"user_email\"\n6) \"[email protected]\"\n7) \"user_desc\"\n8) \"\\xe6\\x88\\x91\\xe6\\x98\\xaf\\xe9\\x98\\xbf\\xe6\\xb2\\x90\"\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"面試官","attrs":{}},{"type":"text","text":":嗯嗯,這是最基礎的語法使用場景,沒有什麼特別強調的,你還可以說說在其他方面的使用嗎?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"面試者","attrs":{}},{"type":"text","text":":可以,我就舉一個比較簡單的案例,通過一個活動中的某一個小部分用hash的一個小場景吧。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.1.2 抽獎場景","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"場景","attrs":{}},{"type":"text","text":":公司要做一個抽獎活動,在網頁上共有8個道具可以抽獎,最大的是一輛豪華蘭博基尼🚘,限制數量2量;其他道具各自限制抽獎數量,其中一個道具不限量,所有用戶抽獎必中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"如何考慮","attrs":{}},{"type":"text","text":":① ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"保證用戶必中","attrs":{}},{"type":"text","text":" ② ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"保證道具不限超","attrs":{}},{"type":"text","text":" ③ ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"保證併發情況下原子性操作","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼大部分剛初入茅廬的小夥伴針對這三種情況如何解決呢?可能會有這種操作情況:爲了保證不限超道具數量,會先redis->get(id)道具數量,然後拿到結果跟限制的數量對比;這種操作不是不可以,但是我們要考慮高併發的情況下,如何保證原子操作。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"解決思路","attrs":{}},{"type":"text","text":":① 在道具概率分配ok的情況下,要對限制數量的道具進行一個","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"兜底","attrs":{}},{"type":"text","text":"操作 ② 每次用戶抽獎對抽中的獎勵進行數量檢測 ③ 併發情況下:1.我們可以使用hincrby原子操作記錄道具抽中的次數 2. 也可以使用get、set,但是必須要使用redis+lua實現原子操作,保證數據ok","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面是代碼案例:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"php"},"content":[{"type":"text","text":"// 抽獎道具列表 道具列表可擴展多個 hash存儲方便統一獲取數據分析\nconst DRAW_PROP_LIST = [\n    [\n        'prop_id' => 123,\n        'prop_name' => '精選課堂筆記',\n        'limit' => 10,\n        'chance' => 15,\n    ],\n    [\n        'prop_id' => 1234,\n        'prop_name' => '豪華蘭博基尼',\n        'limit' => 2,\n        'chance' => 10,\n    ],\n    [\n        'prop_id' => 12345,\n        'prop_name' => 'python入門實戰教程',\n        'limit' => 3,\n        'chance' => 5,\n    ],\n    [\n        'prop_id' => 123456,\n        'prop_name' => 'k8s實踐書籍',\n        'limit' => 1,\n        'chance' => 70,\n    ],\n];\n\n//randomChance 概率方法\n$reward = DRAW_PROP_LIST[randomChance(array_column(DRAW_PROP_LIST, 'chance'))];\n\n$key = \"prop:count:record\";\n\nfor ($i = 1; $i hIncrBy($key, $reward['prop_id'], 1);\n\n    echo $count.'-';\n\n    if ($count > $reward['limit']) {\n        echo '當前道具id爲'.$reward['prop_id'].'已被抽獎完畢,可以考慮兜底數據返回給用戶';\n        break;\n    }\n}\n// 結果集 1-2-當前道具id爲123456已被抽獎完畢,可以考慮兜底數據返回給用戶\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"額外補充,我們也可以使用","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"redis+lua保證原子","attrs":{}},{"type":"text","text":"操作設置:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"lua"},"content":[{"type":"text","text":"$redis = new Redis();\n$redis->connect('127.0.0.1', 6379);\n//echo \"Server is running: \" . $redis->ping();\n\n$lua = <<
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章