散列
Redis 散列键 hash key 会将一个键和一个散列在数据库里关联起来, 散列中可以存任意多个字段 field. 与字符串一样, 散列字段和值既可以是文本数据, 也可以是二进制数据.
HSET: 为字段设置值
PLAINTEXTHEST hash field value
若已给定的字段是否已经存在与散列中, 该设置为一次更新操作, 覆盖旧值后返回0.
相反, 则为一次创建操作, 命令将在散列里面关联起给定的字段和值, 然后返回1.HSETNX: 只在字段不存在的情况下设置值
PLAINTEXTHSETNX hash field value
HSETNX 命令在字段不存在且成功设置值时, 返回1.
字段已存在并设置值未成功时, 返回0.HGET: 获取字段的值
PLAINTEXTHGET hash field
若查找的不存在的散列或字段, 则会返回空(nil)
示例: 短网址生成
为了给用户提供更多空间, 并记录用户在网站上的链接点击行为, 大部分社交网站都会将用户输入的网址转换为短网址. 当用户点击段网址时, 后台就会进行数据统计, 并引导用户跳转到原地址.
创建短网址本质上就是, 要创建出短网址ID与目标网址之间的映射, 并让用户访问短网址时, 根据短网址的ID映射记录中找出与之相对应的目标网址.
短网址 ID | 目标网址 |
---|---|
RqRRz8n | http://redisdoc.com/geo/index.html |
RUwtQBx | http://item.jd.com/117910607.html |
HINCRBY: 对字段存储的整数值执行加法或减法操作
PLAINTEXTHINCRBY hash field increment
与字符串 INCRBY 命令一样, 如果散列字段里面存储着能够被 Redis 解释为整数的数字, 那么用户就可以使用 HINCRBY 命令为该字段的值加上指定的整数增量.
该命令执行成功后, 将返回字段当前的值为命令的结果. 若要执行减法操作, increment 传入负数即可.HINCRBYFLOAT: 对字段存储的数字执行浮点数加法或减法操作
PLAINTEXTHINCRBYFLOAT hash field increment
HINCRBYFLOAT 不仅可以使用整数作为增量, 还可以使用浮点数作为增量. 该命令执行成功后, 返回给定字段的当前值作为结果.
此外, 不仅存储浮点数的字段可以使用该命令, 整数字段也可以使用该命令; 若计算结果可以表示为整数, 则会使用整数表示.HSTRLEN: 获取字段的字节长度
PLAINTEXTHSTRLEN hash field
如果给定的字段或散列不存在, 将返回0
HEXISTS: 检查字段是否存在
PLAINTEXTHEXISTS hash field
如果存在, 返回1, 否则返回0
HDEL: 删除字段
PLAINTEXTHDEL hash field
HLEN: 获取散列包含的字段数量
PLAINTEXTHLEN hash
若不存在返回0
HMSET: 一次为多个字段设置值
PLAINTEXTHMSET hash field value [field value ...]
该命令成功时返回 OK, 可使用新值覆盖旧值
HMGET: 一次获取多个字段值
PLAINTEXTHMGET hash field [field ...]
对于不存在的值, 返回 (nil)
HKEYS, HVALS, HGETALL: 获取所有字段, 所有值, 所有字段和值
PLAINTEXTHEKYS hash HVALS hash HGETALL hash
其中, HGETALL 命令返回的结果列表中, 没两个连续的元素代表散列中的一对字段和值, 奇数位置为字段, 偶数位置为字段值.
若散列不存在, 则返回控列表(empty array)
Redis 散列底层为无序存储的, 因此HKEYS, HVALS 和 HGETALL 可能会得到不同的结果, 因此不应该对其返回元素顺序做任何假设.
示例: 存储图数据
图是一直常用的数据结构, 这里使用 field=edge, value=weight 的表示法来存储图结构, 其中 edge 由 start->edge
构成
from redis import Redis
def make_edge_from_vertexs(start, end):
return str(start) + "->" + str(end)
def decompose_vertexs_from_edge_name(name):
return name.split("->")
class Graph:
def __init__(self, client, key):
self.client = client
self.key = key
def add_edge(self, start, end, weight):
edge = make_edge_from_vertexs(start, end)
self.client.hset(self.key, edge, weight)
def remove_edge(self, start, end):
edge = make_edge_from_vertexs(start, end)
return self.client.hdel(self.key, edge)
def get_edge_weight(self, start, end):
edge = make_edge_from_vertexs(start, end)
return self.client.hget(self.key, edge)
def has_edge(self, start, end):
edge = make_edge_from_vertexs(start, end)
return self.client.hexists(self.key, edge)
def add_multi_edges(self, *tuples):
nodes_and_weights = {}
for start, end, weight in tuples:
edge = make_edge_from_vertexs(start, end)
nodes_and_weights[edge] = weight
self.client.hset(self.key, mapping=nodes_and_weights) # hmset 在 4.0 已抛弃, 使用 .hset(mapping={...})
def get_multi_edge_weights(self, *tuples):
edge_list = []
for start, end in tuples:
edge = make_edge_from_vertexs(start, end)
edge_list.append(edge)
return self.client.hmget(self.key, edge_list)
def get_all_edges(self):
edges = self.client.hkeys(self.key)
result = set()
for edge in edges:
start, end = decompose_vertexs_from_edge_name(edge)
result.add((start, end))
return result
def get_all_edges_with_weight(self):
edges_and_weights = self.client.hgetall(self.key)
result = set()
for edge, weight in edges_and_weights.items():
start, end = decompose_vertexs_from_edge_name(edge)
result.add((start, end, weight))
return result
client = Redis(decode_responses=True)
graph = Graph(client, "test-graph")
graph.add_edge("a", "b", 30)
graph.add_edge("c", "b", 25)
graph.add_multi_edges(("b", "d", 70), ("d", "e", 10))
print("edge a-> b weight:", graph.get_edge_weight("a", "b"))
print("a->b 是否存在:", graph.has_edge("a", "b"))
print("b->a 是否存在:", graph.has_edge("b", "a"))
print("所有边:", graph.get_all_edges())
print("所有边和权重", graph.get_all_edges_with_weight())
这里的图数据结构提供了边和权重的功能, 可以快速检查边是否存在, 能够方便的添加和移除边, 适合存储结点较多但是边较少的稀疏图(sparse graph).
示例: 使用散列键重新实现文章存储程序
from redis import Redis
from time import time
class Article:
def __init__(self, client, article_id):
self.client = client
self.article_id = str(article_id)
self.article_hash = "article::" + self.article_hash
def is_exists(self):
return self.client.hexists(self.article_hash)
def create(self, title, content, author):
if self.is_exists():
return False
article_data = {
"title": title,
"content": content,
"author": author,
"created_at": time(),
}
return self.client.hset(self.article_hash, mapping=article_data)
def get(self):
article_data = self.client.hgetall(self.article_hash)
article_data["id"] = self.article_id # 添加 id 到文章数据, 方便用户操作
return article_data
def update(self, title=None, content=None, author=None):
if not self.is_exists():
return False
article_data = {}
if title is not None:
article_data["title"] = title
if content is not None:
article_data["content"] = content
if author is not None:
article_data["author"] = author
return self.client.hset(self.article_hash, mapping=article_data)
client = Redis(decode_responses=True)
article = Article(client, 10086)
article.create("greeting", "hello world", "peter")
- 字符串有 MSET, MSETNX 命令, 但是并没有为散列提供 HMSET, HMSETNX 命令, 所以创建文章之前要先通过
is_exists()
方法检查文章是否存在, 再考虑是否使用 HMSET 命令进行设置. - 在使用散列存储文章数据的时候, 为了避免数据库中出现键名冲突, 需要为每个属性设置一个独一无二的键, 例如 article::10086::title 键存储 id 为10086 文章的标题.
Wrapping Up
string 和 hash 总结与对比
- 资源占用: 字符串键在数量较多的时候, 将占用大量内存和CPU时间. 相反, 将多个数据项存储到一个散列中可以有效减少内存和CPU消耗
- 支持的操作: 散列键支持的所有命令, 字符串键几乎都支持, 但字符串的 SETRANGE, GETRANGE 等操作散列不支持
- 过期时间: 字符串键可以为每个键单独设置过期时间, 独立删除某个数据项, 而散列一但到期, 其所包含的所有字段和值都会被删除