redis基础与进阶

一、引言

在Web应用发展的初期,那时关系型数据库受到了较为广泛的关注和应用,原因是因为那时候Web站点基本上访问和并发不高、交互也较少。而在后来,随着访问量的提升,使用关系型数据库的Web站点多多少少都开始在性能上出现了一些瓶颈,而瓶颈的源头一般是在磁盘的I/O上。而随着互联网技术的进一步发展,各种类型的应用层出不穷,这导致在当今云计算、大数据盛行的时代,对性能有了更多的需求,主要体现在以下四个方面:

  1. 低延迟的读写速度:应用快速地反应能极大地提升用户的满意度
  2. 支撑海量的数据和流量:对于搜索这样大型应用而言,需要利用PB级别的数据和能应对百万级的流量
  3. 大规模集群的管理:系统管理员希望分布式应用能更简单的部署和管理
  4. 庞大运营成本的考量:IT部门希望在硬件成本、软件成本和人力成本能够有大幅度地降低

为了克服这一问题,NoSQL应运而生,它同时具备了高性能、可扩展性强、高可用等优点,受到广泛开发人员和仓库管理人员的青睐。

关系型数据库(RMDBS)与非关系型数据库(NoSQL)的对比:

数据库中表与表的数据之间存在某种关联的内在关系,因为这种关系,所以我们称这种数据库为关系型数据库。典型:Mysql/MariaDB、postgreSQL、Oracle、SQLServer、DB2、Access、SQLlite3。特点:

  1. 全部使用SQL(结构化查询语言)进行数据库操作。
  2. 都存在主外键关系,表,等等关系特征。
  3. 大部分都支持各种关系型的数据库的特性:事务、存储过程、触发器、视图、临时表、模式、函数

NOSQL:not only sql,泛指非关系型数据库。泛指那些不使用SQL语句进行数据操作的数据库,所有数据库中只要不使用SQL语句的都是非关系型数据库。典型:Redis、MongoDB、hbase、 Hadoop、elasticsearch、图数据库(Neo4j、GraphDB、SequoiaDB)。。。。

