admin 管理员组文章数量: 887018
文章目录
- Redis数据类型的详解与使用场景
-
- 1-1 NoSQL的概述
-
-
- 1. 概述
- 2. 为什么需要NoSQL
- 3. NoSQL产品
- 4. 分类
- 5. 特点
-
- 2-1 Redis的概述
-
-
- 1. 概述
- 2. 应用场景
- 3. Redis的特点
- 4. Redis为什么如此快
-
- A. 数据保存在内存中
- B. 底层数据结构
- C. 单线程模型
- D. IO多路复用
-
- 3-1 Redis的安装
-
-
- 1. CentOS 安装 Redis
-
- 4-1 Redis的数据类型
-
-
- 1. Redis的数据类型
- 2. 规范
- 3. 内存管理
- 4. 简单介绍
-
- 4-2 Redis的数据类型之字符串
-
-
- 1. 字符串
- 2. 底层实现
- 3. 命令
- 4. 场景:缓存
- 5. 场景:计数器
- 6. 场景:共享session
- 7. 场景:限速
- 8. 场景:分布式锁
- 9. 场景:ID生成器
-
- 4-3 Redis的数据类型之哈希
-
-
- 1. 哈希
- 2. 底层实现
- 3. 命令
- 4. 场景:缓存
- 5. 场景:短网址生成程序
- 6. 场景:实现用户登录会话
- 7. 场景:计数器
-
- 4-4 Redis的数据类型之列表
-
-
- 1. 概述
- 2. 底层实现
- 3. 命令
- 4. 场景:先进先出队列
- 5. 场景:消息队列
- 6. 场景:异步队列
- 7. 场景:栈
-
- 4-5 Redis的数据类型之集合
-
-
- 1. 概述
- 2. 底层实现
- 3. 命令
- 4. 使用场景
- 5. 场景:唯一计数器
- 6. 场景:点赞
- 7. 场景:投票
- 8. 场景:社交关系
- 9. 场景:抽奖
- 10. 场景:共同关注与推荐关注
- 11. 场景:商品筛选器
-
- 4-6 Redis的数据类型之有序集合
-
-
- 1. 概述
- 2. 底层实现
- 3. 命令
- 4. 场景:排行榜
- 5. 场景:时间线
- 6. 场景:商品推荐
- 7. 场景:延时队列
- 8. 其他场景
-
- 4-7 Redis的数据类型之HyperLogLog
-
-
- 1. 概述
- 2. 底层实现
- 3. 命令
- 4. 场景:优化唯一计数器
- 5. 场景:检测重复信息
- 6. 场景:每周/月度/年度计数器
-
- 4-8 Redis的数据类型之位图
-
-
- 1. 概述
- 2. 命令
- 3. 场景:用户行为记录器
- 4. 场景:0-1矩阵
- 5. 场景:紧凑计数器
- 6. 其他场景
-
- 4-9 Redis的数据类型之地理坐标
-
-
- 1. 概述
- 2. 命令
- 3. 场景:用户地理位置程序
- 4. 场景:查找附近用户
-
- 4-10 Redis的数据类型之流
-
-
- 1. 概述
- 2. 底层实现
- 3. 命令
- 4. 场景:消息队列
- 5. 消费者组
-
- 创建消费者组
- 查看消费者组信息
- 修改消费者组的消息id
- 删除消费者组/消费者
- 读取消费者组的消息
- 消费者
- 6. 场景:消息队列(消费者组)
-
- 5-1 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提供了很多非常实用的附加功能,比如自动过期、流水线、事务、数据持久化等。
- 可以用作分布式锁。
- 可以作为消息中间件使用,支持发布订阅。
如图:
4. Redis为什么如此快
A. 数据保存在内存中
Redis数据保存在内存中,读写操作只需要访问内存,不需要磁盘IO
B. 底层数据结构
Redis的数据是以key:value的格式存储在散列表中的,时间复杂度为:o(1)
Redis为value定义了丰富的数据结构,包括动态字符串、双向链表、压缩列表、hash、跳表和整数数组,可以根据value的特性选择最高效的数据结构
C. 单线程模型
Redis的网络IO和数据读写使用单线程模型,可以绑定CPU,这避免了线程上下文切换带来的开销。
注意:Redis 6.0对网络请求引入了多线程模型,但读写操作还是单线程。
如下图:
D. IO多路复用
Redis采用epoll的网络模型,如下图:
内核会一直监听新的 socket 连接事件的和已建立 socket 连接的读写事件,把监听到的事件放到事件队列,Redis 使用单线程不停的处理这个事件队列,这避免了阻塞等待连接和读写事件到来。
这些事件绑定了回调函数,会调用 Redis 的处理函数进行处理。
3-1 Redis的安装
Redis官方并不支持Windows系统,所以Windows下可通过虚拟机或Docker等手段进行安装,但如果是只用来测试的话,也可以通过下载msi文件进行下载安装,下载地址为:
https://github/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的结构不一样。如下图:
Redis内部使用一个redisObject对象来表示所有的key和value。
redisObject 最主要的信息如上图所示:
- type 表示一个 value 对象具体是何种数据类型
- encoding 是不同数据类型在 Redis 内部的存储方式
比如:type=string 表示 value 存储的是一个普通字符串,那么 encoding 可以是 raw 或者 int。
4. 简单介绍
4-2 Redis的数据类型之字符串
1. 字符串
- 字符串(String)是最基本的类型,可以理解成与Memcached一样的类型,一个Key对应一个Value,Value不仅是String,也可以是数字
- 字符串类型是二进制安全的,存入和获取的数据相同,可以包含任何数据,比如jpb图片或序列化对象
- Value最多可以容纳的数据长度是512M
- 如果value是一个整数,可以进行自增自减操作,但value的整数范围是在signed long的最大值和最小值之间,超过这个范围会报错
2. 底层实现
内部是一个字符数组,如图:
Redis的字符串是动态字符串,内部结构的实现类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。如图:
内部为当前字符串分配的实际空间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有如下优化:
- SDS修改后大小 > 1M时 系统会多分配空间来进行
空间预分配
。- 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 key
,incr
命令对值做自增操作,返回结果分为以下三种情况:
- 值不是整数,返回错误
- 值是整数,返回自增后的结果
- 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结构的存储消耗要高于单个字符串。
两者对比如下:
什么时候选择字符串什么时候使用哈希?对于这个问题,以下总结了一些选择的条件和方法:
- 如果程序需要为每个数据项单独设置过期时间,那么使用字符串键。
- 如果程序需要对数据项执行诸如SETRANGE、GETRANGE或者APPEND等操作,那么优先考虑使用字符串键。当然,用户也可以选择把数据存储在散列中,然后将类似SETRANGE、GETRANGE这样的操作交给客户端执行。
- 如果程序需要存储的数据项比较多,并且希望尽可能地减少存储数据所需的内存,就应该优先考虑使用散列键。
- 如果多个数据项在逻辑上属于同一组或者同一类,那么应该优先考虑使用散列键。
2. 底层实现
底层实现有两种数据结构:
- 压缩列表(ziplist)
- hash表
如果同时满足下面 2 个条件,就使用压缩列表,否则使用 hash 表:
- 字典中每个 entry 的 key/value 都小于 64 字节
- 字典中元素个数小于 512 个
3. 命令
# 存单个
hset 名称 键 值
# 存多个
hmset 名称 键1 值1 键2 值2 ...
# 取名称的单个键的值,如不存在则返回nil
hget 名称 键
# 取名称的多个键的值,如不存在则返回nil
hmget 名称 键1 键2 ...
# 删除名称的某个键
hdel 名称 键1 键2 ...
# 删除整个名称
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 =
版权声明:本文标题:【Redis】数据类型的详解与使用场景【原创】 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/jishu/1726362520h944974.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论