redis高级
redis高级
redis特性
- 速度快,10w ops(秒读写)数据都是内存操作,c语言实现。
- 单线程模型,所有的读写是同步的,不会出现脏读脏写
- 持久化:rdb和aof策略
- 多种数据结构:5大数据结构
- 支持多种编程语言:基于tcp通信协议,各大编程语言都支持通信
- 功能丰富:发布订阅(消息) Lua脚本,事务(pipeline)
- 操作简单:不依赖外部库,自身功能足够强大
- 主从复制:主服务器和从服务器,主服务器可以同步到从服务器中
- 支持高可用和分布式
linux安装部署
# 1 下载(源代码,c)
wget http://download.redis.io/releases/redis-6.2.9.tar.gz
#2 解压
tar -xzf redis-6.2.9.tar.gz
#3 建立软连接
ln -s redis-6.2.9 redis
cd redis
make&&make install
#4 在src目录下可以看到
#redis-server--->redis服务器
#redis-cli---》redis命令行客户端
#redis-benchmark---》redis性能测试工具
#redis-check-aof--->aof文件修复工具
#redis-check-rdb---》rdb文件检查工具
#redis-sentinel---》sentinel服务器,哨兵
#redis作者对windows维护不好,window自己有安装包
# ####卸载redis
# 1、查看redis进程;
ps aux|grep redis
# 2、kill掉进程;
kill 进程id
# 3、进入到redis目录
cd /usr/local/
# 4、删除redis对应的文件
rm -f /usr/local/redis/bin/redis*
rm -f /usr/local/bin/redis*
# 5、删除对应的文件
rm -rf redis
现在也支持通过yum命令一键下载部署的方式。
redis-server启动
3.1 最简启动
#最简启动
redis-server
ps -ef|grep redis #查看进程
#yum install net-tools -y
netstat -antpl|grep redis #查看端口
redis-cli -h ip -p port ping #命令查看
3.2 动态参数启动
#动态参数启动
redis-serve --port 6380 #启动,监听6380端口
3.3 配置文件启动
# 默认配置文件:是redis文件夹下的redis.conf
# 自定义配置文件启动
#####通过redis-cli连接,输入config get * 可以获得默认配置
#自己手创一个myredis.conf文件
#daemonize--》是否是守护进程启动(no|yes)
#port---》端口号
#logfile--》redis系统日志
#dir--》redis工作目录
daemonize yes
pidfile /var/run/redis.pid
port 6379
dir "/root/redis/data"
logfile 6379.log
# 创建data文件夹
mkdir data # 按照上面的路径创建一个存data的工作目录
# 启动
redis-server myredis.conf
客户端连接命令
redis-cli -h 地址 -p 端口
redis-cli -h 127.0.0.1 -p 6379
redis-cli -h 127.0.0.1 -p 6379 info # info是redis交互的一个命令,但是如果直接在客户端连接后跟交互命令,那么不进入交互环境,直接拿到结果
CONFIG GET * # 获取redis的配置信息
# 只需要关注 dir,port,daemonize等
# requirepass 设置的密码是什么,如果是空,没有密码
# 直接修改redis的配置信息,修改后同步到硬盘,永久生效
CONFIG SET requirepass 123456 # 设置密码配置
CONFIG REWRITE # 将改动的配置写到硬盘上了
# 当redis设置密码后,必须使用如下两种方式验证才能进行其他命令操作
-方式一:直接连接,指定密码:redis-cli -a 123456
-方式二:先连接进去redis-cli,再执行r auth 密码
# redis配置文件:
# bind 127.0.0.1 0.0.0.0
# port 端口
# requirepass 密码
# dir 工作目录
# logfile 日志文件
# daemonize 是否以守护进程运行
# pidfile 放的是进程id号
# maxmemory 可以使用的最大内存
# databases 有多少个库
# dbfilename rdb的持久化方案的存储文件dump.rdb
redis典型场景
- 缓存系统:使用最广泛的就是缓存
- 计数器:网站访问量,转发量,评论数(因为是单线程模型,所以不会出现并发安全问题)
- 消息队列:发布订阅,阻塞队列实现(简单的分布式,blpop:阻塞队列,生产者消费者) -- celery的消息队列就可以用到
- 社交网络:很多特效跟社交网络匹配,粉丝数,关注数,简单的推荐
- 实时系统:垃圾邮件处理系统,黑白名单系统
- 地理位置信息:附近的人
redis通用命令
打印key
打印所有的key
keys *
打印出以he开头的key
keys he*
打印出所有以he开头,第三个字母是h到l的字母的key值
keys he[h-l]
实际上keys后的表达式和正则表达式很像,就是匹配所有符合表达式的key
keys指令不要在生产环境下执行,因为redis就这一个线程,被这个命令占据的话会很影响业务
计算key的总数
redis内置了计数器,所以相当于直接把key的总数查出来了
dbsize
值操作
查看值
get key
设置值
set key value [expiration EX seconds|PX milliseconds] [NX|XX]
查看key是否存在
exists key
(integer) 1
# 存在返回1 不存在返回0
删除值
del key
(integer) 1
# 删除成功返回1 不存在返回0
设置值的过期时间
expire key seconds
expire name 3 # name3s后过期
ttl name # 查看name还有多长时间过期
persist name # 去掉name的过期时间
查看key值的类型
type name
其他命令
info命令:内存,cpu,主从相关
client list 正在连接的会话
client kill ip:端口
dbsize 总共有多少个key
flushall 清空所有
flushdb 只清空当前库
select 数字 选择某个库 总共16个库
monitor 记录操作日志,停止交互,只显示输出日志,能看到操作地址、操作内容等
内部编码和redis架构
数据结构和内部编码
五大数据类型还可以继续细分不同的内置编码
单线程架构
单线程架构
一个瞬间只会执行一条命令
为什么用了单线程还能这么快
是指单线程下为什么并发还挺高
- 纯内存操作
- 非阻塞IO (epoll),自身实现了事件处理,不在网络io上浪费过多时间
- 避免线程间切换和竞态消耗
注意
-
1 一次只运行一条命令
-
2 拒绝长慢命令
-keys,flushall,flushdb,慢的lua脚本,mutil/exec,operate,big value
-
3 其实不完全是单线程,但是数据操作部分就是单线程的(在做持久化是另外的线程)
-fysnc file descriptor
-close file descriptor
redis各数据类型操作
字符串操作
redis的字符串类型可以用来存储json格式的数据,对数字类型可以做简单的运算,适合做计数器,分布式id
get name # 按key查值
set name leethon 【ex|px】【nx|xx】 # 按key存值
del name # 删除kv
### 对字符串中的数字操作
incr age # key为age的值+1
decr age # key为age的值-1
incrby age 10 # key为age的值+10
decrby age 10 # key为age的值-10
# 以上操作适合做统计(单线程下所有操作都是隔离的)
# 还可以做分布式id(单号都是当前时间拼接一个redis自增的id,拼好的字符串一定不会冲突)
### 批量操作(可以节省一些网络时间)
mget key1 key2 key3 # 批量获取
mset key1 value1 【key value...】 # 批量设值
getset key value # 设值新值并返回旧值
### 其他字符串操作
append key extra_value # 往字符串尾部追加字符
setrange key index value # 在字符串指定索引位置上替换字符
getrange key start end # 获取字符串指定范围内的字符
strlen name # 获取字符串的长度(字节数)
increbyfloat key 3.5 # 加浮点自增
哈希操作
key:{field1:value1,field2:value2}
# 几乎以h开头的指令
hget key field # 获取hash key对应的field的value值
hset key field value # 设置hash key对应的field的value值
hdel key field # 删除hash key对应的field键值对
##
hexists key field # 判断hash key是否存在field
hlen key # 判断hash key 的field数量
##
hmget key field1 field2... # 批量获取hash key的一批field对应的值
hmset key field1 value1 field2 value2 # 批量设置hash key的一批field值
## 慢长命令
hgetall key # 拿到hash key的所有field
hvals key # 拿到所有field的value值
hkeys # 拿到hash key的所有field
## 其他操作
hincrby key field intCounter # 操作哈希中的数字类型增长
hsetnx key field value # 设置hash key对应field的value,若已存在的不存
hincrbyfloat key field floatCounter # 数字类型的浮点增长
列表操作
key:[v1,v2,v3]
### 插入操作
rpush key v1 v2。。。 # 从右侧插入列表
# lpush 从左侧插入
linsert key before|after value newValue # 从元素value的前或后插入新值
lpop key # 从列表左侧弹出一个item
rpop key # 从列表右侧弹出一个item
lrem key count value # 根据count值,从列表中删除value相同的值
# count是负数则从右往左删,为正数从左往右删,为0全删
ltrim key start end # 按照索引范围修剪列表,保留start到end
lrange key start end # 按照索引范围查找列表所有item
lindex key index # 按照索引拿到列表的item
llen # 获取列表长度
blpop key timeout # 阻塞版lpop timeout是阻塞超时时间,timeout=0表示时间无限
brpop key timeout
### 列表实现几种数据结构
## 栈
lpush+lpop
## 队列
lpush+rpop
## 固定大小列表
lpush+ltrim
## 消息队列
lpush+brpop
集合操作
key: {v1,v2,v3...}
## 几乎以s开头表示set操作
sadd key element # 向集合key添加element
srem key element # 从集合中移除element
sismember key element # 计算集合大小
srandmember key count # 从集合中随机取出count个元素
spop key # 从集合中随机弹出一个元素
smembers key # 获取集合中所有元素,无序(长慢命令)
sdiff key1 key2 # 求差集(key1拥有而key2没有的)
sinter key1 key2 # 求交集(key1和key2共同拥有的)
sunion key1 key2 # 求并集(key1和key2所有的元素)
# 集合更多的应用于去重,随机等场景
有序集合操作(zset)
key:{score:value...}
# 特点:通过分值字段来保证顺序
key score value
user:ranking 1 lslsls
user:ranking 99 pspsps
user:ranking 88 systemctl
zadd key score element # 加元素,需要带上score值,score可重复
zrem key element # 移除元素,可以多个同时删除
zscore key element # 查看元素的分数
zincrby key increScore element # 对数字元素进行增值
zcard key # 返回元素的总数
zrank key element # 返回element元素排名
zrange key min max # 按排名返回元素,o(log(n)+m) n是集合规模,m是要获取的值规模
zrange key min max withScore # 返回排名,带分数
zrangebyscore key minScore maxScore # 按分数范围,返回排名元素
zcount key minScore maxScore # 指定分数范围内的的元素个数
zremrangebyrank key start end # 删除指定排名内的元素(升序)
zremrangebyscore key minScore maxScore # 删除指定分数内的升序元素
## 其他命令
zrevrank # 返回某个元素从高到低排序的顺序
zrevrange # 从高到低排序取一定范围
zrevrangebyscore # 返回指定分数范围内的降序元素
zinterstore #对两个有序集合交集
zunionstore #对两个有序集合求并集
redis高级操作
redis慢查询指令
由于redis是单线程架构,所以命令是一个个执行的,如果有长慢命令要执行,会阻塞整个redis。
redis除了通过monitor指令自己去监控排查所有的命令执行外,还提供一种更好的方式,可以记录长慢命令放到慢查询队列,用于后续的排查修改工作。
我们需要配置慢查询的两个参数:
slowlog-max-len
:慢查询队列的长度slowly-log-slower-than
:超过多少微秒,算慢命令,才会被记录到慢查询队列中
直接通过config命令配置:
config set slowlog-log-slower-than 0
config set slowlog-max-len 100
config rewrite # 写了永久生效,如果不写,只是暂时生效
客户端连接redis库可以通过config命令设置一些配置,在这次连接中执行config rewrite
会让这些配置写入配置文件,以后哪怕重启redis服务也生效
查看慢查询队列
slowlog len # 获取慢查询队列长度
slowlog reset # 清空慢查询队列
slowlog get # 获取慢查询队列的所有命令
pipeline与事务
redis拥有管道功能(pipeline),在各个语言的client端中都有对应的实现,如python的redis模块。管道的主要功能是将一批命令,批量打包,在redis服务端批量计算(执行)后,然后把结果批量返回。也就是说n条命令原本所需的n次网络时间,就只用一次网络时间了,这是很大的速度提升。
其次管道的结构中也支持开启事务,可以伴随管道开启,也可以单独开启
python中实现redis的pipeline:
import redis
pool = redis.ConnectionPool(host='127.0.0.1', port=6379) # redis连接池
r = redis.Redis(connection_pool=pool) # 从池子里拿连接
# 创建pipeline管道
pipe = r.pipeline(transaction=True) # 伴随管道开启事务
# 对应单独开启事务的命令
pipe.multi() # pipeline对象执行的命令和redis连接是一样的,只不过在execute时才会执行
pipe.set('name', 'leethon')
pipe.execute() # 执行管道中的所有命令
redis中实现悲观锁和乐观锁
无论是悲观锁还是乐观锁,都是人们定义出来的概念,仅仅是一种思想,与语言无关,用于处理并发安全问题。悲观锁和乐观锁都是并发控制的一种手段。
悲观锁认为在数据操作前,数据会被其他线程的数据操作干扰,所以干脆在操作数据时,取消并发操作,全部串行执行,是中悲观的并发控制
乐观锁则假设最好的情况,每次操作数据都认为不会被其他数据操作干扰,所以不上锁,但是会在更新时判断此期间是否有过修改的情况。
实际上,由于redis是单线程架构,操作数据本身就是串行的,所以不需要人为的去加锁,也就是默认就会有一把悲观锁。不过有可能多个操作是建立了事务的,它们是原子性的,那就需要额外判断了。
ps:可以预见这么一种情况,如果数据操作需要基于上次操作的结果,那么是无法防止一次管道中执行的。
实现乐观锁:
with r.pipeline() as pipe:
pipe.watch('count') # 监听count值
pipe.multi() # 开启事务
time.sleep(10) # 给一些操作时间
pipe.decr('count')
pipe.execute() # 执行时如果count被改了,那么会抛异常,且管道中的操作都不会被执行,抛出异常watcherror
redis发布订阅
# 向一个频道,发送消息
publish channel1 hello
# 订阅频道信息,此客户端会处于监听频道的状态
subscribe channel1
# 查看某个频道有几个订阅者
pubsub numsub lqz
# 列出被订阅的频道
pubsub channels
发布订阅和消息队列的区别:
消息队列是生产消费者模型,生产的一份消息会被一个消费者消化掉
发布订阅则是,发布的一份消息会被所有订阅者收到
bitmap位图
bitmap是字符串类型,由于底层是二进制,我们可以用getbit等方法进行二进制操作。
set xx hi # 设置了一个字符串键值,hi对应的二进制是01101000 01101001
getbit xx offset # 拿到第几位的bit数据,返回的是1或者0
setbit xx offset 1/0 # 设置第几位的bit数据
bitcount xx 【字节start end】# 返回指定字节区间1的数量
bitcount xx 0 0 # 统计第一个字节的1,所以这里返回3
bitcount xx 0 1 # 统计前两个字节的1 ,所以这里返回7
用途,可以用小内存记录大量的状态,如登录用户的登录态,然后做统计。
HyperLogLog
redis中支持在极小的空间完成独立数量统计,采用的算法是HyperLogLog,类似于布隆过滤器。
布隆过滤器是针对于大数据去重的一种特化技术。
它采用多个哈希函数将数据结果映射到极小空间的bit位上,重复出现的数据会得到相同的结果,所以会被去重。
这样的算法是专门用于去重的,但是它在空间和时间上的效率都有极大的提升,不过可能会存在一定的去重误差,因为有可能不同数据执行哈希的结果是相同的,这取决于哈希函数的数量和存储的bit位数。
而HyperLogLog是另外一种算法,也可以做数据的去重,而且可以统计加入其中的数据个数。
pfadd key element # 按照HyperLogLog算法向键里放值
pfcount key # 统计已经向键里提交的元素个数
# 加入的值并不能做存储,所以无法取出
pfadd在成功加入时,返回1,如果有重复的,就不加入,返回0。
我们可以利用这个特性,在存储大量数据而需要去重时,逐个执行pfadd,如果返回1,则记录数据,返回0,则不再重复存储。
我们还可以使用pfcount应用于统计某一时段的活跃量等。
GEO地理位置信息
GEO地理信息定位,存储经纬度,应用于计算两地直线距离,范围内地理信息等。redis就可以存储经纬度。
经纬度信息的作用:
- 统计直线距离
- 通过地图开放api(百度,高德)获得地理信息(餐馆,具体地址名)
- 统计某个距离内的其他经纬度信息
# redis存储
geoadd key 经度 纬度 信息
# 可以用不同的key存不同类型的信息,这些信息甚至可以是关系型数据库的主键信息,看业务逻辑了
geoadd person:loca 116.28 39.55 lee 116.30 38.67 lily
geoadd cities:loca 192.22 23.33 ruisa
# 查看位置信息
geopos cities:loca ruisa
# 计算两个点的距离
geodist cities:loca beijing tianjin km
# 查看附近半径的相关信息
georadiusbymember cities:loca beijing 150 km
redis持久化方案
持久化就是将缓存的数据保存到本地,以便重启后能恢复数据。
redis有两大持久化方案,一种是RDB,类似于快照机制,将当前状态保存到本地,一种AOF,类似于日志机制,恢复时按日志再执行一遍。
RDB
save命令:同步操作(不可能用的,因为会阻塞整个redis),生成rdb持久化文件
bgsave命令:异步操作(不会阻塞其他命令的执行),后台生成rdb持久化文件
配置文件配置持久化策略
save 60 1000
save 300 10
dbfilename dump.rdb
dir "/root/redis-6.2.9/data"
# 如果60s改变了1000条数据,就自动生成rdb
# 如果300s改变了10条数据,就自动生成rdb
# 持久化文件存储的目录为dir后配的,文件名为dump.rdb
在关闭redis-server时,会默认bgsave持久化,也可以跟个nosave来取消持久化
rdb的特点是:
备份会不断的覆盖持久化文件,占用cpu和内存偏多。
如果意外宕机,那可能会丢失不少数据(看rdb存储策略)
我们不会太频繁的执行bgsave,但是频率太低也不行。
AOF
AOF方案是客户端每写一条命令,就记录一条日志,放到日志文件中,这样即使意外宕机也可以做数据完全恢复。
但是它的缺点也很明显,就是重新执行这些指令需要大量的时间。
AOF日志不是直接写到硬盘上,而是先放到缓冲区,再根据一些策略写到硬盘上(因为硬盘太慢了,跟不上redis的命令速度)
这里介绍AOF的三种缓冲策略:
- always:redis--写命令刷新缓冲区--每条命令fsync到硬盘(io开销大)
- everysec:redis--写命令刷新的缓冲区--每秒把缓冲区fsync到硬盘
- no:没有策略,由操作系统决定。
AOF重写:
命令逐步写入,AOF文件会越来越大,通过重写来解决掉过期的,无用的,重复的,可以优化的命令,最终效果就是,占用磁盘空间更小了,恢复时更快了。
# AOF重写配置参数
auto-aof-rewrite-min-size:500m
auto-aof-rewrite-percentage:100
AOF持久化的配置:
appendonly yes #将该选项设置为yes,打开
appendfilename "appendonly.aof" #文件保存的名字
appendfsync everysec # 采用每秒刷缓存到文件的策略
no-appendfsync-on-rewrite yes # 在aof重写的时候,是否要做aof的append操作,(因为aof重写消耗性能,磁盘消耗,正常aof写磁盘有一定的冲突,可能丢失,这段期间的数据,我们就允许它丢失)
混合持久化
同时开启rdb和aof,两个策略可以同时启动。
redis4.x后支持混合持久化,融合了两个策略的一些特点,制定的一个新的策略。
配置参数:
# 开启 aof
appendonly yes
# 开启 aof复写
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# 开启 混合持久化
aof-use-rdb-preamble yes
# 关闭 rdb
save ""
ps:aof重写可以使用配置文件出发,也可以手动触发,bgrewriteaof
缓存优化
redis缓存更新策略
redis的内存空间不够用,但是依然想放入新的键值,那么则需要剔除一部分数据腾出空间,再放新的。
- LRU -Least Recently Used,没有被使用时间最长的
- LFU -Least Frequenty Used,一定时间段内使用次数最少的
- FIFO -First In First Out 先进先出
缓存击穿、雪崩、穿透
缓存穿透:
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解决方案:
- 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
- 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
- 通过HyperLogLog记录上述取不到的键,空间很小
缓存击穿:
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
解决方案:
- 设置热点数据永远不过期
缓存雪崩:
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
- 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
- 设置热点数据永远不过期。