特点:

  1. 每一款都不一样。用途不一致,功能不一致,各有各的操作方式。
  2. 基本不支持主外键关系,也没有事务的概念。(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服务启动方式

redis-server --service-install redis.windows.conf 安装服务
redis-server --service-start 启动服务
redis-server --service-stop 停止服务

如果连接操作redis,可以在终端下,使用以下命令

redis-cli

ubuntu下安装:

安装命令: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的配置

cat /etc/redis/redis.conf

redis 安装成功以后,window下的配置文件保存在软件 安装目录下,如果是mac或者linux,则默认安装/etc/redis/redis.conf

redis的核心配置选项

绑定ip:访问白名单,如果需要远程访问,可将此注释,或绑定1个真实ip

bind 127.0.0.1   xx.xx.xx.xx

端⼝,默认为6379

port 6379

是否以守护进程运行

  • 如果以守护进程运行,则不会在命令阻塞,类似于服务
  • 如果以守护进程运行,则当前终端被阻塞
  • 设置为yes表示守护进程,设置为no表示⾮守护进程
  • 推荐设置为yes
daemonize yes

RDB持久化的备份策略(RDB备份是默认开启的)

# save 时间 读写次数
 save 900 1     # 当redis在900内至少有1次读写操作,则触发一次数据库的备份操作
 save 300 10    # 当redis在300内至少有10次读写操作,则触发一次数据库的备份操作
 save 60 10000  # 当redis在60内至少有10000次读写操作,则触发一次数据库的备份操作

RDB持久化的备份文件

dbfilename dump.rdb

RDB持久化数据库数据文件的所在目录

dir /var/lib/redis

日志文件所载目录

loglevel notice
logfile /var/log/redis/redis-server.log

进程ID文件

pidfile /var/run/redis/redis-server.pid

数据库,默认有16个,数据名是不能自定义的,只能是0-15之间,当然这个15是数据库数量-1

database 16

redis的登录密码,生产阶段打开,开发阶段避免麻烦,一般都是注释的。redis在6.0版本以后新增了ACL访问控制机制,新增了用户管理,这个版本以后才有账号和密码,再次之前只有没有密码没有账号

# requirepass foobared

注意:开启了以后,redis-cli终端下使用 auth 密码来认证登录。

image-20211108102339592

AOF持久化的开启配置项(默认值是no,关闭状态)

appendonly no

AOF持久化的备份文件(AOF的备份数据文件与RDB的备份数据文件保存在同一个目录下,由dir配置项指定)

appendfilename "appendonly.aof"

AOF持久化备份策略[时间]

# appendfsync always
appendfsync everysec    # 工作中最常用。每一秒备份一次
# appendfsync no

哨兵集群:一主二从三哨兵(3台服务器)

Redis的使用

redis是一款基于CS架构的数据库,所以redis有客户端redis-cli,也有服务端redis-server。

其中,客户端可以使用go、java、python等编程语言,也可以终端下使用命令行工具管理redis数据库,甚至可以安装一些别人开发的界面工具,例如:RDM。

redis-cli客户端连接服务器:

# redis-cli -h `redis服务器ip` -p `redis服务器port`
redis-cli -h 10.16.244.3 -p 6379

四、redis数据类型

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 设置的数据没有额外操作时,是不会过期的。

set key value

设置键为name值为yuan的数据

set name yuan
set name rain # 一个变量可以设置多次

注意:redis中的所有数据操作,如果设置的键不存在则为添加,如果设置的键已经存在则修改。

设置一个键,当键不存在时才能设置成功,用于一个变量只能被设置一次的情况。

setnx  key  value

一般用于给数据加锁(分布式锁)

27.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中可以对一切的数据进行设置有效期。以秒为单位

setex key seconds value

设置键为goods_1值为101过期时间为10秒的数据

setex name goods_1 10

3. 关于设置保存数据的有效期

setex 添加保存数据到redis,同时设置有效期,格式:

setex key time value

4. 设置多个键值

mset key1 value1 key2 value2 ...

例3:设置键为a1值为goland、键为a2值为java、键为a3值为`c

mset a1 goland a2 java a3 c

5. 字符串拼接值

常见于大文件上传

append key value

向键为a1中拼接值haha

set title "我的"
append title "redis"
append title "学习之路"

6. 根据键获取值

根据键获取值,如果不存在此键则返回nil

get key

获取键name的值

get name

根据多个键获取多个值

mget key1 key2 ...

获取键a1、a2、a3的值

mget a1 a2 a3

getset:设置key的新值,返回旧值

redis> GETSET db mongodb    # 没有旧值,返回 nil
(nil)
redis> GET db
"mongodb"

redis> GETSET db "redis"    # 返回旧值 mongodb
"mongodb"

redis> GET db
"redis"

7. 自增自减

web开发中的电商抢购、秒杀。游戏里面的投票、攻击计数。系统中计算当前在线人数、

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. 获取字符串的长度

set name xiaoming
strlen name  # 8 

9. 比特流操作

1字节=8比特 1kb = 1024字节 1mb = 1024kb 1gb = 1024mb

1个int8就是一个字节,一个中文:3个字节

签到记录

SETBIT     # 设置一个bit数据的值 
GETBIT     # 获取一个bit数据的值
BITCOUNT   # 统计字符串被设置为1的bit数.
BITPOS     # 返回字符串里面第一个被设置为1或者0的bit位。

终端操作:

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)查找键

参数支持简单的正则表达式

keys pattern

查看所有键

keys *

例子:

# 查看名称中包含`a`的键
keys *a*
# 查看以a开头的键
keys a*
# 查看以a结尾的键
keys *a

(2)判断键是否存在

如果存在返回1,不存在返回0

exists key1

判断键title是否存在

exists title

(3)查看键的的值的数据类型

type key

# string    字符串
# hash      哈希类型
# list      列表类型
# set       无序集合
# zset      有序集合

查看键的值类型

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)删除键以及键对应的值

del key1 key2 ...

(5)查看键的有效期

ttl key

# 结果结果是秒作为单位的整数
# -1 表示永不过期
# -2 表示当前数据已经过期,查看一个不存在的数据的有效期就是-2

(6)设置key的有效期

给已有的数据重新设置有效期,redis中所有的数据都可以通过expire来设置它的有效期。有效期到了,数据就被删除。

expire key seconds

(7)清空所有key

慎用,一旦执行,则redis所有数据库0~15的全部key都会被清除

flushall

(8)key重命名

rename  oldkey newkey

把name重命名为username

set name yuan
rename name username
get username

select切换数据库

redis的配置文件中,默认有0~15之间的16个数据库,默认操作的就是0号数据库
select <数据库ID>

操作效果:

# 默认处于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)添加子成员

# 在左侧(前)添加一条或多条数据
lpush key value1 value2 ...

# 在右侧(后)添加一条或多条数据
rpush key value1 value2 ...

# 在指定元素的左边(前)/右边(后)插入一个或多个数据
linsert key before 指定元素 value1 value2 ....
linsert key after 指定元素 value1 value2 ....

从键为brother的列表左侧添加一个或多个数据liubei、guanyu、zhangfei

lpush brother liubei
# [liubei]
lpush brother guanyu zhangfei xiaoming
# [xiaoming,zhangfei,guanyu,liubei]

从键为brother的列表右侧添加一个或多个数据,xiaohong,xiaobai,xiaohui

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

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

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]

注意:当列表如果存在多个成员值一致的情况下,默认只识别第一个。

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开始,逐个递减

lindex key index

获取brother下标为2以及-2的成员

del brother
lpush brother guanyu zhangfei xiaoming
lindex brother 2
# "guanyu"
lindex brother -2
# "zhangfei"

移除并获取列表的第一个成员或最后一个成员

lpop key  # 第一个成员出列
rpop key  # 最后一个成员出列

获取并移除brother中的第一个成员

lpop brother
# 开发中往往使用rpush和lpop实现队列的数据结构->实现入列和出列

(3)获取列表的切片

闭区间[包括stop]

lrange key start stop

操作:

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)获取列表的长度

llen key

获取brother列表的成员个数

llen brother

(5)按索引设置值

lset key index value
# 注意:
# redis的列表也有索引,从左往右,从0开始,逐一递增,第1个元素下标为0
# 索引可以是负数,表示尾部开始计数,如`-1`表示最后1个元素

修改键为brother的列表中下标为4的元素值为xiaohongmao

lset brother 4 xiaohonghong

(6)删除指定成员

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/结构体

结构:

键key:{
    域field: 值value,
    域field: 值value,
    域field: 值value,
}

(1)设置指定键的属性/域

设置指定键的单个属性

hset key field value

设置键 user_1的属性namexiaoming

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)获取指定键的域/属性的值

获取指定键所有的域/属性

hkeys key

获取键user的所有域/属性

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的值

127.0.0.1:6379> hget user:3 name
"xiaohong"

获取指定键的多个域/属性的值

hmget key field1 field2 ...

获取键user:2属性nameage的值

127.0.0.1:6379> hmget user:2 name age
1) "xiaohong"
2) "16"

获取指定键的所有值

hvals key

获取指定键的所有域对应的值

127.0.0.1:6379> hvals user:3
1) "xiaohong"
2) "17"
3) "1"

(3)获取hash的所有域值对

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)删除指定键的域/属性

删除键user:3的属性sex/age/name,当键中的hash数据没有任何属性,则当前键会被redis删除

hdel key field1 field2 ...
hdel user:3 sex age name

(5)判断指定属性/域是否存在于当前键对应的hash中

hexists   key  field

判断user:2中是否存在age属性

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)属性值自增自减

hincrby key field number

给user:2的age属性在原值基础上+/-10,然后在age现有值的基础上-2

# 按指定数值自增
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)添加元素

sadd key member1 member2 ...

向键authors的集合中添加元素zhangsanlisiwangwu

sadd authors zhangsan lisi wangwu

(2)获取集合的所有的成员

smembers key

获取键authors的集合中所有元素

smembers authors

(3)获取集合的长度

scard keys 

获取s2集合的长度

sadd s2 a b c d e

127.0.0.1:6379> scard s2
(integer) 5

(4)随机抽取一个或多个元素

抽取出来的成员被删除掉

spop key [count=1]

# 注意:
# count为可选参数,不填则默认一个。被提取成员会从集合中被删除掉

随机获取s2集合的成员

sadd s2 a c d e

127.0.0.1:6379> spop s2 
"d"
127.0.0.1:6379> spop s2 
"c"

(5)删除指定元素

srem key value

删除键authors的集合中元素wangwu

srem authors wangwu

(6)交集、差集和并集

推荐、(协同过滤,基于用户、基于物品)

sinter  key1 key2 key3 ....    # 交集、比较多个集合中共同存在的成员
sdiff   key1 key2 key3 ....    # 差集、比较多个集合中不同的成员
sunion  key1 key2 key3 ....    # 并集、合并所有集合的成员,并去重
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)添加成员

zadd key score1 member1 score2 member2 score3 member3 ....

设置榜单achievements,设置成绩和用户名作为achievements的成员

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在指定区间的所有成员

zrangebyscore key min max     # 按score进行从低往高排序获取指定score区间
zrevrangebyscore key min max  # 按score进行从高往低排序获取指定score区间
zrange key start stop         # 按scoer进行从低往高排序获取指定索引区间
zrevrange key start stop      # 按scoer进行从高往低排序获取指定索引区间
zrange achievements 0 -1  # 从低到高全部成员

(3)获取集合长度

zcard key

获取users的长度

zcard achievements

(4)获取指定成员的权重值

zscore key member

获取users中xiaoming的成绩

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开始计算

zrank key member      # score从小到大的排名
zrevrank key member   # score从大到小的排名

获取achievements中xiaohei的分数排名,从大到小

127.0.0.1:6379> zrevrank achievements xiaohei
(integer)4

(6)获取score在指定区间的所有成员数量

zcount key min max

获取achievements从0~60分之间的人数[闭区间]

127.0.0.1:6379> zcount achievements 0 60
(integer) 2
127.0.0.1:6379> zcount achievements 54 60
(integer) 2

(7)给指定成员增加增加权重值

zincrby key score member

给achievements中xiaobai增加10分

127.0.0.1:6379> ZINCRBY achievements 10 xiaobai
"93"

(8)删除成员

zrem key member1 member2 member3 ....

从achievements中删除xiaoming的数据

zrem achievements xiaoming

(9)删除指定数量的成员

# 删除指定数量的成员,从最低score开始删除
zpopmin key [count]
# 删除指定数量的成员,从最高score开始删除
zpopmax key [count]

例子:

# 从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 基本连接
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)redis字符串操作

redis中的String在在内存中按照一个name对应一个value来存储。如图:

**1.设置值**
set(name, value, ex=None, px=None, nx=False, xx=False)

在Redis中设置值,默认,不存在则创建,存在则修改
参数:
     ex,过期时间(秒)
     px,过期时间(毫秒)
     nx,如果设置为True,则只有name不存在时,当前set操作才执行
     xx,如果设置为True,则只有name存在时,岗前set操作才执行

setnx(name, value)	#设置值,只有name不存在时,执行设置操作(添加)
setex(name, value, time)	#设置值,参数:time,过期时间(数字秒 或 timedelta对象)
psetex(name, time_ms, value)	#设置值,参数:time_ms,过期时间(数字毫秒 或 timedelta对象)

2.批量设置值

mset(*args, **kwargs)

如:
    mset(k1='v1', k2='v2')
    或
    mget({'k1': 'v1', 'k2': 'v2'})

3.获取值

get(name)

4.批量获取

mget(keys, *args)
如:
    r.mget('ylr', 'wupeiqi')
    或
    r.mget(['ylr', 'wupeiqi'])

5.设置新值并获取原来的值

getset(name, value)

6.获取子序列(根据字节获取,非字符)

getrange(key, start, end)
# 参数:
    # name,Redis 的 name
    # start,起始位置(字节)
    # end,结束位置(字节)

7.修改字符串内容,从指定字符串索引开始向后替换(新值太长时,则向后添加)

setrange(name, offset, value)
# 参数:
    # offset,字符串的索引,字节(一个汉字三个字节)
    # value,要设置的值

8.返回name对应值的字节长度(一个汉字3个字节)

strlen(name)

9.自增 name对应的值,当name不存在时,则创建name=amount,否则,则自增

incr(self, name, amount=1)
# 参数:
    # name,Redis的name
    # amount,自增数(必须是整数)
  
# 注:同incrby

incrbyfloat(self, name, amount=1.0)
# 参数:
    # name,Redis的name
    # amount,自增数(浮点型)

10.自减 name对应的值,当name不存在时,则创建name=amount,否则,则自减

decr(self, name, amount=1)
# 参数:
    # name,Redis的name
    # amount,自减数(整数)

11.在redis name对应的值后面追加内容

append(key, value)
# 参数:
    key, redis的name
    value, 要追加的字符串

(3)redis哈希操作

hash表现形式上有些像pyhton中的dict,可以存储一组关联性较强的数据 , redis中Hash在内存中的存储格式如下图: 

1.name对应的hash中设置一个键值对(不存在,则创建;否则,修改)

hset(name, key, value)
# 参数:
    # name,redis的name
    # key,name对应的hash中的key
    # value,name对应的hash中的value
  
# 注:
    # hsetnx(name, key, value),当name对应的hash中不存在当前key时则创建(相当于添加)

2.在name对应的hash中批量设置键值对

hmset(name, mapping)
# 参数:
    # name,redis的name
    # mapping,字典,如:{'k1':'v1', 'k2': 'v2'}
  
# 如:
    # r.hmset('xx', {'k1':'v1', 'k2': 'v2'})

3.在name对应的hash中获取根据key获取value

hget(name,key)

4.在name对应的hash中获取多个key的值

hmget(name, keys, *args)
# 参数:
    # name,reids对应的name
    # keys,要获取key集合,如:['k1', 'k2', 'k3']
    # *args,要获取的key,如:k1,k2,k3
  
# 如:
    # r.mget('xx', ['k1', 'k2'])
    # 或
    # print r.hmget('xx', 'k1', 'k2')

5.获取name对应hash的所有键值

hgetall(name)

6.获取name对应的hash中键值对的个数

hlen(name) 

7.获取name对应的hash中所有的key的值

hkeys(name)

8.获取name对应的hash中所有的value的值

hvals(name)

9.检查name对应的hash是否存在当前传入的key

hexists(name, key)

10.name对应的hash中指定key的键值对删除

hdel(name,*keys)

11.自增name对应的hash中的指定key的值,不存在则创建key=amount

hincrby(name, key, amount=1)
# 自增name对应的hash中的指定key的值,不存在则创建key=amount
# 参数:
    # name,redis中的name
    # key, hash对应的key
    # amount,自增数(整数)
    
hincrbyfloat(name, key, amount=1.0)
# 参数:
    # name,redis中的name
    # key, hash对应的key
    # amount,自增数(浮点数)

12.利用yield封装hscan创建生成器,实现分批去redis中获取数据

hscan_iter(name, match=None, count=None)
# 参数:
    # match,匹配指定key,默认None 表示所有的key
    # count,每次分片最少获取个数,默认None表示采用Redis的默认分片个数
   
# 如:
    # for item in r.hscan_iter('xx'):
    #     print item

(4)redis链表操作

List操作,redis中的List在在内存中按照一个name对应一个List来存储。如图:

1.在name对应的list中添加元素,每个新的元素都添加到列表的最左边

lpush(name,values)
# 如:
    # r.lpush('oo', 11,22,33)
    # 保存顺序为: 33,22,11
  
# 扩展:
    # rpush(name, values) 表示从右向左操作

2.在name对应的list中添加元素,只有name已经存在时,值添加到列表的最左边

lpushx(name,value)
# 更多:
    # rpushx(name, value) 表示从右向左操作

3.name对应的list元素的个数

llen(name)

4.在name对应的列表的某一个值前或后插入一个新值

linsert(name, where, refvalue, value))
# 参数:
    # name,redis的name
    # where,BEFORE或AFTER
    # refvalue,标杆值,即:在它前后插入数据
    # value,要插入的数据

5.对name对应的list中的某一个索引位置重新赋值

r.lset(name, index, value)
# 参数:
    # name,redis的name
    # index,list的索引位置
    # value,要设置的值

6.在name对应的list中删除指定的值

r.lrem(name, value, num) 
# 参数:
    # name,redis的name
    # value,要删除的值
    # num,  num=0,删除列表中所有的指定值;
           # num=2,从前到后,删除2个;
           # num=-2,从后向前,删除2个

7.在name对应的列表的左侧获取第一个元素并在列表中移除,返回值则是第一个元素

lpop(name)
 更多:
    # rpop(name) 表示从右向左操作

8.在name对应的列表中根据索引获取列表元素

lindex(name, index)

9.在name对应的列表分片获取数据

lrange(name, start, end)
# 参数:
    # name,redis的name
    # start,索引的起始位置
    # end,索引结束位置

10.在name对应的列表中移除没有在start-end索引之间的值

ltrim(name, start, end)
# 参数:
    # name,redis的name
    # start,索引的起始位置
    # end,索引结束位置

(5)redis集合操作

Set操作,Set集合就是不允许重复的列表

1. name对应的集合中添加元素

sadd(name,values)

2. 获取name对应的集合中元素个数

scard(name)

3. 获取一个name对应集合的交集

sinter(keys, *args)

4. 获取一个name对应的集合的并集

sunion(keys, *args)

5. 在第一个name对应的集合中且不在其他name对应的集合的元素集合

sdiff(keys, *args)

6 .获取name对应的集合的所有成员

smembers(name)

7. 检查value是否是name对应的集合的成员

sismember(name, value)

8. 从集合中随机移除一个成员,并将其返回

spop(name)

9. 从name对应的集合中随机获取 numbers 个元素

srandmember(name, numbers)

10. 在name对应的集合中删除某些值

srem(name, values)

11. 同字符串的操作,用于增量迭代分批获取元素,避免内存消耗太大

sscan_iter(name, match=None, count=None)
# 利用yield封装hscan创建生成器,实现分批去redis中获取数据
   
# 参数:
    # match,匹配指定key,默认None 表示所有的key
    # count,每次分片最少获取个数,默认None表示采用Redis的默认分片个数
   
# 如:
    # for item in r.hscan_iter('xx'):
    #     print item

(6)redis有序集合操作

有序集合,在集合的基础上,为每元素排序;元素的排序需要根据另外一个值来进行比较,所以,对于有序集合,每一个元素有两个值,即:值和分数,分数专门用来做排序。

1. 在name对应的有序集合中添加元素

zadd("zz",{"n1":1,"n2":2,"n3":3,"n4":4})
# 查看:print(r.zscan("zz"))  

2. 获取name对应的有序集合元素的数量

zcard(name)

3. 获取name对应的有序集合中分数 在 [min,max] 之间的个数

zcount(name, min, max)

4. 自增name对应的有序集合的 name 对应的分数

zincrby(name,amount,value) # amount要自增的分数

5. 按照索引范围获取name对应的有序集合的元素

# 按照索引范围获取name对应的有序集合的元素
aa=r.zrange("zset_name",0,1,desc=False,withscores=True,score_cast_func=int)
print(aa)
'''参数:
    name    redis的name
    start   有序集合索引起始位置
    end     有序集合索引结束位置
    desc    排序规则,默认按照分数从小到大排序
    withscores  是否获取元素的分数,默认只获取元素的值
    score_cast_func 对分数进行数据转换的函数'''

6. 获取name对应有序集合中 value 对应的分数

zscore(name, value)

7 .获取某个值在 name对应的有序集合中的排行(从 0 开始)

zrank(name, value)
# 更多:
    # zrevrank(name, value),从大到小排序

8 .删除name对应的有序集合中值是values的成员

zrem(name, values)
# 如:zrem('zz', ['s1', 's2'])

9 .根据排行范围删除

zremrangebyrank(name, min, max)

10 .根据分数范围删除

zremrangebyscore(name, min, max)

11.获取两个有序集合的交集,如果遇到相同值不同分数,则按照aggregate进行操作

zinterstore(dest, keys, aggregate=None)
# aggregate的值为:  SUM  MIN  MAX

12.获取两个有序集合的并集,如果遇到相同值不同分数,则按照aggregate进行操作

zunionstore(dest, keys, aggregate=None)
'''
r.zadd("z1",{"n1":1,"n2":2,"n3":3,"x":100})
r.zadd("z2",{"n3":4,"n5":5,"n6":6,"x":100})

r.zunionstore("z3",("z1","z2"))
print(r.zscan("z3"))
'''
# aggregate的值为:  SUM  MIN  MAX
# SUM:分数相加
# MIN:取分数小的值
# MAX:取分数大的值

(7)redis的其它常用操作

1.根据模型获取redis的name

keys(pattern='*')
# 更多:
    # KEYS * 匹配数据库中所有 key 。
    # KEYS h?llo 匹配 hello , hallo 和 hxllo 等。
    # KEYS h*llo 匹配 hllo 和 heeeeello 等。
    # KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo
# 参数:pattern="k*" 过滤以k开头的所有的键

2.根据删除redis中的任意数据类型

delete(*names)

3.检测redis的name是否存在

exists(name)

4.为某个redis的某个name设置超时时间

expire(name ,time)

5.对redis的name重命名

rename(src, dst)

6.随机获取一个redis的name(不删除)

randomkey()

7.获取name对应值的类型

type(name)

8.同字符串操作,用于增量迭代获取key

scan_iter(match=None, count=None)

# match,匹配指定key,默认None 表示所有的key
# count,每次分片最少获取个数,默认None表示采用Redis的默认分片个数

(8)使用场景

(一)String

这个其实没啥好说的,最常规的set/get操作,value可以是String也可以是数字。一般做一些复杂的计数功能的缓存,比如减少库存。

(二)hash

这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。博主在做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。


(三)list

使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。本人还用一个场景,很合适---取行情信息。就也是个生产者和消费者的场景。LIST可以很好的完成排队,先进先出的原则。


(四)set

因为set堆放的是一堆不重复值的集合。所以可以做全局去重的功能。
另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。

(五)sorted set

sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作。

(9)redis的管道操作

redis-py默认在执行每次请求都会创建(连接池申请连接)和断开(归还连接池)一次连接操作,如果想要在一次请求中指定多个命令,则可以使用pipline实现一次请求指定多个命令,并且默认情况下一次pipline 是原子性操作。

import redis
  
pool = redis.ConnectionPool(host='10.211.55.4', port=6379)
r = redis.Redis(connection_pool=pool)
# pipe = r.pipeline(transaction=False)
pipe = r.pipeline(transaction=True)
  
pipe.set('name', 'alex')
pipe.set('role', 'sb')
  
pipe.execute()

(10)redis的发布订阅

# 订阅者:
import redis
 
r=redis.Redis(host='127.0.0.1')
pub=r.pubsub() # 构建发布订阅对象
 
pub.subscribe("fm104.5") # 监听某个name键
pub.parse_response() # 确定监听
 
while 1:
    msg = pub.parse_response()
    print(msg) 
# 发布者:
import redis
 
r=redis.Redis(host='127.0.0.1')
r.publish("fm104.5", "Hi,yuan!") # 向某个name键插入值

​ 发布订阅的特性用来做一个简单的实时聊天系统再适合不过了,当然这样的东西开发中很少涉及到。再比如在分布式架构中,常常会遇到读写分离的场景,在写入的过程中,就可以使用redis发布订阅,使得写入值及时发布到各个读的程序中,就保证数据的完整一致性。再比如,在一个博客网站中,有100个粉丝订阅了你,当你发布新文章,就可以推送消息给粉丝们拉。

二、关于redis的实战案例

(1)案例1:KV缓存

第1个是最基础也是最常?的就是KV功能,我们可以用Redis来缓存用户信息、会话信息、商品信息等等。下面这段代码就是通过缓存读取逻辑。

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

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("抢购失败!")

但是这个方案中,setnxexpire两个命令分开了,「不是原子操作」。如果执行完setnx加锁,正要执行expire设置过期时间时,进程crash或者要重启维护了,那么这个锁就“长生不老”了,「别的线程永远获取不到锁啦」

方案2:SETNX + value值是(系统时间+过期时间)

为了解决方案一,「发生异常锁得不到释放的场景」,可以把过期时间放到setnx的value值里面。如果加锁失败,再拿出value值校验一下即可。加锁代码如下:

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两条指令)代码如下:

r.set("key_resource_id", "1", nx=True, ex=10)

(3)案例3:定时任务

利用 Redis 也能实现订单30分钟自动取消。

用户下单之后,在规定时间内如果不完成付款,订单自动取消,并且释放库存使用技术:Redis键空间通知(过期回调)用户下单之后将订单id作为key,任意值作为值存入redis中,给这条数据设置过期时间,也就是订单超时的时间启用键空间通知

方式1:开启过期key监听

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获取到期的任务进行处理,多线程时为了保障可用性,万一挂了一个线程还有其他线程可以继续处理。因为有多个线程,所有需要考虑并发争抢任务,确保任务不能被多次执行。

20200513191721238606

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:发布订阅

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()
posted @ 2022-08-11 15:50  晚点心动。  阅读(74)  评论(0)    收藏  举报