|
1 | 1 | +++ |
2 | | -date = '2025-08-20T8:00:00+08:00' |
3 | | -draft = true |
| 2 | +date = '2025-08-21T8:00:00+08:00' |
| 3 | +draft = false |
4 | 4 | title = 'Redis Hash' |
5 | 5 | tags = ['Redis', 'Database'] |
6 | 6 | +++ |
7 | 7 |
|
| 8 | +## 散列 |
| 9 | +Redis 散列键 hash key 会将一个键和一个散列在数据库里关联起来, 散列中可以存任意多个字段 field. |
| 10 | +与字符串一样, 散列字段和值既可以是文本数据, 也可以是二进制数据. |
| 11 | + |
| 12 | +- HSET: 为字段设置值 |
| 13 | + ``` |
| 14 | + HEST hash field value |
| 15 | + ``` |
| 16 | + 若已给定的字段是否已经存在与散列中, 该设置为一次更新操作, 覆盖旧值后返回0. |
| 17 | + 相反, 则为一次创建操作, 命令将在散列里面关联起给定的字段和值, 然后返回1. |
| 18 | +
|
| 19 | +- HSETNX: 只在字段不存在的情况下设置值 |
| 20 | + ``` |
| 21 | + HSETNX hash field value |
| 22 | + ``` |
| 23 | + HSETNX 命令在字段不存在且成功设置值时, 返回1. |
| 24 | + 字段已存在并设置值未成功时, 返回0. |
| 25 | +
|
| 26 | +- HGET: 获取字段的值 |
| 27 | + ``` |
| 28 | + HGET hash field |
| 29 | + ``` |
| 30 | + 若查找的不存在的散列或字段, 则会返回空(nil) |
| 31 | +
|
| 32 | +### 示例: 短网址生成 |
| 33 | +为了给用户提供更多空间, 并记录用户在网站上的链接点击行为, 大部分社交网站都会将用户输入的网址转换为短网址. 当用户点击段网址时, 后台就会进行数据统计, 并引导用户跳转到原地址. |
| 34 | +创建短网址本质上就是, 要创建出短网址ID与目标网址之间的映射, 并让用户访问短网址时, 根据短网址的ID映射记录中找出与之相对应的目标网址. |
| 35 | +
|
| 36 | +| 短网址 ID | 目标网址 | |
| 37 | +| :-------- | :------- | |
| 38 | +| RqRRz8n | http://redisdoc.com/geo/index.html | |
| 39 | +| RUwtQBx | http://item.jd.com/117910607.html | |
| 40 | +
|
| 41 | +- HINCRBY: 对字段存储的整数值执行加法或减法操作 |
| 42 | + ``` |
| 43 | + HINCRBY hash field increment |
| 44 | + ``` |
| 45 | + 与字符串 INCRBY 命令一样, 如果散列字段里面存储着能够被 Redis 解释为整数的数字, 那么用户就可以使用 HINCRBY 命令为该字段的值加上指定的整数增量. |
| 46 | + 该命令执行成功后, 将返回字段当前的值为命令的结果. 若要执行减法操作, increment 传入负数即可. |
| 47 | +
|
| 48 | +- HINCRBYFLOAT: 对字段存储的数字执行浮点数加法或减法操作 |
| 49 | + ``` |
| 50 | + HINCRBYFLOAT hash field increment |
| 51 | + ``` |
| 52 | + HINCRBYFLOAT 不仅可以使用整数作为增量, 还可以使用浮点数作为增量. 该命令执行成功后, 返回给定字段的当前值作为结果. |
| 53 | + 此外, 不仅存储浮点数的字段可以使用该命令, 整数字段也可以使用该命令; 若计算结果可以表示为整数, 则会使用整数表示. |
| 54 | +
|
| 55 | +- HSTRLEN: 获取字段的字节长度 |
| 56 | + ``` |
| 57 | + HSTRLEN hash field |
| 58 | + ``` |
| 59 | + 如果给定的字段或散列不存在, 将返回0 |
| 60 | +
|
| 61 | +- HEXISTS: 检查字段是否存在 |
| 62 | + ``` |
| 63 | + HEXISTS hash field |
| 64 | + ``` |
| 65 | + 如果存在, 返回1, 否则返回0 |
| 66 | +
|
| 67 | +- HDEL: 删除字段 |
| 68 | + ``` |
| 69 | + HDEL hash field |
| 70 | + ``` |
| 71 | +
|
| 72 | +- HLEN: 获取散列包含的字段数量 |
| 73 | + ``` |
| 74 | + HLEN hash |
| 75 | + ``` |
| 76 | + 若不存在返回0 |
| 77 | +
|
| 78 | +- HMSET: 一次为多个字段设置值 |
| 79 | + ``` |
| 80 | + HMSET hash field value [field value ...] |
| 81 | + ``` |
| 82 | + 该命令成功时返回 OK, 可使用新值覆盖旧值 |
| 83 | +
|
| 84 | +- HMGET: 一次获取多个字段值 |
| 85 | + ``` |
| 86 | + HMGET hash field [field ...] |
| 87 | + ``` |
| 88 | + 对于不存在的值, 返回 (nil) |
| 89 | +
|
| 90 | +- HKEYS, HVALS, HGETALL: 获取所有字段, 所有值, 所有字段和值 |
| 91 | + ``` |
| 92 | + HEKYS hash |
| 93 | + HVALS hash |
| 94 | + HGETALL hash |
| 95 | + ``` |
| 96 | + 其中, HGETALL 命令返回的结果列表中, 没两个连续的元素代表散列中的一对字段和值, 奇数位置为字段, 偶数位置为字段值. |
| 97 | + 若散列不存在, 则返回控列表`(empty array)` |
| 98 | +
|
| 99 | +Redis 散列底层为无序存储的, 因此HKEYS, HVALS 和 HGETALL 可能会得到不同的结果, 因此不应该对其返回元素顺序做任何假设. |
| 100 | +
|
| 101 | +### 示例: 存储图数据 |
| 102 | +图是一直常用的数据结构, 这里使用 field=edge, value=weight 的表示法来存储图结构, 其中 edge 由 `start->edge` 构成 |
| 103 | +```Python |
| 104 | +from redis import Redis |
| 105 | +
|
| 106 | +def make_edge_from_vertexs(start, end): |
| 107 | + return str(start) + "->" + str(end) |
| 108 | +
|
| 109 | +def decompose_vertexs_from_edge_name(name): |
| 110 | + return name.split("->") |
| 111 | +
|
| 112 | +class Graph: |
| 113 | + def __init__(self, client, key): |
| 114 | + self.client = client |
| 115 | + self.key = key |
| 116 | +
|
| 117 | + def add_edge(self, start, end, weight): |
| 118 | + edge = make_edge_from_vertexs(start, end) |
| 119 | + self.client.hset(self.key, edge, weight) |
| 120 | +
|
| 121 | + def remove_edge(self, start, end): |
| 122 | + edge = make_edge_from_vertexs(start, end) |
| 123 | + return self.client.hdel(self.key, edge) |
| 124 | +
|
| 125 | + def get_edge_weight(self, start, end): |
| 126 | + edge = make_edge_from_vertexs(start, end) |
| 127 | + return self.client.hget(self.key, edge) |
| 128 | +
|
| 129 | + def has_edge(self, start, end): |
| 130 | + edge = make_edge_from_vertexs(start, end) |
| 131 | + return self.client.hexists(self.key, edge) |
| 132 | + |
| 133 | + def add_multi_edges(self, *tuples): |
| 134 | + nodes_and_weights = {} |
| 135 | + for start, end, weight in tuples: |
| 136 | + edge = make_edge_from_vertexs(start, end) |
| 137 | + nodes_and_weights[edge] = weight |
| 138 | + self.client.hset(self.key, mapping=nodes_and_weights) # hmset 在 4.0 已抛弃, 使用 .hset(mapping={...}) |
| 139 | +
|
| 140 | + def get_multi_edge_weights(self, *tuples): |
| 141 | + edge_list = [] |
| 142 | + for start, end in tuples: |
| 143 | + edge = make_edge_from_vertexs(start, end) |
| 144 | + edge_list.append(edge) |
| 145 | + return self.client.hmget(self.key, edge_list) |
| 146 | +
|
| 147 | + def get_all_edges(self): |
| 148 | + edges = self.client.hkeys(self.key) |
| 149 | + result = set() |
| 150 | + for edge in edges: |
| 151 | + start, end = decompose_vertexs_from_edge_name(edge) |
| 152 | + result.add((start, end)) |
| 153 | + return result |
| 154 | +
|
| 155 | + def get_all_edges_with_weight(self): |
| 156 | + edges_and_weights = self.client.hgetall(self.key) |
| 157 | + result = set() |
| 158 | + for edge, weight in edges_and_weights.items(): |
| 159 | + start, end = decompose_vertexs_from_edge_name(edge) |
| 160 | + result.add((start, end, weight)) |
| 161 | + return result |
| 162 | +
|
| 163 | +client = Redis(decode_responses=True) |
| 164 | +graph = Graph(client, "test-graph") |
| 165 | +graph.add_edge("a", "b", 30) |
| 166 | +graph.add_edge("c", "b", 25) |
| 167 | +graph.add_multi_edges(("b", "d", 70), ("d", "e", 10)) |
| 168 | +
|
| 169 | +print("edge a-> b weight:", graph.get_edge_weight("a", "b")) |
| 170 | +print("a->b 是否存在:", graph.has_edge("a", "b")) |
| 171 | +print("b->a 是否存在:", graph.has_edge("b", "a")) |
| 172 | +print("所有边:", graph.get_all_edges()) |
| 173 | +print("所有边和权重", graph.get_all_edges_with_weight()) |
| 174 | +``` |
| 175 | +这里的图数据结构提供了边和权重的功能, 可以快速检查边是否存在, 能够方便的添加和移除边, 适合存储结点较多但是边较少的稀疏图(sparse graph). |
| 176 | + |
| 177 | +### 示例: 使用散列键重新实现文章存储程序 |
| 178 | +```Python |
| 179 | +from redis import Redis |
| 180 | +from time import time |
| 181 | + |
| 182 | +class Article: |
| 183 | + def __init__(self, client, article_id): |
| 184 | + self.client = client |
| 185 | + self.article_id = str(article_id) |
| 186 | + self.article_hash = "article::" + self.article_hash |
| 187 | + def is_exists(self): |
| 188 | + return self.client.hexists(self.article_hash) |
| 189 | + def create(self, title, content, author): |
| 190 | + if self.is_exists(): |
| 191 | + return False |
| 192 | + article_data = { |
| 193 | + "title": title, |
| 194 | + "content": content, |
| 195 | + "author": author, |
| 196 | + "created_at": time(), |
| 197 | + } |
| 198 | + return self.client.hset(self.article_hash, mapping=article_data) |
| 199 | + def get(self): |
| 200 | + article_data = self.client.hgetall(self.article_hash) |
| 201 | + article_data["id"] = self.article_id # 添加 id 到文章数据, 方便用户操作 |
| 202 | + return article_data |
| 203 | + def update(self, title=None, content=None, author=None): |
| 204 | + if not self.is_exists(): |
| 205 | + return False |
| 206 | + article_data = {} |
| 207 | + if title is not None: |
| 208 | + article_data["title"] = title |
| 209 | + if content is not None: |
| 210 | + article_data["content"] = content |
| 211 | + if author is not None: |
| 212 | + article_data["author"] = author |
| 213 | + return self.client.hset(self.article_hash, mapping=article_data) |
| 214 | + |
| 215 | +client = Redis(decode_responses=True) |
| 216 | +article = Article(client, 10086) |
| 217 | +article.create("greeting", "hello world", "peter") |
| 218 | +``` |
| 219 | +- 字符串有 MSET, MSETNX 命令, 但是并没有为散列提供 HMSET, HMSETNX 命令, 所以创建文章之前要先通过 `is_exists()` 方法检查文章是否存在, 再考虑是否使用 HMSET 命令进行设置. |
| 220 | +- 在使用散列存储文章数据的时候, 为了避免数据库中出现键名冲突, 需要为每个属性设置一个独一无二的键, 例如 article::10086::title 键存储 id 为10086 文章的标题. |
| 221 | + |
| 222 | +## Wrapping Up |
| 223 | +string 和 hash 总结与对比 |
| 224 | +- 资源占用: 字符串键在数量较多的时候, 将占用大量内存和CPU时间. 相反, 将多个数据项存储到一个散列中可以有效减少内存和CPU消耗 |
| 225 | +- 支持的操作: 散列键支持的所有命令, 字符串键几乎都支持, 但字符串的 SETRANGE, GETRANGE 等操作散列不支持 |
| 226 | +- 过期时间: 字符串键可以为每个键单独设置过期时间, 独立删除某个数据项, 而散列一但到期, 其所包含的所有字段和值都会被删除 |
| 227 | + |
0 commit comments