Redis
一、安装
默认安装
以Ubuntu为例
1 |
|
这种方式会把Redis安装到/usr/bin/
下,但安装的版本可能不是最新的
卸载
使用apt安装的话,卸载很简单:
1 |
|
源码安装
需要确保已经安装了gcc和tcl
官网下载安装包,放到linux某个目录(这里放到/data目录下)
执行以下命令
1 |
|
这样,Redis就安装在了/usr/local/bin/
下,但是redis的配置文件还在安装包的目录下(/data/redis-stable/redis.conf)
1 |
|
卸载
停止redis服务
删除/usr/local/bin下的redis文件
1
rm -rf /usr/local/bin/redis*
删除配置文件
/etc/redis/redis.conf
还可进一步删除源码包
服务端运行
Redis运行需要配置文件,而配置文件在/etc/redis/
下
1 |
|
Redis默认是非daemeon的,可以将配置文件中修改为daemonize yes
客户端运行
1 |
|
二、相关知识介绍
redis默认有16(0~15)个数据库,默认使用0号库
通过命令select n
来切换数据库
1 |
|
redis是单线程+多路IO复用技术
三、常用数据类型
Redis键操作
redis是key:value数据库
1 |
|
String
一个key对应一个value
String类型并不表示只能存字符串,比如存储数组[1,2,3],在String中会转换为”[1,2,3]”;也可存整型类型。value最大为512M
- set
设置key
1 |
|
- get
查看
1 |
|
- append
将给定的值追加到原值的末尾
1 |
|
- strlen
获得长度
1 |
|
- setnx
只有当key不存在时,设置key的值
1 |
|
- incr
将key中存储的数字+1,只能对数字值进行操作;如果为空(key不存在),新增值为1
1 |
|
decr
将key中存储的数字-1 incrby/decrby
自定义设置key存储的数字增加/减少多少
mset
… 同时设置一个或多个key-value mget
… 同时获得一个或多个value msetnx
… 同时设置一个或多个key-value(需所有key都不存在) getrange
<起始位置><结束位置> 获得值的范围
1 |
|
- setrange
<起始位置> 用value覆盖key所存储的字符串值,从<起始位置>开始
1 |
|
setex
<过期时间> 设置键值的同时设置过期时间,单位秒 getset
设置新值的同时设置旧值
数据结构
String的value数据结构为简单动态字符串(SDS),类似于java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配
capacity为分配的空间,但是可使用的空间为len(即会预留一部分空的空间)
当实际存储的字符串超过len时:
- 当存储的字符串长度小于1M时,扩容加倍现有的空间
- 如果超过1M,扩容时只会多扩1M的空间(因为每次都是加倍现有空间的只会越来越大。假如字符串长度为2M,存储为3M。当字符串长度为3M时,存储为4M而不是6M,节省空间)
List
队列 一个key对应多个value
它的底层其实是个双向链表,所以对两端的操作性能很强,对中间节点的操作性能会较差
lpush / rpush <key><value1>…<valuen> 从左边 / 右边插入一个或多个值
1 |
|
lpop / rpop <key> 从左边 / 右边取出一个值(值在键在,值光键亡)
rpoplpush <key1><key2> 从key1右边取出一个值,添加到key2的左边
lrange <key><start><end> 按照索引下标获得元素(从左到右)0 -1 表所有
lindex<key><index> 根据索引下标获得元素(从左到右)
llen<key> 获得列表的长度
linsert
before/after 在value前/后插入新值 (根据值) lrem
从左边开始删除n个value(从左到右) lset
将列表key下标为index的值替换为value (根据下标)
数据结构
list的数据结构为快速链表quickList
在列表元素较少时会采用一块连续的内存存储,所有元素紧挨在一起存储,这个结构是ziplist(类似于数组)
当列表元素较多时会改为quicklist(链表),一个quicklist由多个ziplist组成
Redis将链表和ziplist结合起来组成了quicklist,即将多个ziplist使用双向指针串起来,这样既满足了快速的插入删除性能,又不会出现太大的空间冗余
Set
Set和list类似,不同点在于set不允许重复值,是无序的。 如果需要存储列表数据,并不希望出现重复数据,可使用Set,例:关注人
Set的底层是一个value为null的哈希表,对于添加、删除、查找的复杂度都是O(1)
sadd <key><value1><value2>….. 将一个或多个元素添加到key中,相同元素将被忽略。
smembers <key> 查看该集合中所有值。
sismember <key><value> 判断key中是否存在value,有1,没有0。
scard <key> 返回该集合中的元素个数。
srem <key><value><value2>…. 删除该集合中的某个元素。
spop <key> 随机取出该集合中一个值。
srandmember <key><n> 随机查看该集合中n个值。
smove <key1><key2><value> 将key1中value移动到key2中。
sinter <key1><key2> 输出两个集合的交集元素
sunion <key1><key2> 输出两个集合的并集元素
sdiff <key1><key2> 输出两个集合中的差集元素(不存在于key2中的key1元素)
数据结构
Set的数据结构是dict字典,字典是采用哈希表进行实现的。这就说明了为什么Set的元素不会重复,加入Set时会根据元素的值计算出元素的下标,相同的元素那么下标也会相同
Zset
Zset与set类似,都是没有重复元素的,不同在于Zset每个元素都关联了一个评分(score),score被用来按照从低到高排序每个元素,因为元素是有序的,所以也可根据score或次序(position)获取一个范围内容的元素
- zadd <key><score1><value1><score2><value2> 将一个或多个元素及其score添加到key中
1 |
|
- zrange <key><start><stop>[withscores] 按照索引下标输出元素。0 -1 表所有,withscores可以带上score一起返回
1 |
|
zrangebyscore <key> <min> <max> [withscores] 返回某个score范围内容的值
zrevrangebyscore <key> <max> <min> [withscores] 同上,从大到小排序
zincrby <key><n><value> 为元素的score加上n
zrem <key><value> 删除key中指定的value
zcount <key><min><max> 统计该区间的元素个数
zrank <key><value> 返回该value在集合中的排名,从0开始
数据结构
Zset的数据结构很特别,一方面它等价于Java的Map<String,Double>,String是value,Double是score,可以给每个value赋值一个权重score,另一方面它又类似于TreeSet,value根据权重score进行排序。
Zset底层使用了两个数据结构
hash,hash的作用在于关联value和score
跳跃表,目的在于给value排序,根据score的范围获取元素列表
跳跃表(skipList)是一种可以替代平衡树的数据结构,跳跃表让已排序的数据分布在多层次的链表结构中,默认是将 Key 值升序排列的,以 0-1 的随机值决定一个数据是否能够攀升到高层次的链表中。它通过容许一定的数据冗余,达到 “以空间换时间” 的目的。
一个跳跃表有若干层链表组成;
最底层包含所有数据;
如果一个元素出现在第 i 层,那么比 i 小的层都包含该元素;
第 i 层的元素通过一个指针指向下一层拥有相同值的元素;
例:要查找51,首先在最高层查找:1比51小,指针右移,21比51小,指针右移,为null,第二层找完了也没有,指针左移,下移到下一层下一个节点,41比51小,指针右移,61比51大,指针左移,下移到下一层下一个节点找到51
Hash
hash是一个键值对集合,类似于Java中的Map<String,Object>,其中String为键(field),Object为value。
例:用户id为key,value为用户对象数据(name、age、sex)。hash适合存储对象
这样通过key(用户的ID)+field(属性标签)可操作对应的属性数据
- hset <key><field><value> 给key中的field赋值value
1 |
|
- hget <key><field> 从key中取出field的值
1 |
|
hmset <key><field1><value1><field2><value2>… 批量设置hash值
hexists <key><field> 判断是否存在field
hkeys <key> 查看key中所有field
hvals <key> 查看key中所有value
hincrby <key><field><n> 为key中field的值加上n
hsetnx <key><field><value> 当field不存在时,将field的值设为value
数据结构
hash的数据结构为ziplist和hashtable。当field-value长度较短、个数较少时,使用ziplist,否则使用hashtable
四、配置文件
启动redis时需要配置文件redis.conf
Units单位
配置大小单位,只支持bytes,不支持bit,大小写不敏感
include
当前文件也可包含其他配置文件
网络配置
bind
默认bind=127.0.0.1,表示访问redis只能通过127.0.0.1访问,即本地访问
protected-mode
保护模式,默认yes打开,不运行远程访问。如需要远程访问,将bind注释掉,protected-mode改为no
port
端口
tcp-backlog
设置tcp的backlog,backlog是一个连接队列,backlog队列总和=未完成三次握手队列+已完成三次握手队列。
默认为511次,如需要高并发,需要一个高bakclog值来避免客户端连接慢的问题
timeout
超时时间(秒),默认0(永不超时)
连接redis后多少秒后没操作会断开连接
tcp-keepalive
检查心跳时间,默认300秒。即每隔300秒redis服务端向客户端发送一次请求,检查客户端是否还活着。
GENERAL通用
daemonize
Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
pidfile
存放pid文件的位置,每个实例都会产生一个不同的pid文件
当Redis以守护进程方式运行时,Redis默认会把pid写入/var/run/redis.pid文件,可以通过pidfile指定
loglevel
日志级别
logfile
日志文件输出路径,默认为空
databases
数据库
SECURITY安全
密码
默认是没有密码,将其注释取消掉
也可以在命令种临时设置密码(重启后,密码就还原了)
LIMITS限制
maxclients
最多可以同时与多少个客户端连接,默认1000
maxmemory
最大内存
五、发布订阅
什么是发布订阅
发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接受消息。
- redis客户端可以订阅任意数量的频道
- 当这个频道发布消息后,消息就会发送给订阅的客户端
发布订阅实现
- 打开一个客户端订阅channel1
1 |
|
- 打开另一个客户端,通过channel1发布消息hello
1 |
|
返回的1为订阅者数量
- 打开第一个客户端可以看到发送的消息
1 |
|
六、新数据类型
redis在新版本中出现了几个新的类型
Bitmaps
进行位操作
现代计算机采用二进制(位),1个字节=8位。
“abc”由3个字节组成,在计算机存储时采用二进制位表示,abc的ASCII码为97、98、99,二进制位是01100001、01100010、01100011
所以通过操作位能够有效提高使用效率,相当于跳过了字节转换为位,直接操作计算机最底层存储数据。
- Bitmaps本身不是一种数据结构,实际上它是字符串(key-value),但它可以对字符串进行位操作。
- Bitmaps单独提供了一套命令,所以命令不同与其他数据类型。可以把Bitmaps当成一个以位为单位的数组,只能存储0和1,下标在Bitmaps叫做偏移量。
命令
setbit <key><offset><value> 设置Bitmaps中某个偏移量的值(0或1)
例:每个用户是否访问过网站存放在Bitmaps中,访问过记做1,没访问过记做0,偏移量作为用户id。
1
127.0.0.1:6379> setbit users:20220423 1 1
getbit <key><offset> 获取某个偏移量的值。因为5不存在,返回也是0
1
2
3
4127.0.0.1:6379> getbit users:20220423 1
(integer) 1
127.0.0.1:6379> getbit users:20220423 5
(integer) 0bitcount <key>[start end] 统计字符串从start到end范围内比特值为1的数量。start、end可使用负值,-1最后一位,-2倒数第二位
bittop and/or/not/xor <destkey> [key…] 求多个Bitmaps的and(交集)、or(并集)、not(非)、xor(异或),将结果存在destkey中
HyperLogLog
HyperLogLog用来做基数统计(求集合中不重复元素个数)的算法,优点在于速度快、运行效率高,计算基数所需的空间总是固定的
需要注意的是HyperLogLog只是用来计算基数,不能存储
什么是基数
数据集{1,3,5,7,5,7,8},基数集为{1,3,5,7,8},基数就是5(有5个不重复元素)
命令
pfadd
[element…] 添加元素到HyperLogLog中 1
2127.0.0.1:6379> pfadd h1 redis
(integer) 1pfcount
[key….] 计算key的近似基数 pfmerge
[sourcekey…] 将一个或多个sourcekey存在destkey中,比如每月活跃用户可以用每天的活跃用户合并可得
Geospatial
GEO:Geographic,地理信息的缩写。
该类型是元素的二维坐标,redis基于该类型,提供了经纬度设置、查询等
命令
geoadd <key><longitude><latitude><member> 添加地理位置(经度,纬度,名称)
1
2127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1geopos
[member…] 获得指定地区的坐标值 1
2
3127.0.0.1:6379> geopos china:city shanghai
1) 1) "121.47000163793563843"
2) "31.22999903975783553"geodist
[m|km]获得两个地区位置之间的直线距离 georadius
<longitude><latitude> m|km 以给定的经纬度为中心,找出某一半径内的元素
七、Jedis
1 |
|
1 |
|
注:远程访问需要将配置文件中bind注释掉,protected-mode改为no
八、事务操作
redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序执行。事务在执行过程中,不会被其他客户端发送来的命令请求所打断
redis事务的主要作用就是串联多个命令防止别的命令插队
Multi、Exec、discard
首先输入multi
,之后输入的命令将会依次进入命令队列中等待,输入exec
后,redis会将命令队列中的命令依次执行
在输入命令的过程中假如输错了、少输了,可以使用discard
放弃输入阶段
- 命令队列中某个命令出现错误(明显的编译错误),则所有命令都会取消
- 执行阶段某个命令出现错误(运行时错误),则只有错误命令不执行
事务冲突
例:同一个银行卡,有很多人去消费,如何解决数据冲突问题
通过上锁的机制解决
悲观锁
每次操作前将数据上锁
每次操作数据时都认为别人会修改,所以就提前上锁。传统的关系型数据库里面就用了悲观锁机制,比如行锁、表锁、读锁、写锁,每次操作前上锁
乐观锁
每次操作数据时都认为别人不会修改,所以不会上锁,而是在更新时判断一下别人有没有更新这个数据,可以使用版本号等机制。乐观锁用于多读的应用类型,可以提高吞吐量,Redis就是采用这个check-and-set机制实现事务的、还有抢票
例:10000元,版本号1.0,A操作后变成了2000元,版本号1.1;B操作时检查判断版本号等不等于1.1,不等的话重新更新数据
WATCH
在执行multi
之前,先执行watch key1 [key2]可以监视一个或多个key,如果在事务执行之前这些key被其他命令所改动,那么事务将被打断
UNWATCH
取消watch
命令对所有key的监视
事务三特性
单独的隔离操作
事务中所有命令都会序列化、顺序执行。事务在执行的过程中,不会被其他客户端发送的命令所打断
没有隔离级别的概念
队列中的命令没有提前之前都不会实际执行
不保证原子性
事务中如果有一条命令执行失败,其后命令任然执行,没有回滚
持久化之RDB
redis的数据都在内存中,但也可以在硬盘中,将内存中的数据写入硬盘中的过程就是持久化
RDB介绍
RDB(Redis DataBase)指的是在指定的时间间隔内将内存中的数据集快照写入磁盘。恢复是将快照文件直接读到内存里。RDB默认开启
备份是如何执行的
redis持久化后生成的快照文件为dump.rdb
,在备份的过程中并不是直接覆盖原文件
redis会单独创建(fork)一个子进程进行持久化,先将数据写入到一个临时文件中,等到持久化结束了,用这个临时文件替换上次持久化好的文件。在整个过程中,主进程不进行任何IO操作,这就确保了极高的性能,如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB比AOF更高效,RDB的缺点是最后一次持久化的数据可能会丢失
Fork命令
fork的作用是复制一个与当前进一样的进程,包括数据(变量、环境变量、程序计数器等),并作为原进程的子进程
在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后会用exec系统调用,出于效率考虑,linux引入了“写时复制技术”
父子进程共用一段物理内存,只有当进程空间内各段数据要改变时,会将父进程内容复制一份给子进程
配置文件
在redis.conf配置文件中,备份文件默认为dump.rdb
save 秒 key
:指定持久化操作(多少秒内有几个key改变了)的触发操作(save只管保存,会阻塞,不建议设置)
bgsave
:redis会在后台异步进行快照操作,快照同时还可以响应客户端请求,还可以使用lastsave命令获取最后一次成功执行快照的时间
stop-writes-on-bgsave-error yes
:当redis无法写入磁盘,就会关掉redis的写操作
rdbcompression yes
:持久化的文件是否进行压缩,压缩使用LZF算法
rdbchecksum yes
:存储快照后,使用CRC64算法进行数据校验
dbfilename dump.rdb
:备份文件名称
dir ./
:备份文件目录
优缺点
优点
- 适合大规模的数据恢复
- 适合对数据完整性和一致性要求不高
- 节省磁盘空间
- 恢复速度块
缺点
- fork时,内存数据被克隆了一份,大致2倍的膨胀性
- 虽然redis在fork时使用了写时拷贝技术,但数据庞大时还是比较消耗性能
- 在备份周期每个一段时间做一次备份,如果redis意外挂掉,就会丢失最后一次快照后的所有修改
持久化之AOF
AOF介绍
AOF(Append Only File)以日志的形式记录每个写操作,将redis执行过的所有写指令记录下来(读操作不记录),只需追加文件不可以改写文件。redis在启动之初会读取该文件重新构建数据,即redis会调用日志文件的命令从前到后执行一次以完成数据的恢复工作
AOF默认不开启,可以在redis.conf配置,appendonly no
改为yes
开启,默认存储文件名为appendonly.aof
如果RDB和AOF同时开启,系统默认取AOF
AOF持久化流程
- 客户端的请求写命令被append追加到AOF缓冲区中
- AOF缓冲区根据配置文件策略(always、everysec、no)将操作同步到磁盘的AOF文件中
- AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩文件容量大小
AOF启动/修复/恢复
AOF的机制与RDB不同,但备份和恢复的操作与RDB一样,都是拷贝备份文件,需要恢复时再拷贝至redis工作目录下,启动系统即加载
正常恢复:
- 开启AOF:修改
appendonly no
为yes
- 将aof文件放在对应目录下(查看目录:config get dir)
- 重启redis,会自动加载aof文件的数据
异常恢复:
- 开启AOF:修改
appendonly no
为yes
- 如遇到AOF文件损坏,通过
/usr/local/bin/redis-check-aof --fix appendonly.aof
进行恢复 - 将修复后的aof文件放在对应目录下(查看目录:config get dir)
- 重启redis,会自动加载aof文件的数据
配置文件
AOF同步频率设置
appendfsync always
:始终同步。每次写操作立刻记入日志,性能较差但数据完整性好
appendfsync everysec
:每秒同步。每秒计入日志一次
appendfsync no
:redis不主动进行同步,将同步时机交给操作系统
Rewrite压缩
AOF采用文件追加方式,文件会越来越大,因此增加了重写机制,当AOF文件大小超过设定的阈值时,redis就会对AOF文件进行压缩,只保留可以恢复数据的最小指令集,可以使用命令bgrewriteaof
实现过程:AOF文件过大时,会fork出一条新进程将文件重写(也是先对临时文件重写再替换),redis4.0之后的重写,是将RDB的快照,以二进制的形式附在新的AOF头部,作为已有的历史数据,替换掉原来的写操作。
触发条件:redis会记录上次重写时AOF的大小,默认为当前AOF文件大小是上次重写后的一倍且大于64M时触发
auto-aof-rewrite-percentage
:设置重写的基准值,100指文件是原来文件的2倍时
auto-aof-rewrite-min-size
:设置重写的大小,最小文件为64MB,达到这个值开始重写
1 |
|
系统载入时或上次重写完毕后,会记录此时AOF大小,记为base_size
当前大小>=base_size+base_size*100%,且>=64mb时重写
重写过程:
bgwriteaof触发重写,判断是否有bgsave、bgwriteaof在运行,有的话等待该命令结束再执行
主进程fork子进程执行重写操作,保证主进程不会阻塞
子进程遍历redis数据到临时文件中,同时客户端的写请求写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区,以保证原AOF文件完整性和新AOF文件在生成期间的新的数据修改动作不会丢失
子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。把aof_rewrite_buf中的数据写入新的AOF文件中
使用新的AOF文件覆盖旧的,完成重写
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!