【Redis】数据类型的详解与使用场景【原创】

文章目录

Redis数据类型的详解与使用场景

1-1 NoSQL的概述

1. 概述

NoSQL,Not Only SQL,泛指非关系型数据库。

随 Web2.0 的诞生,传统关系型数据库难以应对 Web2.0,尤其是超大规模的高并发社区。NoSQL 在当今大数据程序下较为流行。


2. 为什么需要NoSQL

关系型数据库容易暴露如下问题:

  • High performance - 高并发读写问题
  • Huge Storage - 海量数据的高效率存储和访问
  • High Scalability && High Availability -高可扩展性和高可用性

扩展:Web 1.0时,浏览网页都是不能互动的,而Web 2.0时,基于Web的时代,这时候已经可以互动了,比如微博对他人进行点赞等操作,但是用的传统关系型数据库已经不再是最合适的选择,尤其对于超大规模和高并发SNS交互型类型的网站。这里就会暴露很多问题,如下三个问题:

  • High performance - 高并发读写问题,因此数据库的并发负载就非常高了。

  • Huge Storage - 海量数据的高效率存储和访问(例如:某软件、每月2.5亿数据需要插入,如果查询在这2.5亿,那么对于关系型数据库效率是非常低的)。

  • High Scalability && High Availability -高可扩展性和高可用性,基于Web的架构中,数据库很难横向扩展,当一个应用的用户量和访问量与日俱增的时候,关系型数据库无法像应用服务器、数据库服务器这些通过添加硬件来搭建负载均衡,这样对于数据库系统的升级和扩展是很痛苦的事情(往往需要停机维护,数据迁移)。


3. NoSQL产品

主流产品有:Redis、mongoDB、CouchDB、Cassandra、riak、membase


4. 分类

四大分类:

  • 键值(Key-Value)存储:比如Redis,优点是快速查询,缺点:存储的数据缺少结构化。
  • 列存储
  • 文档数据库:比如mongoDB,优点:要求数据格式不是很严格。缺点:查询性能不是很好,缺少统一的查询语法。
  • 图形数据库

分类 实例 应用场景 数据模型 优点 缺点
键值对(key-value) Redis、Voldemort 内存缓存,用于处理大量数据的高访问负载,也可用于日志系统等 key 指向 value 的键值对,通常是用 HashTable 来实现 查找速度快 数据无结构化,通常只被当做字符串或二进制数据
列存储数据库 HBase、Riak 分布式文件系统 以列簇式存储,讲同一列数据存储在一起 1. 查找速度快
2. 扩展性强
3. 更容易进行分布式扩展
功能相对局限
文档型数据库 MongoDb、CouchDB Web 应用,类似于 Key-Value key-value 对应的键值对,value 为结构化的数据 1. 数据结构要求宽松
2. 表结构可变,无需像关系型数据库一样预先定义表结构
查询性能低,且查询语法不统一
图形数据库(Graph) Neo4j、InfoGrid 社交网络、推荐系统等 图结构 可以利用图结构相关算法,如最短路径寻址、N度关系查找等 许多时候需要对整个图进行计算才能得到最终结果,效率不高;而且做分布式集群较困难

5. 特点
  • 易扩展:由于属于非关系型的,数据之间没有关系,所以非常易扩展
  • 灵活的数据模型:不需要对读写的数据建立字段
  • 大数据量,高性能:对于大数据量和高并发的读写性能支持很好,官方给定数据,写操作 8w次/s,读操作 11w次/s
  • 高可用:在不影响系统性能情况下,可以使用框架

2-1 Redis的概述

1. 概述

Redis,是C语言开发的开源的高性能的键值对的数据库,通过提供多种键值数据类型来适应不同场景下的存储需求,目前支持的键值数据类型有很多种,支持的键值数据类型:

  • 字符串类型
  • 列表类型
  • 有序集合类型
  • 散列类型
  • 集合类型

2. 应用场景
  • 缓存:数据的查询,新闻和商品的查询等,聊天室的在线好友列表
  • 任务队列
  • 应用排行榜
  • 网站访问统计
  • 数据过期处理
  • 分布式集群架构中的session分离

