Redis

一、安装

默认安装

以Ubuntu为例

1
apt-get install redis-server

这种方式会把Redis安装到/usr/bin/下,但安装的版本可能不是最新的

卸载

使用apt安装的话,卸载很简单:

1
apt-get purge --auto-remove redis-server

源码安装

需要确保已经安装了gcc和tcl

官网下载安装包,放到linux某个目录(这里放到/data目录下)

执行以下命令

1
2
3
4
5
6
7
8
#解压
tar zxf redis-stable.tar.gz
#进入目录
cd redis-stable/
#编译
make -j4
#安装
make install

这样,Redis就安装在了/usr/local/bin/下,但是redis的配置文件还在安装包的目录下(/data/redis-stable/redis.conf)

1
2
3
#将其拷贝至/etc/redis/下
mkdir -p /etc/redis/
cp redis.conf /etc/redis/

卸载

  1. 停止redis服务

  2. 删除/usr/local/bin下的redis文件

    1
    rm -rf /usr/local/bin/redis*
  3. 删除配置文件/etc/redis/redis.conf

  4. 还可进一步删除源码包


服务端运行

Redis运行需要配置文件,而配置文件在/etc/redis/

1
2
#启动redis服务端
redis-server /etc/redis/redis.conf

Redis默认是非daemeon的,可以将配置文件中修改为daemonize yes

客户端运行

1
redis-cli

二、相关知识介绍

redis默认有16(0~15)个数据库,默认使用0号库
通过命令select n来切换数据库

1
2
3
dbsize 		#查看当前数据库的key的数量
flushdb #清空当前库
flushall #清空所有库

redis是单线程+多路IO复用技术

三、常用数据类型

Redis键操作

redis是key:value数据库

1
2
3
4
5
6
keys *		#查看当前库所有key
exists key #判断某个key是否存在
type key #查看key的类型
del key #删除key
expire key 10 #给指定的key设置过期时间,单位秒
ttl key #查看还有多少秒过期,-1永不过期,-2已过期

String

一个key对应一个value

String类型并不表示只能存字符串,比如存储数组[1,2,3],在String中会转换为”[1,2,3]”;也可存整型类型。value最大为512M

  1. set 设置key
1
2
127.0.0.1:6379> set k1 123456
OK
  1. get 查看
1
2
127.0.0.1:6379> get k1
"123456"
  1. append 将给定的值追加到原值的末尾
1
2
3
4
127.0.0.1:6379> append k1 000
(integer) 9
127.0.0.1:6379> get k1
"123456000"
  1. strlen 获得长度
1
2
127.0.0.1:6379> strlen k1
(integer) 9
  1. setnx 只有当key不存在时,设置key的值
1
2
3
4
5
6
#k1存在,返回0失败
127.0.0.1:6379> setnx k1 22
(integer) 0
#k2不存在,设置成功
127.0.0.1:6379> setnx k2 22
(integer) 1
  1. incr 将key中存储的数字+1,只能对数字值进行操作;如果为空(key不存在),新增值为1
1
2
127.0.0.1:6379>  incr k1
(integer) 123456001
  1. decr 将key中存储的数字-1

  2. incrby/decrby 自定义设置key存储的数字增加/减少多少

  1. mset 同时设置一个或多个key-value

  2. mget 同时获得一个或多个value

  3. msetnx 同时设置一个或多个key-value(需所有key都不存在)

  4. getrange <起始位置><结束位置> 获得值的范围

1
2
3
4
127.0.0.1:6379>  set name lingdiand
OK
127.0.0.1:6379> GETRANGE name 0 1
"li"
  1. setrange <起始位置> 用value覆盖key所存储的字符串值,从<起始位置>开始
1
2
3
4
127.0.0.1:6379> SETRANGE name 4 555
(integer) 9
127.0.0.1:6379> get name
"ling555nd"
  1. setex <过期时间> 设置键值的同时设置过期时间,单位秒

  2. getset 设置新值的同时设置旧值


数据结构

