面试系列-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 = <<
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章