3. Redis的特点
  • 性能优秀,数据在内存中,读写速度非常快,支持并发 10W QPS。
  • 单进程单线程,是线程安全的,采用 IO 多路复用机制。
  • 丰富的数据类型,支持字符串(strings)、散列(hash )、列表(lists)、集合(sets)、有序集合(sorted sets)、HyperLogLog、位图、流、地理坐标等。
  • 支持数据持久化。可以将内存中数据保存在磁盘中,重启时加载。
  • 不仅可单机使用,还可多机使用,支持主从复制,哨兵(Sentinel)和集群功能。
  • 功能完备,Redis提供了很多非常实用的附加功能,比如自动过期、流水线、事务、数据持久化等。
  • 可以用作分布式锁。
  • 可以作为消息中间件使用,支持发布订阅。

如图:

image-20210307002715760

4. Redis为什么如此快

image-20210424212230723

A. 数据保存在内存中

Redis数据保存在内存中,读写操作只需要访问内存,不需要磁盘IO


B. 底层数据结构

Redis的数据是以key:value的格式存储在散列表中的,时间复杂度为:o(1)

Redis为value定义了丰富的数据结构,包括动态字符串、双向链表、压缩列表、hash、跳表和整数数组,可以根据value的特性选择最高效的数据结构


C. 单线程模型

Redis的网络IO和数据读写使用单线程模型,可以绑定CPU,这避免了线程上下文切换带来的开销。

注意:Redis 6.0对网络请求引入了多线程模型,但读写操作还是单线程。

如下图:

image-20210325000130410


D. IO多路复用

Redis采用epoll的网络模型,如下图:

image-20210325000219432

内核会一直监听新的 socket 连接事件的和已建立 socket 连接的读写事件,把监听到的事件放到事件队列,Redis 使用单线程不停的处理这个事件队列,这避免了阻塞等待连接和读写事件到来。


这些事件绑定了回调函数,会调用 Redis 的处理函数进行处理。


3-1 Redis的安装

Redis官方并不支持Windows系统,所以Windows下可通过虚拟机或Docker等手段进行安装,但如果是只用来测试的话,也可以通过下载msi文件进行下载安装,下载地址为:

https://github.com/microsoftarchive/redis/releases/tag/win-3.2.100

注意:Windows下的安装包还停留在3.2版本且很久没更新,只用来测试,不建议生产环境使用。


这里主要是介绍Linux下的安装,主要有几种方式:

  • Docker安装
  • Github源码编译安装
  • 直接yum install

1. CentOS 安装 Redis

安装步骤:

A. 安装编译器

yum install -y gcc-c++

B. 官网下载Redis:https://redis.io/download

wget https://download.redis.io/releases/redis-6.0.8.tar.gz

也可去官网下载最新的稳定版本。


C. 解压

tar -zxvf redis-6.0.8.tar.gz

D. 编译安装到指定安装目录

cd redis-6.0.8 && make && make PREFIX=/usr/local/redis install

安装目录可自定义,我这里是/usr/local/redis


E. 复制redis.conf到安装目录

cp redis.conf /usr/local/redis

F. 修改redis.conf

vim /usr/local/redis/redis.conf

# 修改daemonize为yes,即可以后台模板运行
daemonize yes

注意:如需要更改redis的端口也可以在这里面改,默认端口是6379。

注意:如需要设置密码也同样在这里改,默认是无密码的。


G. 启动服务端

cd /usr/local/redis && ./bin/redis-server ./redis.conf

可以通过ps -ef | grep redis来查看是否启动,默认端口是6379


H. 终止

cd /usr/local/redis && ./bin/redis-cli shutdown

也可以通过kill - 9 进程号来终止,但是不建议。


I. 连接Redis

redis-cli

即可进入Redis客户端


补充:

一般redis服务器不会直接通过./redis-server来启动,一般是会通过systemd来做成守护进程进行启动关闭等。


systemd添加redis服务

# vi /etc/systemd/system/redis.service

[Unit]
Description=redis-server
After=network.target

[Service]
Type=forking
ExecStart=/usr/local/redis/bin/redis-server /usr/local/redis/bin/redis.conf
PrivateTmp=true

[Install]
WantedBy=multi-user.target

注意:ExecStart需要配置为redis的路径