String的value数据结构为简单动态字符串(SDS),类似于java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配

image-20211219203612155

capacity为分配的空间,但是可使用的空间为len(即会预留一部分空的空间)

当实际存储的字符串超过len时:

  • 当存储的字符串长度小于1M时,扩容加倍现有的空间
  • 如果超过1M,扩容时只会多扩1M的空间(因为每次都是加倍现有空间的只会越来越大。假如字符串长度为2M,存储为3M。当字符串长度为3M时,存储为4M而不是6M,节省空间)

List

队列 一个key对应多个value

它的底层其实是个双向链表,所以对两端的操作性能很强,对中间节点的操作性能会较差

image-20211219210108097

lpush / rpush <key><value1>…<valuen> 从左边 / 右边插入一个或多个值

1
2
3
4
5
6
7
8
127.0.0.1:6379> lpush k1 aa bb cc dd
(integer) 4
#lpush是左侧添加,第一次aa,第二次bb aa,第三次cc bb aa....
127.0.0.1:6379> lrange k1 0 -1
1) "dd"
2) "cc"
3) "bb"
4) "aa"
  1. lpop / rpop <key> 从左边 / 右边取出一个值(值在键在,值光键亡)

  2. rpoplpush <key1><key2> 从key1右边取出一个值,添加到key2的左边

  3. lrange <key><start><end> 按照索引下标获得元素(从左到右)0 -1 表所有

  4. lindex<key><index> 根据索引下标获得元素(从左到右)

  5. llen<key> 获得列表的长度

  6. linsert before/after 在value前/后插入新值 (根据值)

  7. lrem 从左边开始删除n个value(从左到右)

  8. lset 将列表key下标为index的值替换为value (根据下标)


数据结构

list的数据结构为快速链表quickList

在列表元素较少时会采用一块连续的内存存储,所有元素紧挨在一起存储,这个结构是ziplist(类似于数组)

当列表元素较多时会改为quicklist(链表),一个quicklist由多个ziplist组成

image-20211220220445575

Redis将链表和ziplist结合起来组成了quicklist,即将多个ziplist使用双向指针串起来,这样既满足了快速的插入删除性能,又不会出现太大的空间冗余

Set

Set和list类似,不同点在于set不允许重复值,是无序的。 如果需要存储列表数据,并不希望出现重复数据,可使用Set,例:关注人
Set的底层是一个value为null的哈希表,对于添加、删除、查找的复杂度都是O(1)

  1. sadd <key><value1><value2>….. 将一个或多个元素添加到key中,相同元素将被忽略。

  2. smembers <key> 查看该集合中所有值。

  3. sismember <key><value> 判断key中是否存在value,有1,没有0。

  4. scard <key> 返回该集合中的元素个数。

  5. srem <key><value><value2>…. 删除该集合中的某个元素。

  6. spop <key> 随机取出该集合中一个值。

  7. srandmember <key><n> 随机查看该集合中n个值。

  8. smove <key1><key2><value> 将key1中value移动到key2中。

  9. sinter <key1><key2> 输出两个集合的交集元素

  10. sunion <key1><key2> 输出两个集合的并集元素

  11. sdiff <key1><key2> 输出两个集合中的差集元素(不存在于key2中的key1元素)


数据结构

Set的数据结构是dict字典,字典是采用哈希表进行实现的。这就说明了为什么Set的元素不会重复,加入Set时会根据元素的值计算出元素的下标,相同的元素那么下标也会相同

Zset

Zset与set类似,都是没有重复元素的,不同在于Zset每个元素都关联了一个评分(score),score被用来按照从低到高排序每个元素,因为元素是有序的,所以也可根据score或次序(position)获取一个范围内容的元素

  1. zadd <key><score1><value1><score2><value2> 将一个或多个元素及其score添加到key中
1
127.0.0.1:6379> zadd top 1 java 2 c++ 100 php 5 mysql
  1. zrange <key><start><stop>[withscores] 按照索引下标输出元素。0 -1 表所有,withscores可以带上score一起返回
