一、引言
在Web应用发展的初期,那时关系型数据库受到了较为广泛的关注和应用,原因是因为那时候Web站点基本上访问和并发不高、交互也较少。而在后来,随着访问量的提升,使用关系型数据库的Web站点多多少少都开始在性能上出现了一些瓶颈,而瓶颈的源头一般是在磁盘的I/O上。而随着互联网技术的进一步发展,各种类型的应用层出不穷,这导致在当今云计算、大数据盛行的时代,对性能有了更多的需求,主要体现在以下四个方面:
- 低延迟的读写速度:应用快速地反应能极大地提升用户的满意度
- 支撑海量的数据和流量:对于搜索这样大型应用而言,需要利用PB级别的数据和能应对百万级的流量
- 大规模集群的管理:系统管理员希望分布式应用能更简单的部署和管理
- 庞大运营成本的考量:IT部门希望在硬件成本、软件成本和人力成本能够有大幅度地降低
为了克服这一问题,NoSQL应运而生,它同时具备了高性能、可扩展性强、高可用等优点,受到广泛开发人员和仓库管理人员的青睐。
关系型数据库(RMDBS)与非关系型数据库(NoSQL)的对比:
数据库中表与表的数据之间存在某种关联的内在关系,因为这种关系,所以我们称这种数据库为关系型数据库。典型:Mysql/MariaDB、postgreSQL、Oracle、SQLServer、DB2、Access、SQLlite3。特点:
- 全部使用SQL(结构化查询语言)进行数据库操作。
- 都存在主外键关系,表,等等关系特征。
- 大部分都支持各种关系型的数据库的特性:事务、存储过程、触发器、视图、临时表、模式、函数
NOSQL:not only sql,泛指非关系型数据库。泛指那些不使用SQL语句进行数据操作的数据库,所有数据库中只要不使用SQL语句的都是非关系型数据库。典型:Redis、MongoDB、hbase、 Hadoop、elasticsearch、图数据库(Neo4j、GraphDB、SequoiaDB)。。。。
特点:
- 每一款都不一样。用途不一致,功能不一致,各有各的操作方式。
- 基本不支持主外键关系,也没有事务的概念。(MongoDB号称最接近关系型数据库的,所以MongoDB有这些的。)
二、redis介绍
2.1、定义
Redis(Remote Dictionary Server ,远程字典服务) 是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库,是NoSQL数据库。
redis的出现主要是为了替代早期的Memcache缓存系统的。map内存型(数据存放在内存中)的非关系型(nosql)key-value(键值存储)数据库,
支持数据的持久化(基于RDB和AOF,注: 数据持久化时将数据存放到文件中,每次启动redis之后会先将文件中数据加载到内存,经常用来做缓存、数据共享、购物车、消息队列、计数器、限流等。(最基本的就是缓存一些经常用到的数据,提高读写速度)。
redis特性:
- 速度快
- 持久化
- 多种数据结构
- 支持多种编程语言
- 功能丰富
- 简单:代码短小精悍
- 主从复制
- 高可用、分布式
redis的官方只提供了linux版本的redis,window系统的redis是微软团队根据官方的linux版本高仿的。
官方原版: https://redis.io/
中文官网:http://www.redis.cn
2.2、Redis的应用场景有哪些?
Redis 的应用场景包括:缓存系统(“热点”数据:高频读、低频写)、计数器、消息队列系统、排行榜、社交网络和实时系统。
2.3、Redis的数据类型及主要特性
Redis提供的数据类型主要分为5种自有类型和一种自定义类型,这5种自有类型包括:String类型、哈希类型、列表类型、集合类型和顺序集合类型。
三、redis环境安装
3.1、下载和安装
下载地址:https://github.com/tporadowski/redis/releases
使用以下命令启动redis服务端
redis-server C:/tool/redis/redis.windows.conf
关闭上面这个cmd窗口就关闭redis服务器服务了。
redis作为windows服务启动方式
1
|
redis-server --service-install redis.windows.conf
|
启动服务:redis-server –service-start
停止服务:redis-server –service-stop
1
2
|
# 如果连接操作redis,可以在终端下,使用以下命令:
redis-cli
|
ubuntu下安装:
1
2
3
4
5
6
|
安装命令:sudo apt-get install -y redis-server
卸载命令:sudo apt-get purge --auto-remove redis-server
关闭命令:sudo service redis-server stop
开启命令:sudo service redis-server start
重启命令:sudo service redis-server restart
配置文件:/etc/redis/redis.conf
|
3.2、redis的配置
1
|
cat /etc/redis/redis.conf
|
redis 安装成功以后,window下的配置文件保存在软件 安装目录下,如果是mac或者linux,则默认安装/etc/redis/redis.conf
redis的核心配置选项
绑定ip:访问白名单,如果需要远程访问,可将此注释,或绑定1个真实ip
1
|
bind 127.0.0.1 xx.xx.xx.xx
|
端⼝,默认为6379
是否以守护进程运行
- 如果以守护进程运行,则不会在命令阻塞,类似于服务
- 如果以守护进程运行,则当前终端被阻塞
- 设置为yes表示守护进程,设置为no表示⾮守护进程
- 推荐设置为yes
RDB持久化的备份策略(RDB备份是默认开启的)
1
2
3
4
|
# save 时间 读写次数
save 900 1 # 当redis在900内至少有1次读写操作,则触发一次数据库的备份操作
save 300 10 # 当redis在300内至少有10次读写操作,则触发一次数据库的备份操作
save 60 10000 # 当redis在60内至少有10000次读写操作,则触发一次数据库的备份操作
|
RDB持久化的备份文件
RDB持久化数据库数据文件的所在目录
日志文件所载目录
1
2
|
loglevel notice
logfile /var/log/redis/redis-server.log
|
进程ID文件
1
|
pidfile /var/run/redis/redis-server.pid
|
数据库,默认有16个,数据名是不能自定义的,只能是0-15之间,当然这个15是数据库数量-1
redis的登录密码,生产阶段打开,开发阶段避免麻烦,一般都是注释的。redis在6.0版本以后新增了ACL访问控制机制,新增了用户管理,这个版本以后才有账号和密码,再次之前只有没有密码没有账号
注意:开启了以后,redis-cli终端下使用 auth 密码
来认证登录。
AOF持久化的开启配置项(默认值是no,关闭状态)
AOF持久化的备份文件(AOF的备份数据文件与RDB的备份数据文件保存在同一个目录下,由dir配置项指定)
1
|
appendfilename "appendonly.aof"
|
AOF持久化备份策略[时间]
1
2
3
|
# appendfsync always
appendfsync everysec # 工作中最常用。每一秒备份一次
# appendfsync no
|
哨兵集群:一主二从三哨兵(3台服务器)
Redis的使用
redis是一款基于CS架构的数据库,所以redis有客户端redis-cli,也有服务端redis-server。
其中,客户端可以使用go、java、python等编程语言,也可以终端下使用命令行工具管理redis数据库,甚至可以安装一些别人开发的界面工具,例如:RDM。
redis-cli客户端连接服务器:
1
2
|
# redis-cli -h `redis服务器ip` -p `redis服务器port`
redis-cli -h 10.16.244.3 -p 6379
|
四、redis数据类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
redis可以理解成一个全局的大字典,key就是数据的唯一标识符。根据key对应的值不同,可以划分成5个基本数据类型。
1. string类型:
字符串类型,是 Redis 中最为基础的数据存储类型,它在 Redis 中是二进制安全的,也就是byte类型。
单个数据的最大容量是512M。
key: 值
2. hash类型:
哈希类型,用于存储对象/字典,对象/字典的结构为键值对。key、域、值的类型都为string。域在同一个hash中是唯一的。
key:{
域(属性): 值,
域:值,
域:值,
域:值,
...
}
3. list类型:
列表类型,它的子成员类型为string。
key: [值1,值2, 值3.....]
4. set类型:
无序集合,它的子成员类型为string类型,元素唯一不重复,没有修改操作。
key: {值1, 值4, 值3, ...., 值5}
5. zset类型(sortedSet):
有序集合,它的子成员值的类型为string类型,元素唯一不重复,没有修改操作。权重值(score,分数)从小到大排列。
key: {
值1 权重值1(数字);
值2 权重值2;
值3 权重值3;
值4 权重值4;
}
|
针对各种数据类型它们的特性,使用场景如下:
字符串string: 用于保存项目中普通数据,只要键值对都可以保存,例如,保存 session/jwt,定时记录状态,倒计时、验证码、防灌水答案
哈希hash:用于保存项目中的一些结构体/map类型数据,但是不能保存多维结构,例如,商城的购物车,文章信息,json结构数据
列表list:用于保存项目中的列表/切片数据,但是也不能保存多维结构,例如,消息队列,秒杀系统,排队,
无序集合set: 用于保存项目中的一些不能重复的数据,可以用于过滤,例如,候选人名单, 作者名单,
有序集合zset:用于保存项目中一些不能重复,但是需要进行排序的数据,例如:分数排行榜, 海选人排行榜,热搜排行,
4.1. string(字符串)
- GET/MGET
- SET/SETEX/MSET/MSETNX
- INCR/DECR
- GETSET
- DEL
1. 设置键值
set 设置的数据没有额外操作时,是不会过期的。
设置键为name
值为yuan
的数据
1
2
|
set name yuan
set name rain # 一个变量可以设置多次
|
注意:redis中的所有数据操作,如果设置的键不存在则为添加,如果设置的键已经存在则修改。
设置一个键,当键不存在时才能设置成功,用于一个变量只能被设置一次的情况。
一般用于给数据加锁(分布式锁)
1
2
3
4
5
6
7
8
9
|
127.0.0.1:6379> setnx goods_1 101
(integer) 1
127.0.0.1:6379> setnx goods_1 102
(integer) 0 # 表示设置不成功
127.0.0.1:6379> del goods_1
(integer) 1
127.0.0.1:6379> setnx goods_1 102
(integer) 1
|
2. 设置键值的过期时间
redis中可以对一切的数据进行设置有效期。以秒为单位
1
|
setex key seconds value
|
设置键为goods_1
值为101
过期时间为10秒的数据
3. 关于设置保存数据的有效期
setex 添加保存数据到redis,同时设置有效期,格式:
4. 设置多个键值
1
|
mset key1 value1 key2 value2 ...
|
例3:设置键为a1
值为goland
、键为a2
值为java
、键为a3
值为c
1
|
mset a1 goland a2 java a3 c
|
5. 字符串拼接值
常见于大文件上传
向键为a1
中拼接值haha
1
2
3
|
set title "我的"
append title "redis"
append title "学习之路"
|
6. 根据键获取值
根据键获取值,如果不存在此键则返回nil
获取键name
的值
根据多个键获取多个值
获取键a1、a2、a3
的值
getset:设置key的新值,返回旧值
1
2
3
4
5
6
7
8
9
10
|
redis> GETSET db mongodb # 没有旧值,返回 nil
(nil)
redis> GET db
"mongodb"
redis> GETSET db redis # 返回旧值 mongodb
"mongodb"
redis> GET db
"redis"
|
7. 自增自减
web开发中的电商抢购、秒杀。游戏里面的投票、攻击计数。系统中计算当前在线人数、
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
set id 1
incr id # 相当于id+1
get id # 2
incr id # 相当于id+1
get id # 3
set goods_id_1 10
decr goods_id_1 # 相当于 id-1
get goods_id_1 # "9"
decr goods_id_1 # 相当于id-1
get goods_id_1 # 8
set age 22
incrby age 2 # 自增自减大于1的值时候用incrby
|
8. 获取字符串的长度
1
2
|
set name xiaoming
strlen name # 8
|
9. 比特流操作
1字节=8比特 1kb = 1024字节 1mb = 1024kb 1gb = 1024mb
1个int8就是一个字节,一个中文:3个字节
签到记录
1
2
3
4
|
SETBIT # 设置一个bit数据的值
GETBIT # 获取一个bit数据的值
BITCOUNT # 统计字符串被设置为1的bit数.
BITPOS # 返回字符串里面第一个被设置为1或者0的bit位。
|
终端操作:
1
2
3
4
5
6
7
8
9
10
11
12
|
SETBIT mykey 7 1
# 00000001
getbit mykey 7
# 00000001
SETBIT mykey 4 1
# 00001001
SETBIT mykey 15 1
# 0000100100000001
BITCOUNT mykey
# 3
BITPOS mykey 1
# 4
|
4.2. key操作
redis中所有的数据都是通过key(键)来进行操作,这里我们学习一下关于任何数据类型都通用的命令。
(1)查找键
参数支持简单的正则表达式
查看所有键
例子:
1
2
3
4
5
6
|
# 查看名称中包含`a`的键
keys *a*
# 查看以a开头的键
keys a*
# 查看以a结尾的键
keys *a
|
(2)判断键是否存在
如果存在返回1
,不存在返回0
判断键title
是否存在
(3)查看键的的值的数据类型
1
2
3
4
5
6
7
|
type key
# string 字符串
# hash 哈希类型
# list 列表类型
# set 无序集合
# zset 有序集合
|
查看键的值类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
type a1
# string
sadd member_list xiaoming xiaohong xiaobai
# (integer) 3
type member_list
# set
hset user_1 name xiaobai age 17 sex 1
# (integer) 3
type user_1
# hash
lpush brothers zhangfei guangyu liubei xiaohei
# (integer) 4
type brothers
# list
zadd achievements 61 xiaoming 62 xiaohong 83 xiaobai 78 xiaohei 87 xiaohui 99 xiaolong
# (integer) 6
type achievements
# zset
|
(4)删除键以及键对应的值
(5)查看键的有效期
1
2
3
4
5
|
ttl key
# 结果结果是秒作为单位的整数
# -1 表示永不过期
# -2 表示当前数据已经过期,查看一个不存在的数据的有效期就是-2
|
(6)设置key的有效期
给已有的数据重新设置有效期,redis中所有的数据都可以通过expire来设置它的有效期。有效期到了,数据就被删除。
(7)清空所有key
慎用,一旦执行,则redis所有数据库0~15的全部key都会被清除
(8)key重命名
把name重命名为username
1
2
3
|
set name yuan
rename name username
get username
|
select切换数据库
1
2
|
redis的配置文件中,默认有0~15之间的16个数据库,默认操作的就是0号数据库
select <数据库ID>
|
操作效果:
1
2
3
4
5
6
7
8
9
10
11
|
# 默认处于0号库
127.0.0.1:6379> select 1
OK
# 这是在1号库
127.0.0.1:6379[1]> set name xiaoming
OK
127.0.0.1:6379[1]> select 2
OK
# 这是在2号库
127.0.0.1:6379[2]> set name xiaohei
OK
|
4.3. list(数组)
队列,列表的子成员类型为string
(1)添加子成员
1
2
3
4
5
6
7
8
9
|
# 在左侧(前)添加一条或多条数据
lpush key value1 value2 ...
# 在右侧(后)添加一条或多条数据
rpush key value1 value2 ...
# 在指定元素的左边(前)/右边(后)插入一个或多个数据
linsert key before 指定元素 value1 value2 ....
linsert key after 指定元素 value1 value2 ....
|
从键为brother
的列表左侧添加一个或多个数据liubei、guanyu、zhangfei
1
2
3
4
|
lpush brother liubei
# [liubei]
lpush brother guanyu zhangfei xiaoming
# [xiaoming,zhangfei,guanyu,liubei]
|
从键为brother的列表右侧添加一个或多个数据,xiaohong,xiaobai,xiaohui
1
2
3
4
|
rpush brother xiaohong
# [xiaoming,zhangfei,guanyu,liubei,xiaohong]
rpush brother xiaobai xiaohui
# [xiaoming,zhangfei,guanyu,liubei,xiaohong,xiaobai,xiaohui]
|
从key=brother,key=xiaohong的列表位置左侧添加一个数据,xiaoA,xiaoB
1
2
3
4
|
linsert brother before xiaohong xiaoA
# [xiaoming,zhangfei,guanyu,liubei,xiaoA,xiaohong,xiaobai,xiaohui]
linsert brother before xiaohong xiaoB
# [xiaoming,zhangfei,guanyu,liubei,xiaoA,xiaoB,xiaohong,xiaobai,xiaohui]
|
从key=brother,key=xiaohong的列表位置右侧添加一个数据,xiaoC,xiaoD
1
2
3
4
|
linsert brother after xiaohong xiaoC
# [xiaoming,zhangfei,guanyu,liubei,xiaoA,xiaohong,xiaoC,xiaobai,xiaohui]
linsert brother after xiaohong xiaoD
# [xiaoming,zhangfei,guanyu,liubei,xiaoA,xiaohong,xiaoD,xiaoC,xiaobai,xiaohui]
|
注意:当列表如果存在多个成员值一致的情况下,默认只识别第一个。
1
2
3
4
5
6
|
127.0.0.1:6379> linsert brother before xiaoA xiaohong
# [xiaoming,zhangfei,guanyu,liubei,xiaohong,xiaoA,xiaohong,xiaoD,xiaoC,xiaobai,xiaohui]
127.0.0.1:6379> linsert brother before xiaohong xiaoE
# [xiaoming,zhangfei,guanyu,liubei,xiaoE,xiaohong,xiaoA,xiaohong,xiaoD,xiaoC,xiaobai,xiaohui]
127.0.0.1:6379> linsert brother after xiaohong xiaoF
# [xiaoming,zhangfei,guanyu,liubei,xiaoE,xiaohong,xiaoF,xiaoA,xiaohong,xiaoD,xiaoC,xiaobai,xiaohui]
|
(2)获取列表成员
根据指定的索引(下标)获取成员的值,负数下标从右边-1开始,逐个递减
获取brother下标为2以及-2的成员
1
2
3
4
5
6
|
del brother
lpush brother guanyu zhangfei xiaoming
lindex brother 2
# "guanyu"
lindex brother -2
# "zhangfei"
|
移除并获取列表的第一个成员或最后一个成员
1
2
|
lpop key # 第一个成员出列
rpop key # 最后一个成员出列
|
获取并移除brother中的第一个成员
1
2
|
lpop brother
# 开发中往往使用rpush和lpop实现队列的数据结构->实现入列和出列
|
(3)获取列表的切片
闭区间[包括stop]
操作:
1
2
3
4
5
6
7
8
|
del brother
rpush brother liubei guanyu zhangfei xiaoming xaiohong
# 获取btother的全部成员
lrange brother 0 -1
# 获取brother的前2个成员
lrange brother 0 1
# 获取brother的后2个成员
lrange brother -2 -1
|
(4)获取列表的长度
获取brother列表的成员个数
(5)按索引设置值
1
2
3
4
|
lset key index value
# 注意:
# redis的列表也有索引,从左往右,从0开始,逐一递增,第1个元素下标为0
# 索引可以是负数,表示尾部开始计数,如`-1`表示最后1个元素
|
修改键为brother
的列表中下标为4
的元素值为xiaohongmao
1
|
lset brother 4 xiaohonghong
|
(6)删除指定成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
lrem key count value
# 注意:
# count表示删除的数量,value表示要删除的成员。该命令默认表示将列表从左侧前count个value的元素移除
# count==0,表示删除列表所有值为value的成员
# count >0,表示删除列表左侧开始的前count个value成员
# count <0,表示删除列表右侧开始的前count个value成员
del brother
rpush brother A B A C A
lrem brother 0 A
["B","C"]
del brother
rpush brother A B A C A
lrem brother -2 A
["A","B","C"]
del brother
rpush brother A B A C A
lrem brother 2 A
["B","C","A"]
|
4.4. hash(哈希)
专门用于结构化的数据信息。对应的就是map/结构体
结构:
1
2
3
4
5
|
键key:{
域field: 值value,
域field: 值value,
域field: 值value,
}
|
(1)设置指定键的属性/域
设置指定键的单个属性
设置键 user_1
的属性name
为xiaoming
1
2
3
4
5
6
7
8
9
10
11
|
127.0.0.1:6379> hset user_1 name xiaoming # user_1没有会自动创建
(integer) 1
127.0.0.1:6379> hset user_1 name xiaohei # user_1中重复的属性会被修改
(integer) 0
127.0.0.1:6379> hset user_1 age 16 # user_1中不存在的属性会被新增
(integer) 1
127.0.0.1:6379> hset user:1 name xiaohui # user:1会在redis界面操作中以:作为目录分隔符
(integer) 1
127.0.0.1:6379> hset user:1 age 15
(integer) 1
127.0.0.1:6379> hset user:2 name xiaohong age 16 # 一次性添加或修改多个属性
|
(2)获取指定键的域/属性的值
获取指定键所有的域/属性
获取键user的所有域/属性
1
2
3
4
5
6
7
|
127.0.0.1:6379> hkeys user:2
1) "name"
2) "age"
127.0.0.1:6379> hkeys user:3
1) "name"
2) "age"
3) "sex"
|
获取指定键的单个域/属性的值
hget key field
获取键user:3
属性name
的值
1
2
|
127.0.0.1:6379> hget user:3 name
"xiaohong"
|
获取指定键的多个域/属性的值
1
|
hmget key field1 field2 ...
|
获取键user:2
属性name
、age
的值
1
2
3
|
127.0.0.1:6379> hmget user:2 name age
1) "xiaohong"
2) "16"
|
获取指定键的所有值
获取指定键的所有域值对
1
2
3
4
|
127.0.0.1:6379> hvals user:3
1) "xiaohong"
2) "17"
3) "1"
|
(3)获取hash的所有域值对
1
2
3
4
5
6
7
8
9
|
127.0.0.1:6379> hset user:1 name xiaoming age 16 sex 1
(integer) 3
127.0.0.1:6379> hgetall user:1
1) "name"
2) "xiaoming"
3) "age"
4) "16"
5) "sex"
6) "1"
|
(4)删除指定键的域/属性
1
|
hdel key field1 field2 ...
|
删除键user:3
的属性sex/age/name
,当键中的hash数据没有任何属性,则当前键会被redis删除
1
|
hdel user:3 sex age name
|
(5)判断指定属性/域是否存在于当前键对应的hash中
判断user:2中是否存在age属性
1
2
3
4
5
|
127.0.0.1:6379> hexists user:3 age
(integer) 0
127.0.0.1:6379> hexists user:2 age
(integer) 1
127.0.0.1:6379>
|
(6)属性值自增自减
1
|
hincrby key field number
|
给user:2的age属性在原值基础上+/-10,然后在age现有值的基础上-2
1
2
3
4
5
6
7
8
9
10
|
# 按指定数值自增
127.0.0.1:6379> hincrby user:2 age 10
(integer) 77
127.0.0.1:6379> hincrby user:2 age 10
(integer) 87
# 按指定数值自减
127.0.0.1:6379> hincrby user:2 age -10
(integer) 77
127.0.0.1:6379> hincrby user:2 age -10
|
4.5. set(集合)
无序集合,重点就是去重和无序。
(1)添加元素
1
|
sadd key member1 member2 ...
|
向键authors
的集合中添加元素zhangsan
、lisi
、wangwu
1
|
sadd authors zhangsan lisi wangwu
|
(2)获取集合的所有的成员
获取键authors
的集合中所有元素
(3)获取集合的长度
获取s2集合的长度
1
2
3
4
|
sadd s2 a b c d e
127.0.0.1:6379> scard s2
(integer) 5
|
(4)随机抽取一个或多个元素
抽取出来的成员被删除掉
1
2
3
4
|
spop key [count=1]
# 注意:
# count为可选参数,不填则默认一个。被提取成员会从集合中被删除掉
|
随机获取s2集合的成员
1
2
3
4
5
6
|
sadd s2 a c d e
127.0.0.1:6379> spop s2
"d"
127.0.0.1:6379> spop s2
"c"
|
(5)删除指定元素
删除键authors
的集合中元素wangwu
(6)交集、差集和并集
推荐、(协同过滤,基于用户、基于物品)
1
2
3
|
sinter key1 key2 key3 .... # 交集、比较多个集合中共同存在的成员
sdiff key1 key2 key3 .... # 差集、比较多个集合中不同的成员
sunion key1 key2 key3 .... # 并集、合并所有集合的成员,并去重
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
del user:1 user:2 user:3 user:4
sadd user:1 1 2 3 4 # user:1 = {1,2,3,4}
sadd user:2 1 3 4 5 # user:2 = {1,3,4,5}
sadd user:3 1 3 5 6 # user:3 = {1,3,5,6}
sadd user:4 2 3 4 # user:4 = {2,3,4}
# 交集
127.0.0.1:6379> sinter user:1 user:2
1) "1"
2) "3"
3) "4"
127.0.0.1:6379> sinter user:1 user:3
1) "1"
2) "3"
127.0.0.1:6379> sinter user:1 user:4
1) "2"
2) "3"
3) "4"
127.0.0.1:6379> sinter user:2 user:4
1) "3"
2) "4"
# 并集
127.0.0.1:6379> sunion user:1 user:2 user:4
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
# 差集
127.0.0.1:6379> sdiff user:2 user:3
1) "4" # 此时可以给user:3推荐4
127.0.0.1:6379> sdiff user:3 user:2
1) "6" # 此时可以给user:2推荐6
127.0.0.1:6379> sdiff user:1 user:3
1) "2"
2) "4"
|
4.6. zset(有序集合)
有序集合,去重并且根据score权重值来进行排序的。score从小到大排列。
(1)添加成员
1
|
zadd key score1 member1 score2 member2 score3 member3 ....
|
设置榜单achievements,设置成绩和用户名作为achievements的成员
1
2
3
4
5
|
127.0.0.1:6379> zadd achievements 61 xiaoming 62 xiaohong 83 xiaobai 78 xiaohei 87 xiaohui 99 xiaolan
(integer) 6
127.0.0.1:6379> zadd achievements 85 xiaohuang
(integer) 1
127.0.0.1:6379> zadd achievements 54 xiaoqing
|
(2)获取score在指定区间的所有成员
1
2
3
4
|
zrangebyscore key min max # 按score进行从低往高排序获取指定score区间
zrevrangebyscore key min max # 按score进行从高往低排序获取指定score区间
zrange key start stop # 按scoer进行从低往高排序获取指定索引区间
zrevrange key start stop # 按scoer进行从高往低排序获取指定索引区间
|
1
|
zrange achievements 0 -1 # 从低到高全部成员
|
(3)获取集合长度
获取users的长度
(4)获取指定成员的权重值
获取users中xiaoming的成绩
1
2
3
4
5
6
|
127.0.0.1:6379> zscore achievements xiaobai
"93"
127.0.0.1:6379> zscore achievements xiaohong
"62"
127.0.0.1:6379> zscore achievements xiaoming
"61"
|
(5)获取指定成员在集合中的排名
排名从0开始计算
1
2
|
zrank key member # score从小到大的排名
zrevrank key member # score从大到小的排名
|
获取achievements中xiaohei的分数排名,从大到小
1
2
|
127.0.0.1:6379> zrevrank achievements xiaohei
(integer) 4
|
(6)获取score在指定区间的所有成员数量
获取achievements从0~60分之间的人数[闭区间]
1
2
3
4
|
127.0.0.1:6379> zcount achievements 0 60
(integer) 2
127.0.0.1:6379> zcount achievements 54 60
(integer) 2
|
(7)给指定成员增加增加权重值
1
|
zincrby key score member
|
给achievements中xiaobai增加10分
1
2
|
127.0.0.1:6379> ZINCRBY achievements 10 xiaobai
"93
|
(8)删除成员
1
|
zrem key member1 member2 member3 ....
|
从achievements中删除xiaoming的数据
1
|
zrem achievements xiaoming
|
(9)删除指定数量的成员
1
2
3
4
|
# 删除指定数量的成员,从最低score开始删除
zpopmin key [count]
# 删除指定数量的成员,从最高score开始删除
zpopmax key [count]
|
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# 从achievements中提取并删除成绩最低的2个数据
127.0.0.1:6379> zpopmin achievements 2
1) "xiaoqing"
2) "54"
3) "xiaolv"
4) "60"
# 从achievements中提取并删除成绩最高的2个数据
127.0.0.1:6379> zpopmax achievements 2
1) "xiaolan"
2) "99"
3) "xiaobai"
4) "93"
|
五、Python操作redis
一、python对redis基本操作
(1)连接redis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# 方式1
import redis
r = redis.Redis(host='127.0.0.1', port=6379)
r.set('foo', 'Bar')
print(r.get('foo'))
# 方式2
import redis
pool = redis.ConnectionPool(host='127.0.0.1', port=6379)
r = redis.Redis(connection_pool=pool)
r.set('bar', 'Foo')
print(r.get('bar'))
|
通常情况下, 当我们需要做redis操作时, 会创建一个连接, 并基于这个连接进行redis操作, 操作完成后, 释放连接,一般情况下, 这是没问题的, 但当并发量比较高的时候, 频繁的连接创建和释放对性能会有较高的影响。于是, 连接池就发挥作用了。连接池的原理是, 通过预先创建多个连接, 当进行redis操作时, 直接获取已经创建的连接进行操作, 而且操作完成后, 不会释放, 用于后续的其他redis操作。这样就达到了避免频繁的redis连接创建和释放的目的, 从而提高性能。
(2)数据类型操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
import redis
pool = redis.ConnectionPool(host='127.0.0.1', port=6379, db=0, decode_responses=True)
r = redis.Redis(connection_pool=pool)
# (1)字符串操作:不允许对已经存在的键设置值
ret = r.setnx("name", "yuan")
print(ret) # False
# (2)字符串操作:设置键有效期
r.setex("good_1001", 10, "2")
# (3)字符串操作:自增自减
r.set("age", 20)
r.incrby("age", 2)
print(r.get("age")) # b'22'
# (4)hash操作:设置hash
r.hset("info", "name", "rain")
print(r.hget("info", "name")) # b'rain'
r.hset("info", "gender", "male", {"age": 22})
print(r.hgetall("info")) # {b'name': b'rain', b'gender': b'male', b'age': b'22'}
# (5)list操作:设置list
r.rpush("scores", "100", "90", "80")
r.rpush("scores", "70")
r.lpush("scores", "120")
print(r.lrange("scores", 0, -1)) # ['120', '100', '90', '80', '70']
r.linsert("scores", "AFTER", "100", 95)
print(r.lrange("scores", 0, -1)) # ['120', '100', '95', '90', '80', '70']
print(r.lpop("scores")) # 120
print(r.rpop("scores")) # 70
print(r.lindex("scores", 1)) # '95'
# (6)集合操作
# key对应的集合中添加元素
r.sadd("name_set", "zhangsan", "lisi", "wangwu")
# 获取key对应的集合的所有成员
print(r.smembers("name_set")) # {'lisi', 'zhangsan', 'wangwu'}
# 从key对应的集合中随机获取 numbers 个元素
print(r.srandmember("name_set", 2))
r.srem("name_set", "lisi")
print(r.smembers("name_set")) # {'wangwu', 'zhangsan'}
# (7)有序集合操作
# 在key对应的有序集合中添加元素
r.zadd("jifenbang", {"yuan": 78, "rain": 20, "alvin": 89, "eric": 45})
# 按照索引范围获取key对应的有序集合的元素
# zrange( name, start, end, desc=False, withscores=False, score_cast_func=float)
print(r.zrange("jifenbang", 0, -1)) # ['rain', 'eric', 'yuan', 'alvin']
print(r.zrange("jifenbang", 0, -1, withscores=True)) # ['rain', 'eric', 'yuan', 'alvin']
print(r.zrevrange("jifenbang", 0, -1, withscores=True)) # ['rain', 'eric', 'yuan', 'alvin']
print(r.zrangebyscore("jifenbang", 0, 100))
print(r.zrangebyscore("jifenbang", 0, 100, start=0, num=1))
# 删除key对应的有序集合中值是values的成员
print(r.zrem("jifenbang", "yuan")) # 删除成功返回1
print(r.zrange("jifenbang", 0, -1)) # ['rain', 'eric', 'alvin']
# (8)键操作
r.delete("scores")
print(r.exists("scores"))
print(r.keys("*"))
r.expire("name",10)
|
二、关于redis的实战案例
(1)案例1:KV缓存
第1个是最基础也是最常?的就是KV功能,我们可以用Redis来缓存用户信息、会话信息、商品信息等等。下面这段代码就是通过缓存读取逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import redis
pool = redis.ConnectionPool(host='127.0.0.1', port=6379, db=6, decode_responses=True)
r = redis.Redis(connection_pool=pool)
def get_user(user_id):
user = r.get(user_id)
if not user:
user = UserInfo.objects.get(pk=user_id)
r.setex(user_id, 3600, user)
return user
|
(2)案例2:分布式锁
什么是分布式锁
❝
分布式锁其实就是,控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。
❞
提到Redis的分布式锁,很多小伙伴马上就会想到setnx
+ expire
命令。即先用setnx
来抢锁,如果抢到之后,再用expire
给锁设置一个过期时间,防止锁忘记了释放。
❝
SETNX 是SET IF NOT EXISTS的简写.日常命令格式是SETNX key value,如果 key不存在,则SETNX成功返回1,如果这个key已经存在了,则返回0。
❞
假设某电商网站的某商品做秒杀活动,key可以设置为key_resource_id,value设置任意值,伪代码如下:
方案1
1
2
3
4
5
6
7
8
9
10
11
|
import redis
pool = redis.ConnectionPool(host='127.0.0.1')
r = redis.Redis(connection_pool=pool)
ret = r.setnx("key_resource_id", "ok")
if ret:
r.expire("key_resource_id", 5) # 设置过期时间
print("抢购成功!")
r.delete("key_resource_id") # 释放资源
else:
print("抢购失败!")
|
但是这个方案中,setnx
和expire
两个命令分开了,「不是原子操作」。如果执行完setnx
加锁,正要执行expire
设置过期时间时,进程crash或者要重启维护了,那么这个锁就“长生不老”了,「别的线程永远获取不到锁啦」。
方案2:SETNX + value值是(系统时间+过期时间)
为了解决方案一,「发生异常锁得不到释放的场景」,可以把过期时间放到setnx
的value值里面。如果加锁失败,再拿出value值校验一下即可。加锁代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import time
def foo():
expiresTime = time.time() + 10
ret = r.setnx("key_resource_id", expiresTime)
if ret:
print("当前锁不存在,加锁成功")
return True
oldExpiresTime = r.get("key_resource_id")
if float(oldExpiresTime) < time.time(): # 如果获取到的过期时间,小于系统当前时间,表示已经过期
# 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
newExpiresTime = r.getset("key_resource_id", expiresTime)
if oldExpiresTime == newExpiresTime:
# 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁
return True # 加锁成功
return False # 其余情况加锁皆失败
foo()
|
方案3
实际上,我们还可以使用Py的redis模块中的set函数来保证原子性(包含setnx和expire两条指令)代码如下:
1
|
r.set("key_resource_id", "1", nx=True, ex=10)
|
(3)案例3:定时任务
利用 Redis 也能实现订单30分钟自动取消。
用户下单之后,在规定时间内如果不完成付款,订单自动取消,并且释放库存使用技术:Redis键空间通知(过期回调)用户下单之后将订单id作为key,任意值作为值存入redis中,给这条数据设置过期时间,也就是订单超时的时间启用键空间通知
方式1:开启过期key监听
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
from redis import StrictRedis
redis = StrictRedis(host='localhost', port=6379)
# 监听所有事件
# pubsub = redis.pubsub()
# pubsub.psubscribe('__keyspace@0__:*')
#
# print('Starting message loop')
# while True:
# message = pubsub.get_message()
# if message:
# print(message)
# 监听过期key
def event_handler(msg):
print("sss",msg)
thread.stop()
pubsub = redis.pubsub()
pubsub.psubscribe(**{'__keyevent@0__:expired': event_handler})
thread = pubsub.run_in_thread(sleep_time=0.01)
|
(4)案例4:延迟队列
延时队列可以通过Redis的zset(有序列表)来实现。我们将消息序列化为一个字符串作为zset的值。这个消息的到期时间处理时间作为score,然后用多个线程轮询zset获取到期的任务进行处理,多线程时为了保障可用性,万一挂了一个线程还有其他线程可以继续处理。因为有多个线程,所有需要考虑并发争抢任务,确保任务不能被多次执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
import time
import uuid
import redis
pool = redis.ConnectionPool(host='127.0.0.1', port=6379, decode_responses=True)
r = redis.Redis(connection_pool=pool)
def delay_task(task_name, delay_time):
# 保证value唯一
task_id = task_name + str(uuid.uuid4())
print()
# 5s后重试
retry_ts = time.time() + delay_time
r.zadd("delay-queue", {task_id: retry_ts})
def loop():
print("循环监听中...")
while True:
# 最多取1条
task_list = r.zrangebyscore("delay-queue", 0, time.time(), start=0, num=1)
if not task_list:
# 延时队列空的,休息1s
print("cost 1秒钟")
time.sleep(1)
continue
task_id = task_list[0]
success = r.zrem("delay-queue", task_id)
if success:
# 处理消息逻辑函数
handle_msg(task_id)
def handle_msg(msg):
"""消息处理逻辑"""
print(f"消息{msg}已经被处理完成!")
import threading
t = threading.Thread(target=loop)
t.start()
delay_task("任务1延迟5", 5)
delay_task("任务2延迟2", 2)
delay_task("任务3延迟3", 3)
delay_task("任务4延迟10", 10)
|
redis的zrem方法是对多线程争抢任务的关键,它的返回值决定了当前实例有没有抢到任务,因为loop方法可能会被多个线程、多个进程调用, 同一个任务可能会被多个进程线程抢到,通过zrem来决定唯一的属主。
同时,一定要对handle_msg进行异常捕获, 避免因为个别任务处理问题导致的循环异常退出。
(5)案例5:发布订阅
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
import threading
import redis
r = redis.Redis(host='127.0.0.1')
def recv_msg():
pub = r.pubsub()
pub.subscribe("fm104.5")
pub.parse_response()
while 1:
msg = pub.parse_response()
print(msg)
def send_msg():
msg = input(">>>")
r.publish("fm104.5", msg)
t = threading.Thread(target=send_msg)
t.start()
recv_msg()
|