设置开机启动以及启动redsi-serser

systemctl daemon-reload
systemctl start redis.service
systemctl enable redis.service

创建软连接【非必要,把redis-cli简化为redis】

ln -s /usr/local/redis/bin/redis-cli /usr/bin/redis

服务操作命令

systemctl start redis.service   #启动redis服务
systemctl stop redis.service   #停止redis服务
systemctl restart redis.service   #重新启动服务
systemctl status redis.service   #查看服务当前状态
systemctl enable redis.service   #设置开机自启动
systemctl disable redis.service   #停止开机自启动

4-1 Redis的数据类型

1. Redis的数据类型
  • 字符串(String)
  • 哈希(hash)
  • 列表(list)
  • 集合(set)
  • 有序集合(sorted set)
  • 基数(HyperLogLog)
  • 位图(Bitmaps)
  • 流(Streams)
  • 地理坐标(Geospatial)

注意:很多教程只介绍了前5种数据类型,但官网是有提及支持的数据类型是9种。

但其实呢,HyperLogLog底层是String实现,相当于是对String数据类型封装的应用程序,而Bitmap底层也是String实现,赋值的每一个bit均对应ASCII码的二进制位。Geospatial是基于有序集合实现的。Streams是Redis5.0引入的一个新的数据类型,支持消费者组,借鉴Kafka设计的支持多播的可持久化消息队列(支持group,不支持partition)。


2. 规范
  • key值不要太长也不能太短,应该有一个统一的命名规范,一般来说不使用特殊字符,使用冒号或下划线进行连接
  • key必须有合理的过期时间
  • value值不要过大,不要超过100M

3. 内存管理

Redis的所有数据结构都是以唯一的key字符串作为名称,然后通过这个唯一key值来获取相应的value数据,不同类型的数据结构的差异就在于value的结构不一样。如下图:

image-20210424204749319

Redis内部使用一个redisObject对象来表示所有的key和value。

image-20210306143459994

redisObject 最主要的信息如上图所示:

  • type 表示一个 value 对象具体是何种数据类型
  • encoding 是不同数据类型在 Redis 内部的存储方式

比如:type=string 表示 value 存储的是一个普通字符串,那么 encoding 可以是 raw 或者 int。


4. 简单介绍

image-20210306193953410

image-20210424212508389


4-2 Redis的数据类型之字符串

1. 字符串
  • 字符串(String)是最基本的类型,可以理解成与Memcached一样的类型,一个Key对应一个Value,Value不仅是String,也可以是数字
  • 字符串类型是二进制安全的,存入和获取的数据相同,可以包含任何数据,比如jpb图片或序列化对象
  • Value最多可以容纳的数据长度是512M
  • 如果value是一个整数,可以进行自增自减操作,但value的整数范围是在signed long的最大值和最小值之间,超过这个范围会报错

2. 底层实现

内部是一个字符数组,如图:

image-20210406232555112

Redis的字符串是动态字符串,内部结构的实现类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。如图:

image-20210406232913218

内部为当前字符串分配的实际空间capacity一般要高于实际字符串长度len 。

当字符串长度小于1MB 肘,扩容都是加倍现有的空间。如果字符串长度超过1MB ,扩容时一次只会多扩1MB 的空间。

需要注意的是字符串最大长度为512MB 。


底层是C语言中String用char[]数组表示,源码中用SDS(simple dynamic string)封装char[],这是是Redis存储的最小单元,一个SDS最大可以存储512M信息。

struct sdshdr{
   
  unsigned int len; // 标记char[]的长度
  unsigned int free; //标记char[]中未使用的元素个数
  char buf[]; // 存放元素的坑
}

Redis对SDS再次封装生成了RedisObject,核心有两个作用:

  • 说明是哪种数据类型,string、hash、list、set或者是sorted set
  • 里面有指针用来指向SDS

比如当执行set name aaa的时候,其实Redis会创建两个RedisObject对象,键的RedisObject 和 值的RedisOjbect 其中它们type = REDIS_STRING,而SDS分别存储的就是 name 跟 aaa字符串。


并且Redis底层对SDS有如下优化:

  1. SDS修改后大小 > 1M时 系统会多分配空间来进行空间预分配
  2. SDS是惰性释放空间的,你free了空间,可是系统把数据记录下来下次想用时候可直接使用。不用新申请空间。

