散列

Redis 散列键 hash key 会将一个键和一个散列在数据库里关联起来, 散列中可以存任意多个字段 field. 与字符串一样, 散列字段和值既可以是文本数据, 也可以是二进制数据.

  • HSET: 为字段设置值

    HEST hash field value
    

    若已给定的字段是否已经存在与散列中, 该设置为一次更新操作, 覆盖旧值后返回0.
    相反, 则为一次创建操作, 命令将在散列里面关联起给定的字段和值, 然后返回1.

  • HSETNX: 只在字段不存在的情况下设置值

    HSETNX hash field value
    

    HSETNX 命令在字段不存在且成功设置值时, 返回1.
    字段已存在并设置值未成功时, 返回0.

  • HGET: 获取字段的值

    HGET hash field
    

    若查找的不存在的散列或字段, 则会返回空(nil)

示例: 短网址生成

为了给用户提供更多空间, 并记录用户在网站上的链接点击行为, 大部分社交网站都会将用户输入的网址转换为短网址. 当用户点击段网址时, 后台就会进行数据统计, 并引导用户跳转到原地址.
创建短网址本质上就是, 要创建出短网址ID与目标网址之间的映射, 并让用户访问短网址时, 根据短网址的ID映射记录中找出与之相对应的目标网址.

短网址 ID目标网址
RqRRz8nhttp://redisdoc.com/geo/index.html
RUwtQBxhttp://item.jd.com/117910607.html
  • HINCRBY: 对字段存储的整数值执行加法或减法操作

    HINCRBY hash field increment
    

    与字符串 INCRBY 命令一样, 如果散列字段里面存储着能够被 Redis 解释为整数的数字, 那么用户就可以使用 HINCRBY 命令为该字段的值加上指定的整数增量.
    该命令执行成功后, 将返回字段当前的值为命令的结果. 若要执行减法操作, increment 传入负数即可.

  • HINCRBYFLOAT: 对字段存储的数字执行浮点数加法或减法操作

    HINCRBYFLOAT hash field increment
    

    HINCRBYFLOAT 不仅可以使用整数作为增量, 还可以使用浮点数作为增量. 该命令执行成功后, 返回给定字段的当前值作为结果.
    此外, 不仅存储浮点数的字段可以使用该命令, 整数字段也可以使用该命令; 若计算结果可以表示为整数, 则会使用整数表示.

  • HSTRLEN: 获取字段的字节长度

    HSTRLEN hash field
    

    如果给定的字段或散列不存在, 将返回0

  • HEXISTS: 检查字段是否存在

    HEXISTS hash field
    

    如果存在, 返回1, 否则返回0

  • HDEL: 删除字段

    HDEL hash field
    
  • HLEN: 获取散列包含的字段数量

    HLEN hash
    

    若不存在返回0

  • HMSET: 一次为多个字段设置值

    HMSET hash field value [field value ...]
    

    该命令成功时返回 OK, 可使用新值覆盖旧值

  • HMGET: 一次获取多个字段值

    HMGET hash field [field ...]
    

    对于不存在的值, 返回 (nil)

  • HKEYS, HVALS, HGETALL: 获取所有字段, 所有值, 所有字段和值

    HEKYS 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 等操作散列不支持
  • 过期时间: 字符串键可以为每个键单独设置过期时间, 独立删除某个数据项, 而散列一但到期, 其所包含的所有字段和值都会被删除