1
127.0.0.1:6379> zrange top 0 -1 withscores
  1. zrangebyscore <key> <min> <max> [withscores] 返回某个score范围内容的值

  2. zrevrangebyscore <key> <max> <min> [withscores] 同上,从大到小排序

  3. zincrby <key><n><value> 为元素的score加上n

  4. zrem <key><value> 删除key中指定的value

  5. zcount <key><min><max> 统计该区间的元素个数

  6. zrank <key><value> 返回该value在集合中的排名,从0开始


数据结构

Zset的数据结构很特别,一方面它等价于Java的Map<String,Double>,String是value,Double是score,可以给每个value赋值一个权重score,另一方面它又类似于TreeSet,value根据权重score进行排序。

Zset底层使用了两个数据结构

  1. hash,hash的作用在于关联value和score

    image-20220422175117494

  2. 跳跃表,目的在于给value排序,根据score的范围获取元素列表

    跳跃表(skipList)是一种可以替代平衡树的数据结构,跳跃表让已排序的数据分布在多层次的链表结构中,默认是将 Key 值升序排列的,以 0-1 的随机值决定一个数据是否能够攀升到高层次的链表中。它通过容许一定的数据冗余,达到 “以空间换时间” 的目的。

    一个跳跃表有若干层链表组成;

    最底层包含所有数据;

    如果一个元素出现在第 i 层,那么比 i 小的层都包含该元素;

    第 i 层的元素通过一个指针指向下一层拥有相同值的元素;

    image-20220422180420166

    例:要查找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适合存储对象

image-20220422102933473

这样通过key(用户的ID)+field(属性标签)可操作对应的属性数据

  1. hset <key><field><value> 给key中的field赋值value
1
127.0.0.1:6379> hset user:1001 id 1001 
  1. hget <key><field> 从key中取出field的值
1
2
127.0.0.1:6379> hget user:1001 id
"1001"
  1. hmset <key><field1><value1><field2><value2>… 批量设置hash值

  2. hexists <key><field> 判断是否存在field

  3. hkeys <key> 查看key中所有field

  4. hvals <key> 查看key中所有value

  5. hincrby <key><field><n> 为key中field的值加上n

  6. hsetnx <key><field><value> 当field不存在时,将field的值设为value


数据结构

hash的数据结构为ziplist和hashtable。当field-value长度较短、个数较少时,使用ziplist,否则使用hashtable

四、配置文件

启动redis时需要配置文件redis.conf

Units单位

配置大小单位,只支持bytes,不支持bit,大小写不敏感

image-20220423140508697

include

当前文件也可包含其他配置文件

image-20220423140611472

网络配置

image-20220423140728527

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安全

密码

image-20220423142831659

默认是没有密码,将其注释取消掉

也可以在命令种临时设置密码(重启后,密码就还原了)

image-20220423143740906

LIMITS限制

maxclients

最多可以同时与多少个客户端连接,默认1000

maxmemory

最大内存

五、发布订阅

什么是发布订阅

发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接受消息。

  1. redis客户端可以订阅任意数量的频道

image-20220423161020465

  1. 当这个频道发布消息后,消息就会发送给订阅的客户端

image-20220423162828549

发布订阅实现

  1. 打开一个客户端订阅channel1
1
2
3
4
5
127.0.0.1:6379> SUBSCRIBE channel1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel1"
3) (integer) 1
  1. 打开另一个客户端,通过channel1发布消息hello
1
2
127.0.0.1:6379> PUBLISH channel1 hello
(integer) 1

​ 返回的1为订阅者数量

  1. 打开第一个客户端可以看到发送的消息
1
2
3
1) "message"
2) "channel1"
3) "hello"

六、新数据类型

redis在新版本中出现了几个新的类型

Bitmaps

进行位操作

现代计算机采用二进制(位),1个字节=8位。

“abc”由3个字节组成,在计算机存储时采用二进制位表示,abc的ASCII码为97、98、99,二进制位是01100001、01100010、01100011