3. 命令
# 赋值,注意:Redis 2.6.12版本才有NX、XX的可选项
set 属性名 字符串值 [ex 时间] [NX|XX]

# 如属性名没有值才进行设置,如已经有值则设置失败返回nil
set 属性名 字符串值 NX

# 获取值,如不存在返回nil
get 属性名

# 获取属性的ttl时间
ttl 属性名

# 给属性设置过期时间
expire 属性名 秒数

# 先获取值再设置,如不存在返回nil且进行设置,注意:获取的是属性名之前的值
getset 属性名 新字符串值

# 删除值,成功返回1,失败返回9
del 属性名

# 值递增加1,如不存在会默认为0然后加1,如属性名存在但值不为整型的话会报错
incr 属性名

# 值递减1,如不存在会默认为0然后减1,如属性名存在但值不为整型的话会报错
decr 属性名

# 值递增n,用法类似于incr,只不过n是可以自定义的
incrby 属性名 n

# 值递减n
decrby 属性名 n

# 值追加字符串n,如不存在则直接追加字符串n,注意是字符串
append 属性名 n

# 判断属性名是否存在
exists 属性名

# 在指定的 key 不存在时,为 key 设置指定的值,set if not exists,如存在则不做任何操作,该指令在高并发下经常使用,新版本被set 属性 字符串值 EX 时间 NX 替代
setnx 属性名 字符串值
# 等同于 set 属性名 字符串值 NX

# 设置多个
mset 属性名1 value1 [属性名2 value2 ...]

# 获取多个
mget 属性名1 [属性名2 ...]

比如:

127.0.0.1:6379> set aaa test
OK
127.0.0.1:6379> get aaa
"test"
127.0.0.1:6379> get bbb
(nil)
127.0.0.1:6379> set test test_value ex 20
OK
127.0.0.1:6379> ttl test
(integer) 17
127.0.0.1:6379> getset aaa new_test
"test"
127.0.0.1:6379> get aaa
"new_test"
127.0.0.1:6379> getset ccc test
(nil)
127.0.0.1:6379> get ccc
"test"
127.0.0.1:6379> del ccc
(integer) 1
127.0.0.1:6379> get ccc
(nil)
127.0.0.1:6379> incr num
(integer) 1
127.0.0.1:6379> get num
"1"
127.0.0.1:6379> incr num
(integer) 2
127.0.0.1:6379> get num
"2"
127.0.0.1:6379> incr aaa
(error) ERR value is not an integer or out of range
127.0.0.1:6379> decr num
(integer) 1
127.0.0.1:6379> decr num
(integer) 0
127.0.0.1:6379> get num
"0"
127.0.0.1:6379> decr new_num
(integer) -1
127.0.0.1:6379> get new_num
"-1"
127.0.0.1:6379> decr aaa
(error) ERR value is not an integer or out of range
127.0.0.1:6379> decrby num 6
(integer) -6
127.0.0.1:6379> get num
"-6"
127.0.0.1:6379> decrby num2 4
(integer) -4
127.0.0.1:6379> append num2 10
(integer) 4
127.0.0.1:6379> get num2
"-410"
127.0.0.1:6379> append hahha test
(integer) 4
127.0.0.1:6379> get hahha
"test"

4. 场景:缓存

在web服务中,使用MySQL作为数据库,Redis作为缓存。由于Redis具有支撑高并发的特性,通常能起到加速读写和降低后端压力的作用。web端的大多数请求都是从Redis中获取的数据,如果Redis中没有需要的数据,则会从MySQL中去获取,并将获取到的数据写入redis。

import redis

