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架构

数据结构和内部编码

五大数据类型还可以继续细分不同的内置编码

image

单线程架构

单线程架构

一个瞬间只会执行一条命令

为什么用了单线程还能这么快

是指单线程下为什么并发还挺高

  1. 纯内存操作
  2. 非阻塞IO (epoll),自身实现了事件处理,不在网络io上浪费过多时间
  3. 避免线程间切换和竞态消耗

注意

  • 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

image

# 查看某个频道有几个订阅者
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的三种缓冲策略:

  1. always:redis--写命令刷新缓冲区--每条命令fsync到硬盘(io开销大)
  2. everysec:redis--写命令刷新的缓冲区--每秒把缓冲区fsync到硬盘
  3. 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机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案:

  • 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  • 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
  • 设置热点数据永远不过期。
posted @ 2023-04-17 16:22  leethon  阅读(51)  评论(0编辑  收藏  举报