所以通过操作位能够有效提高使用效率,相当于跳过了字节转换为位,直接操作计算机最底层存储数据。

  1. Bitmaps本身不是一种数据结构,实际上它是字符串(key-value),但它可以对字符串进行位操作。
  2. Bitmaps单独提供了一套命令,所以命令不同与其他数据类型。可以把Bitmaps当成一个以位为单位的数组,只能存储0和1,下标在Bitmaps叫做偏移量。

image-20220423164443286


命令

  1. setbit <key><offset><value> 设置Bitmaps中某个偏移量的值(0或1)

    例:每个用户是否访问过网站存放在Bitmaps中,访问过记做1,没访问过记做0,偏移量作为用户id。

    1
    127.0.0.1:6379> setbit users:20220423 1 1
  2. getbit <key><offset> 获取某个偏移量的值。因为5不存在,返回也是0

    1
    2
    3
    4
    127.0.0.1:6379> getbit users:20220423 1
    (integer) 1
    127.0.0.1:6379> getbit users:20220423 5
    (integer) 0
  3. bitcount <key>[start end] 统计字符串从start到end范围内比特值为1的数量。start、end可使用负值,-1最后一位,-2倒数第二位

  4. 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个不重复元素)


命令

  1. pfadd [element…] 添加元素到HyperLogLog中

    1
    2
    127.0.0.1:6379> pfadd h1 redis
    (integer) 1
  2. pfcount [key….] 计算key的近似基数

  3. pfmerge [sourcekey…] 将一个或多个sourcekey存在destkey中,比如每月活跃用户可以用每天的活跃用户合并可得

Geospatial

GEO:Geographic,地理信息的缩写。

该类型是元素的二维坐标,redis基于该类型,提供了经纬度设置、查询等


命令

  1. geoadd <key><longitude><latitude><member> 添加地理位置(经度,纬度,名称)

    1
    2
    127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
    (integer) 1
  2. geopos [member…] 获得指定地区的坐标值

    1
    2
    3
    127.0.0.1:6379> geopos china:city shanghai
    1) 1) "121.47000163793563843"
    2) "31.22999903975783553"
  3. geodist [m|km]获得两个地区位置之间的直线距离

  4. georadius <longitude><latitude> m|km 以给定的经纬度为中心,找出某一半径内的元素

七、Jedis

1
2
3
4
5
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
1
2
3
4
5
6
7
public static void main(String ...arg){
//创建客户端
Jedis jedis=new Jedis("121.196.221.116",6379);
//测试
String test=jedis.ping();
System.out.println(test);
}

注:远程访问需要将配置文件中bind注释掉,protected-mode改为no

八、事务操作

redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序执行。事务在执行过程中,不会被其他客户端发送来的命令请求所打断

redis事务的主要作用就是串联多个命令防止别的命令插队

Multi、Exec、discard

首先输入multi,之后输入的命令将会依次进入命令队列中等待,输入exec后,redis会将命令队列中的命令依次执行

在输入命令的过程中假如输错了、少输了,可以使用discard放弃输入阶段

image-20220526173936339

  1. 命令队列中某个命令出现错误(明显的编译错误),则所有命令都会取消
  2. 执行阶段某个命令出现错误(运行时错误),则只有错误命令不执行

事务冲突

例:同一个银行卡,有很多人去消费,如何解决数据冲突问题

通过上锁的机制解决

悲观锁

每次操作前将数据上锁

每次操作数据时都认为别人会修改,所以就提前上锁。传统的关系型数据库里面就用了悲观锁机制,比如行锁、表锁、读锁、写锁,每次操作前上锁

乐观锁

每次操作数据时都认为别人不会修改,所以不会上锁,而是在更新时判断一下别人有没有更新这个数据,可以使用版本号等机制。乐观锁用于多读的应用类型,可以提高吞吐量,Redis就是采用这个check-and-set机制实现事务的、还有抢票