class Cache:
    def __init__(self, client=None):
        self.client = client if client else redis.Redis(decode_response=True)

    def set(self, key, value):
        """
        把需要被缓存的数据储存到键 key 里面,
        如果键 key 已经有值,那么使用新值去覆盖旧值。
        """
        self.client.set(key, value)

    def get(self, key):
        """
        获取储存在键 key 里面的缓存数据,
        如果数据不存在,那么返回 None 。
        """
        return self.client.get(key)

    def update(self, key, new_value):
        """
        对键 key 储存的缓存数据进行更新,
        并返回键 key 在被更新之前储存的缓存数据。
        如果键 key 之前并没有储存数据,
        那么返回 None 。
        """
        return self.client.getset(key, new_value)
    
    def is_exists(self, key):
        """
        检查给定的字段是否储存了缓存值,
        是的话返回 True ,否则的话返回 False 。
        """
        return self.client.exists(key)

    def size(self, key):
        """
        返回目前已缓存的值长度
        """
        return self.client.strlen(key)

    def delete(self, key):
        """
        删除指定字段储存的缓存值,
        删除成功时返回 True ,因为缓存值不存在而导致删除失败时返回 False 。
        """
        return self.client.del(key) == 1

5. 场景:计数器

计数器也是构建应用程序时必不可少的组件之一,比如网站的访客数量、用户执行某个操作的次数、某个视频的播放量、论坛帖子的回复数量等,记录这些信息都需要用到计数器。

Redis中有一个字符串相关的命令incr keyincr命令对值做自增操作,返回结果分为以下三种情况:

  • 值不是整数,返回错误
  • 值是整数,返回自增后的结果
  • key不存在,默认键为0,返回1

比如文章的阅读量,视频的播放量等等都会使用redis来计数,每播放一次,对应的播放量就会加1,同时将这些数据异步存储到数据库中达到持久化的目的。

在必要时,用户还可以通过调用getset方法来清零计数器并获得清零之前的旧值。

import redis

class Counter:

    def __init__(self, key, client=None):
        self.client = client if client else redis.Redis(decode_response=True)
        self.key = key

    def increase(self, n=1):
        """
        将计数器的值加上 n ,然后返回计数器当前的值。
        如果用户没有显式地指定 n ,那么将计数器的值加上一。
        """
        return self.client.incr(self.key, n)

    def decrease(self, n=1):
        """
        将计数器的值减去 n ,然后返回计数器当前的值。
        如果用户没有显式地指定 n ,那么将计数器的值减去一。
        """
        return self.client.decr(self.key, n)

    def get(self):
        """
        返回计数器当前的值。
        """
        # 尝试获取计数器当前的值
        value = self.client.get(self.key)
        # 如果计数器并不存在,那么返回 0 作为计数器的默认值
        if value is None:
            return 0
        else:
            # 因为 redis-py 的 get() 方法返回的是字符串值
            # 所以这里需要使用 int() 函数,将字符串格式的数字转换为真正的数字类型
            # 比如将 "10" 转换为 10
            return int(value)

    def reset(self):
        """
        清零计数器,并返回计数器在被清零之前的值。
        """
        old_value = self.client.getset(self.key, 0)
        # 如果计数器之前并不存在,那么返回 0 作为它的旧值
        if old_value is None:
            return 0
        else:
            # 跟 redis-py 的 get() 方法一样, getset() 方法返回的也是字符串值
            # 所以程序在将计数器的旧值返回给调用者之前,需要先将它转换成真正的数字
            return int(old_value)

6. 场景:共享session

在分布式系统中,用户的每次请求会访问到不同的服务器,这就会导致session不同步的问题,假如一个用来获取用户信息的请求落在A服务器上,获取到用户信息后存入session。下一个请求落在B服务器上,想要从session中获取用户信息就不能正常获取了,因为用户信息的session在服务器A上,为了解决这个问题,使用redis集中管理这些session,将session存入redis,使用的时候直接从redis中获取就可以了。


7. 场景:限速

为了保障系统的安全性和性能,并保证系统的重要资源不被滥用,应用程序经常会对用户的某些行为进行限制,比如:

  • 为了防止网站内容被忘了爬虫抓取,网站管理者通常会限制每个IP地址在固定时间段内能够访问的页面数量,比如1分钟之内最多只能访问30个页面,超过这一限制的用户将被要求进行身份验证,确认本人并非网络爬虫,或者是等到限制解除之后再进行访问。
  • 为了防止用户的账号遭到暴力破解,网上银行通常会对访客的密码试错次数进行限制,如果一个访客在尝试登录某个账号的过程中,连续好几次输入了错误的密码,那么这个账号将被冻结,只能等到第二天再尝试登录,有的银行还会向账号持有者的手机发送通知来汇报这一情况。
  • 限制输入密码的错误次数

