我们可以把一些有关联的键值对作为一个整体,存储为另一个键的值。这种类似于json和python字典的数据类型就叫做hash,中文叫哈希。
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。
文章目录
- hash数据类型
- hashmap内存结构
- 常用命令
- json字符串 vs hash
- 注意事项
- hash实现购物车
- 购物车模型
- 商品模型
- 改进商品模型
- hash实现抢购
hash数据类型
其实可以理解为redis中存了一个redis的感觉,不过外面这个键叫做key,里面的key叫做field,字段或者域。
注意哈希的值只能是string,不要在哈希中又嵌套一个哈希。
hashmap内存结构
redis的hash数据结构在内存中采用hashmap,了解hashmap在内存中的结构有助于帮助我们理解为什么hash结构查询非常快,也能帮助我们理解为什么说数据量不大的时候是数组结构,数据量大的时候是链表结构。
hashmap的底层由数组和链表一起构成,内存中一块连续的区域构成数组,通过计算key的hash值然后对数组长度取模来决定这个key存在哪一块区域。因为查询次数为1,所以查询速度很快,这也就是为什么数据量小的时候就是个数组结构。
数据量多了以后肯定会出现key的hash值计算出来一致的情况,也就是hash冲突,hashmap是通过单向链表来解决这个问题。数组中存储的只是一个单链表的头节点。如果不同的key映射到了数组的同一位置处,就将其放入单链表中。
下图引用自知乎专栏,出处在此。
真是基于这一数据存储特点,hash数据结构在查询的时候是有顺序的,但是并不是按照key的输入前后顺序。而删除增加修改都不会改变数据的顺序。
常用命令
- hset key field value
对key中的某个域进行赋值
127.0.0.1:6380[1]> hset user:1 name xiaofu
(integer) 1
127.0.0.1:6380[1]> type user:1
hash
127.0.0.1:6380[1]>
这里key为user:1,其中存储了一个哈希数据类型。哈希当中有一个键值对,哈希中的键为name,值为xiaofu
注意如果是已经存在的key,但是类型是string,赋值hash类型的话会报错。但反之却可以给一个哈希类型赋值一个string
27.0.0.1:6380[1]> set user:1 hahaha
OK
127.0.0.1:6380[1]> type user:1
string
127.0.0.1:6380[1]> hset user:1 name xiaofu
(error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6380[1]>
- hmset key field1 value1 field2 value2 …
对多个域同时赋值
127.0.0.1:6380[1]> hmset user:2 name xiaofu age 8 sex male
OK
127.0.0.1:6380[1]>
- hsetnx key field value
只有在key中有该field不做动作,如果没有该field就赋值
127.0.0.1:6380> hset user name xiaofu
(integer) 1
127.0.0.1:6380> hsetnx user age 18
(integer) 1
127.0.0.1:6380> hsetnx user name james
(integer) 0
127.0.0.1:6380> hgetall user
1) "name"
2) "xiaofu"
3) "age"
4) "18"
127.0.0.1:6380>
- hget key field
- hmget key field1 field2 …
- hgetall key
获取哈希中的一个或多个或所有域对应的值
127.0.0.1:6380[1]> hgetall user:2
1) "name"
2) "xiaofu"
3) "age"
4) "8"
5) "sex"
6) "male"
127.0.0.1:6380[1]> hget user:2 name
"xiaofu"
127.0.0.1:6380[1]> hmget user:2 name sex age
1) "xiaofu"
2) "male"
3) "8"
127.0.0.1:6380[1]>
这里hgetall的输出看着会比较别扭,单数行是field,双数行是field对应的值
- hkeys key
获取一个哈希类型中所有的field
27.0.0.1:6380[1]> hkeys user:2
1) "name"
2) "age"
3) "sex"
127.0.0.1:6380[1]>
- hvals key
获取一个哈希类型中所有的值
127.0.0.1:6380[1]> hvals user:2
1) "2"
2) "xiaofu"
3) "22"
4) "male"
127.0.0.1:6380[1]>
- hlen key
获取一个哈希类型中所有field的个数
27.0.0.1:6380[1]> hlen user:2
(integer) 3
127.0.0.1:6380[1]>
- hdel key field1 field2 …
删除一个或多个哈希数据中的域
127.0.0.1:6380[1]> hgetall user:2
1) "name"
2) "xiaofu"
3) "age"
4) "8"
5) "sex"
6) "male"
127.0.0.1:6380[1]> hdel user:2 age
(integer) 1
127.0.0.1:6380[1]> hgetall user:2
1) "name"
2) "xiaofu"
3) "sex"
4) "male"
127.0.0.1:6380[1]>
del key 是删除整个哈希数据
- hincrby key field num
对哈希中的某一个整数类型的域进行增加操作
127.0.0.1:6380[1]> hgetall user:2
1) "id"
2) "2"
3) "name"
4) "xiaofu"
5) "age"
6) "18"
7) "sex"
8) "male"
127.0.0.1:6380[1]> hincrby user:2 age 4
(integer) 22
127.0.0.1:6380[1]>
hash中没有减小的命令,可以将num变为负数达到减小的效果
- hincrbyfloat key field num
对哈希中的某一个浮点型的域进行增加操作
- hexists key field
查询哈希中某一个域是否存在,存在返回1,不存在返回0
127.0.0.1:6380[1]> hexists user:2 hobby
(integer) 0
127.0.0.1:6380[1]> hexists user:2 name
(integer) 1
127.0.0.1:6380[1]>
json字符串 vs hash
json字符串更多的是做为一个整体给外界去读取,而很少会修改;哈希因为是按照field去分开存储,更多的是方便修改。
当然哈希中也可以存储jason字符串,例如下面实例中提到的购物车模型。
注意事项
- 哈希类型十分贴近对象的数据存储形式,并且可以灵活添加删除对象属性。但hash的设计初衷不是为了存储大量对象而设计的,切记不可滥用,更不可将哈希做为对象列表使用
- hgetall操作可以获取全部属性,如果内部field过多,遍历整体数据效率就会很低,有可能成为数据访问的瓶颈
hash实现购物车
用户实时对购物车内的商品种类进行添加和删除,同时对每一个种类的数量也要进行实时增减,涉及到实时地读取数据库,所以考虑用redis。
购物车模型
将每个用户的购物车做为一个哈希,用户id做为key,购物车内容做为哈希数据类型,商品名称做为field,商品数量做为value,如下
127.0.0.1:6380> hmset user:1 good:1 3 good:2 20
OK
127.0.0.1:6380> hmset user:2 good:2 10 good:4 8
OK
127.0.0.1:6380>
此时有两个用户有了自己的购物车
如果要给用户1添加45个商品3,如下
127.0.0.1:6380> hset user:1 good:3 45
(integer) 1
127.0.0.1:6380>
用户1去查看自己的购物车,如下
127.0.0.1:6380> hgetall user:1
1) "good:1"
2) "3"
3) "good:2"
4) "20"
5) "good:3"
6) "45"
127.0.0.1:6380>
用户1不想要商品2了,全部删除,如下
127.0.0.1:6380> hdel user:1 good:2
(integer) 1
127.0.0.1:6380> hgetall user:1
1) "good:1"
2) "3"
3) "good:3"
4) "45"
127.0.0.1:6380>
用户1想多买2个商品3,如下
127.0.0.1:6380> hincrby user:1 good:3 2
(integer) 47
127.0.0.1:6380> hgetall user:1
1) "good:1"
2) "3"
3) "good:3"
4) "47"
127.0.0.1:6380>
购物车模型仅仅是解决了商品的数量问题,但是还没有解决商品的详情显示问题,例如商品图片,价格等等
商品模型
比较容易想到的解决方式就是将上述的商品field变为两个,分别是good:1:nums
和good:1:info
分别保存商品的数量和商品的详情。因为商品详情不会经常改,所以可以直接用json字符串来表示
127.0.0.1:6380> hmset user:3 good:1:nums 3 good:1:info {name:book1,pic:123,price:20}
OK
127.0.0.1:6380> hgetall user:3
1) "good:1:nums"
2) "3"
3) "good:1:info"
4) "{name:book1,pic:123,price:20}"
127.0.0.1:6380>
但是现在有个问题,假如用户4也买这个商品,只是商品数量不太一样
127.0.0.1:6380> hmset user:4 good:1:nums 5 good:1:info {name:book1,pic:123,price:20}
OK
127.0.0.1:6380> hgetall user:4
1) "good:1:nums"
2) "5"
3) "good:1:info"
4) "{name:book1,pic:123,price:20}"
127.0.0.1:6380>
这样在数据库中造成了大量的数据重复,所以应该考虑将商品的info提取出来,做一个单独的哈希
改进商品模型
所以最后的模型应该如下
127.0.0.1:6380> hmset user:5 good:1 10 good:2 8
OK
127.0.0.1:6380> hmset good:1 name book1 price 10 pic 111
OK
127.0.0.1:6380> hmset good:2 name book2 price 20 pic 222
OK
127.0.0.1:6380>
这样在生成购物车页面的时候只需要查询一次数据库,并且将用户和商品分开节约了数据库资源。
而且商品的信息也可以进行快速的修改(被商家),例如价格。
hash实现抢购
抢购或者秒杀的时候,商品数量瞬息万变,实时性高而且有原子性要求,适合用redis。
假设一个商家在双十一销售三种食物,每种限购500个。考虑实际情况我们可以对每个商品对象建立一个哈希,这里如果我们只考虑商品数量的话,可以创建一个哈希,key为商家id,field为三种食物的名字,value为食物剩余数量
127.0.0.1:6380> hmset shop:1 food:1 500 food:2 500 food:3 500
OK
127.0.0.1:6380> hgetall shop:1
1) "food:1"
2) "500"
3) "food:2"
4) "500"
5) "food:3"
6) "500"
127.0.0.1:6380>
每次有买家下单,就可以进行递减操作。但是哈希里面没有递减操作,可以将递增的偏移量设置为负值。
127.0.0.1:6380> hincrby shop:1 food:1 -5
(integer) 495
127.0.0.1:6380> hincrby shop:1 food:1 -20
(integer) 475
127.0.0.1:6380>
这里我们只考虑数据库的操作,不考虑是否还有商品之类的逻辑判断,逻辑判断交给业务代码去实现。