例:10000元,版本号1.0,A操作后变成了2000元,版本号1.1;B操作时检查判断版本号等不等于1.1,不等的话重新更新数据

WATCH

在执行multi之前,先执行watch key1 [key2]可以监视一个或多个key,如果在事务执行之前这些key被其他命令所改动,那么事务将被打断

UNWATCH

取消watch命令对所有key的监视

事务三特性

  1. 单独的隔离操作

    事务中所有命令都会序列化、顺序执行。事务在执行的过程中,不会被其他客户端发送的命令所打断

  2. 没有隔离级别的概念

    队列中的命令没有提前之前都不会实际执行

  3. 不保证原子性

    事务中如果有一条命令执行失败,其后命令任然执行,没有回滚

持久化之RDB

redis的数据都在内存中,但也可以在硬盘中,将内存中的数据写入硬盘中的过程就是持久化

RDB介绍

RDB(Redis DataBase)指的是在指定的时间间隔内将内存中的数据集快照写入磁盘。恢复是将快照文件直接读到内存里。RDB默认开启

备份是如何执行的

redis持久化后生成的快照文件为dump.rdb,在备份的过程中并不是直接覆盖原文件

redis会单独创建(fork)一个子进程进行持久化,先将数据写入到一个临时文件中,等到持久化结束了,用这个临时文件替换上次持久化好的文件。在整个过程中,主进程不进行任何IO操作,这就确保了极高的性能,如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB比AOF更高效,RDB的缺点是最后一次持久化的数据可能会丢失

image-20220530102155328

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 ./:备份文件目录

优缺点

优点

  1. 适合大规模的数据恢复
  2. 适合对数据完整性和一致性要求不高
  3. 节省磁盘空间
  4. 恢复速度块

缺点

  1. fork时,内存数据被克隆了一份,大致2倍的膨胀性
  2. 虽然redis在fork时使用了写时拷贝技术,但数据庞大时还是比较消耗性能
  3. 在备份周期每个一段时间做一次备份,如果redis意外挂掉,就会丢失最后一次快照后的所有修改

持久化之AOF

AOF介绍

AOF(Append Only File)以日志的形式记录每个写操作,将redis执行过的所有写指令记录下来(读操作不记录),只需追加文件不可以改写文件。redis在启动之初会读取该文件重新构建数据,即redis会调用日志文件的命令从前到后执行一次以完成数据的恢复工作

AOF默认不开启,可以在redis.conf配置,appendonly no改为yes开启,默认存储文件名为appendonly.aof

如果RDB和AOF同时开启,系统默认取AOF

AOF持久化流程

  1. 客户端的请求写命令被append追加到AOF缓冲区中
  2. AOF缓冲区根据配置文件策略(always、everysec、no)将操作同步到磁盘的AOF文件中
  3. AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩文件容量大小

AOF启动/修复/恢复

AOF的机制与RDB不同,但备份和恢复的操作与RDB一样,都是拷贝备份文件,需要恢复时再拷贝至redis工作目录下,启动系统即加载

正常恢复:

  • 开启AOF:修改appendonly noyes
  • 将aof文件放在对应目录下(查看目录:config get dir)
  • 重启redis,会自动加载aof文件的数据

异常恢复:

  • 开启AOF:修改appendonly noyes
  • 如遇到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
2
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

系统载入时或上次重写完毕后,会记录此时AOF大小,记为base_size

当前大小>=base_size+base_size*100%,且>=64mb时重写

重写过程

  1. bgwriteaof触发重写,判断是否有bgsave、bgwriteaof在运行,有的话等待该命令结束再执行

  2. 主进程fork子进程执行重写操作,保证主进程不会阻塞

  3. 子进程遍历redis数据到临时文件中,同时客户端的写请求写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区,以保证原AOF文件完整性和新AOF文件在生成期间的新的数据修改动作不会丢失

  4. 子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。把aof_rewrite_buf中的数据写入新的AOF文件中

  5. 使用新的AOF文件覆盖旧的,完成重写


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!