实现这些限制机制的其中一种方法是使用限速器,它可以限制用户在指定时间段之内能够执行某项操作的次数。

限速器,可以使用Redis的字符串来进行实现,限速器程序会把操作的最大可执行次数存储在一个字符串键里面,然后在用户每次尝试执行被限制的操作之前,使用DECR命令将操作的可执行次数减1,最后通过检查可执行次数的值来判断是否执行该操作。

import redis

class Limiter:

    def __init__(self, key, client=None):
        
        
    def __init__(self, limiter_name, client=None):
        self.client = client if client else redis.Redis(decode_response=True)
        self.max_execute_times_key = limiter_name + '::max_execute_times'
        self.current_execute_times_key = limiter_name + '::current_execute_times'

    def set_max_execute_times(self, n):
        """
        设置操作的最大可执行次数。
        """
        self.client.set(self.max_execute_times_key, n)
        # 初始化操作的已执行次数为 0
        self.client.set(self.current_execute_times_key, 0)

    def get_max_execute_times(self):
        """
        返回操作的最大可执行次数。
        """
        return int(self.client.get(self.max_execute_times_key))

    def get_current_execute_times(self):
        """
        返回操作的当前已执行次数。
        """
        current_execute_times = int(self.client.get(self.current_execute_times_key))
        max_execute_times = self.get_max_execute_times()

        if current_execute_times > max_execute_times:
            # 当用户尝试执行操作的次数超过最大可执行次数时
            # current_execute_times 的值就会比 max_execute_times 的值更大
            # 为了将已执行次数的值保持在 
            # 0 <= current_execute_times <= max_execute_times 这一区间
            # 如果已执行次数已经超过最大可执行次数
            # 那么程序将返回最大可执行次数作为结果
            return max_execute_times
        else:
            # 否则的话,返回真正的当前已执行次数作为结果
            return current_execute_times

    def still_valid_to_execute(self):
        """
        检查是否可以继续执行被限制的操作,
        是的话返回 True ,不是的话返回 False 。
        """
        updated_current_execute_times = self.client.incr(self.current_execute_times_key)
        max_execute_times = self.get_max_execute_times()
        return (updated_current_execute_times <= max_execute_times)

    def remaining_execute_times(self):
        """
        返回操作的剩余可执行次数。
        """
        current_execute_times = self.get_current_execute_times()
        max_execute_times = self.get_max_execute_times()
        return max_execute_times - current_execute_times

    def reset_current_execute_times(self):
        """
        清零操作的已执行次数。
        """
        self.client.set(self.current_execute_times_key, 0)

8. 场景:分布式锁

分布式锁是一种同步机制,用于保证一项资源在任何时候只能被一个进程使用,如果有其它进程想要使用相同的资源,那么必须等待直到正在使用资源的进程放弃使用为止。

一个锁的实现通常由获取锁和释放锁这两种操作:

  • 获取锁一般是通过执行带有NX选项的set命令来实现的,且带有过期时间
  • 释放锁虽然可以通过delete方法来进行释放,但为了保证不错删别的进行的锁,一般是用lua脚本来进行释放锁

具体可参考:三种分布式锁的实现


9. 场景:ID生成器

在构建应用程序的时候,经常需要用到各式各样的ID,比如存储用户信息的程序需要创建一个新的用户ID等,ID通常会以数字形式出现,并且通过递增的方式来创建新的ID,Redis的字符串可以通过执行Incr命令来生成新的ID,并且可以通过set命令来保留数字之前的ID,从而避免用户为了得到某个指定的ID而生成大量无效ID。

import redis

class IdGenerator:

    def __init__(self, key, client=None):
        self.client = client if client else redis.Redis(decode_response=True)
        self.key = key

    def produce(self):
        """
        生成并返回下一个 ID 。
        """
        return self.client.incr(self.key)

    def reserve(self, n):
        """
        保留前 n 个 ID ,使得之后执行的 produce() 方法产生的 ID 都大于 n 。
        为了避免 produce() 方法产生重复 ID ,
        这个方法只能在 produce() 方法和 reserve() 方法都没有执行过的情况下使用。
        这个方法在 ID 被成功保留时返回 True ,
        在 produce() 方法或 reserve() 方法已经执行过而导致保留失败时返回 False 。
        """
        result = self.client.set(self.key, n, nx=True)
        return result is True

4-3 Redis的数据类型之哈希

1. 哈希
  • 哈希(Hash)是一个键值(key-value)的集合,是String key 和 String Value的map容器(比如:姓名、年龄),又可称之为字典、散列
  • 适合存储对象
  • 每一个Hash可以存储4294967295个键值对
  • 相比String操作消耗内存与CPU更小,且更节省空间
  • 过期功能不能使用在field上,只能作用于key上
  • hash在Redis集群架构下不适合大规模使用,hash的会分配槽位,集群中会导致数据过于集中,没办法分片

当hash移除了最后一个元素之后,该数据结构被自动删除,内存被回收。
hash结构也可以用来存储用户信息,与字符串需要一次性全部序列化整个对象不同,hash可以对用户结构中的每个字段单独存储,这样当我们需要获取用户信息时可以进行部分获取。

而以整个字符串的形式去保存用户信息的话, 就只能一次性全部读取,这样就会浪费网络流量。
hash也有缺点,hash结构的存储消耗要高于单个字符串。

两者对比如下:

image-20210411011340735

什么时候选择字符串什么时候使用哈希?对于这个问题,以下总结了一些选择的条件和方法:

  • 如果程序需要为每个数据项单独设置过期时间,那么使用字符串键。
  • 如果程序需要对数据项执行诸如SETRANGE、GETRANGE或者APPEND等操作,那么优先考虑使用字符串键。当然,用户也可以选择把数据存储在散列中,然后将类似SETRANGE、GETRANGE这样的操作交给客户端执行。
  • 如果程序需要存储的数据项比较多,并且希望尽可能地减少存储数据所需的内存,就应该优先考虑使用散列键。
  • 如果多个数据项在逻辑上属于同一组或者同一类,那么应该优先考虑使用散列键。

2. 底层实现

底层实现有两种数据结构:

  • 压缩列表(ziplist)
  • hash表

如果同时满足下面 2 个条件,就使用压缩列表,否则使用 hash 表:

  • 字典中每个 entry 的 key/value 都小于 64 字节
  • 字典中元素个数小于 512 个

3. 命令
# 存单个
hset 名称 键 值

# 存多个
hmset 名称 键1122 ...

# 取名称的单个键的值,如不存在则返回nil
hget 名称 键

# 取名称的多个键的值,如不存在则返回nil
hmget 名称 键12 ...

# 删除名称的某个键
hdel 名称 键12 ...

# 删除整个名称
del 名称

# 获取名称的所有键值,如不存在返回empty list or set
hgetall 名称

# 对名称的某个键递增n,如键非整数会报错
hincrby 名称 键 n

# 判断名称的某个键是否存在,存在返回1,不存在返回0
hexists 名称 键 

# 获取名称下的键值对数量
hlen 名称

# 获取名称下的所有键
hkeys 名称

# 获取名称下的所有值
hvals 名称

# 只在名称不存在的情况下才设置值,设置成功返回1,如已存在返回0
hsetnx 名称 键 值

比如:

127.0.0.1:6379> hset user01 username John           
(integer) 0                                         
127.0.0.1:6379> hset user01 age 23                  
(integer) 0                                         
127.0.0.1:6379> hmset user02 username Hello age 30  
OK                                                  
127.0.0.1:6379> hget user01 age                     
"23"                                                
127.0.0.1:6379> hmget user01 age username           
1) "23"                                             
2) "John"                                           
127.0.0.1:6379> hdel user02 age                     
(integer) 1                                         
127.0.0.1:6379> hgetall user02                      
1) "username"                                       
2) "Hello"                                          
127.0.0.1:6379> del user02                          
(integer) 1                                         
127.0.0.1:6379> hget user02 age                     
(nil)                                               
127.0.0.1:6379> hgetall user02                      
(empty list or set)                                 
127.0.0.1:6379> hincrby user01 age 5                
(integer) 28                                        
127.0.0.1:6379> hincrby user01 username 5           
(error) ERR hash value is not an integer            
127.0.0.1:6379> hexists user02 age                  
(integer) 0                                         
127.0.0.1:6379> hexists user01 age                  
(integer) 1                                         
127.0.0.1:6379> hlen user01                         
(integer) 2                                         
127.0.0.1:6379> hvals user01                        
1) "John"                                           
2) "28"                                             

4. 场景:缓存

和字符串实现的缓存很类似,最大的区别是字符串是处理的是字符串键,而哈希处理的是散列键。

import redis

class Cache:

    def __init__(self, hash, client=None):
        self.client = client if client else redis.Redis(decode_response=True)
        self.hash = hash

    def set(self, field, value):
        """
        将给定的值缓存到散列的指定字段中。
        """
        self.client.hset(self.hash, field, value)

    def get(self, field):
        """
        从散列的指定字段中获取被缓存的值,
        如果值不存在,那么返回 None 。
        """
        return self.client.hget(self.hash, field)

    def is_exists(self, field):
        """
        检查给定的字段是否储存了缓存值,
        是的话返回 True ,否则的话返回 False 。
        """
        return self.client.hexists(self.hash, field)

    def size(self):
        """
        返回散列目前已缓存的值数量。
        """
        return self.client.hlen(self.hash)

    def delete(self, field):
        """
        从散列中删除指定字段储存的缓存值,
        删除成功时返回 True ,因为缓存值不存在而导致删除失败时返回 False 。
        """
        return self.client.hdel(self.hash, field) == 1

5. 场景:短网址生成程序

Redis的哈希很适合用来存储短网址ID与目标网址之间的映射,所以可以基于Redis的哈希来实现短网址程序。

注意:里面的Cache指的是场景4中实现的Cache类。

import redis
from cache import Cache

ID_COUNTER = "ShortyUrl::id_counter"
URL_HASH = "ShortyUrl::url_hash" 
URL_CACHE = "ShortyUrl::url_cache"

class ShortyUrl:

    def __init__(self, client=None):
        self.client = client if client else redis.Redis(decode_response=True)
        self.cache = Cache(self.client, URL_CACHE)  # 创建缓存对象

    def shorten(self, target_url):
        """
        为目标网址创建一个短网址 ID 。
        """
        # 尝试在缓存里面寻找目标网址对应的短网址 ID
        cached_short_id = self.cache.get(target_url)
        if cached_short_id is not None:
            return cached_short_id

        new_id = self.client.incr(ID_COUNTER)
        short_id = self.base10_to_base36(new_id)
        self.client.hset(URL_HASH, short_id, target_url)
        # 在缓存里面关联起目标网址和短网址 ID
        # 这样程序就可以在用户下次输入相同的目标网址时
        # 直接重用已有的短网址 ID
        self.cache.set(target_url, short_id)
        return short_id

    def restore(self, short_id):
        """
        根据给定的短网址 ID ,返回与之对应的目标网址。
        """
        return self.client.hget(URL_HASH, short_id)
    
    def base10_to_base36(number):
        """
        将十进制数字转换为36进制数字
        """
    	alphabets = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    	result = ""

    	while number != 0 :
        	number, i = divmod(number, 36)
        	result = (alphabets[i] + result)

    	return result or alphabets[0]

6. 场景:实现用户登录会话

为了方便用户,网站一般都会为已登录的用户生成一个加密令牌,然后把这个令牌分别存储在服务器端和客户端,之后每当用户再次访问该网站的时候,网站就可以通过验证客户端提交的令牌来确认用户的身份,从而使得用户不必重复地执行登录操作。

另外,为了防止用户因为长时间不输入密码而遗忘密码,以及为了保证令牌的安全性,网站一般都会为令牌设置一个过期期限(比如一个月),当期限到达之后,用户的会话就会过时,而网站则会要求用户重新登录。

上面描述的这种使用令牌来避免重复登录的机制一般称为登录会话(login session),可以通过使用Redis的哈希来实现。

import redis
import random
from time import time  # 获取浮点数格式的 unix 时间戳
from hashlib import sha256

# 会话的默认过期时间
DEFAULT_TIMEOUT = 3600*24*30    # 一个月

# 储存会话令牌以及会话过期时间戳的散列
SESSION_TOKEN_HASH =
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值