本系列文章:
Redis进阶之路(一)5种数据类型、Redis常用命令
Redis学习之路(二)Jedis、持久化、事务
Redis学习之路(三)主从复制、键过期删除策略、内存溢出策略、慢查询
Redis学习之路(四)哨兵、集群、读写分离
Redis学习之路(五)缓存、分布式锁
一、Redis客户端
Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性
。Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
Redis的客户端有很多,官方推荐使用Redisson。但Jedis功能更丰富,所以接下来以Jedis为例。
Redis客户端与服务端之间的通信协议是在TCP协议之上构建的。
1.1 Jedis的基本使用
1.1.1 Jedis的简单使用
创建一个SpringBoot项目,引入jedis的依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.2</version>
</dependency>
然后就可以查看jedis的使用,示例:
//生成一个 Jedis 对象,这个对象和指定 Redis 实例进行通信
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.set("hello", "world");
System.out.println(jedis.get("hello")); //world
以上代码中使用的Jedis构造方法中,初始化了Redis实例的IP和端口。除此之外,还有一个包含了四个参数的构造函数是比较常用的:
Jedis(final String host, final int port, final int connectionTimeout,
final int soTimeout)
4个参数的意义:
host
:Redis实例的所在机器的IP。
port
:Redis实例的端口。
connectionTimeout
:客户端连接超时。
soTimeout
:客户端读写超时。
在实际使用Jedis时,肯定要注意关闭流之类的操作:
Jedis jedis = null;
try {
jedis = new Jedis("127.0.0.1", 6379);
System.out.println(jedis.get("hello"));
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
//关闭流
if (jedis != null) {
jedis.close();
}
}
Jedis对于Redis五种数据结构的简单操作示例:
Jedis jedis = new Jedis("127.0.0.1", 6379);
// 1.string
jedis.set("hello", "world");
System.out.println(jedis.get("hello")); //world
// 2.hash
jedis.hset("myhash", "f1", "v1");
jedis.hset("myhash", "f2", "v2");
System.out.println(jedis.hgetAll("myhash")); //{f2=v2, f1=v1}
// 3.list
jedis.rpush("mylist", "1");
jedis.rpush("mylist", "2");
jedis.rpush("mylist", "3");
System.out.println(jedis.lrange("mylist", 0, -1)); //[1, 2, 3]
// 4.set
jedis.sadd("myset", "a");
jedis.sadd("myset", "b");
jedis.sadd("myset", "a");
System.out.println(jedis.smembers("myset")); //[a, b]
// 5.zset
jedis.zadd("myzset", 99, "tom");
jedis.zadd("myzset", 66, "peter");
jedis.zadd("myzset", 33, "james");
System.out.println(jedis.zrange("myzset", 0, -1)); //[james, peter, tom]
1.1.2 Jedis常用API
- 1、Jedis中对键通用的操作
方法 | 描述 | 返回值 /补充说明 |
---|---|---|
boolean exists(String key) |
判断某个键是否存在 | |
set(String key,String value) |
新增键值对(key,value) | 返回String类型的OK代表成功 |
Set< String > jedis.keys(*) |
获取所有key | 返回set 无序集合 |
del(String key) |
删除指定key | |
expire(String key,int i) |
设置键为key的过期时间为i秒 | |
int jedis.ttl(String key) |
获取key数据项的剩余时间(秒) | |
persist(String key) |
移除键为key属性项的生存时间限制 | |
type(String key) | 查看键为key所对应value的数据类型 |
- 2、Jedis中的字符串操作
字符串类型是Redis中最为基础的数据存储类型,在Redis中字符串类型的Value最多可以容纳的数据长度是512M。
语法 | 描述 |
---|---|
set(String key,String value) |
增加(或覆盖)数据项 |
setnx(String key,String value) |
不覆盖增加数据项(重复的不插入) |
setex(String ,int t,String value) |
增加数据项并设置有效时间 |
del(String key) |
删除键为key的数据项 |
get(String key) |
获取键为key对应的value |
append(String key, String s) | 在key对应value 后边追加字符串 s |
mset(String k1,String V1,String K2,String V2,…) |
增加多个键值对 |
String[] mget(String K1,String K2,…) |
获取多个key对应的value |
del(new String[](String K1,String K2,.... )) |
删除多个key对应的数据项 |
String getSet(String key,String value) | 获取key对应value并更新value |
String getrange(String key , int i, int j) | 获取key对应value第i到j字符 ,从0开始,包头包尾 |
- 3、Jedis中的增减操作
语法 | 描述 |
---|---|
incr(String key) | 将key对应的value 加1 |
incrBy(String key,int n) | 将key对应的value 加 n |
decr(String key) | 将key对应的value 减1 |
decrBy(String key , int n) | 将key对应的value 减 n |
- 4、Jedis中的列表操作
语法 | 描述 |
---|---|
lpush(String key, String v1, String v2,....) |
添加一个List , 如果已经有该List对应的key, 则按顺序在左边追加 一个或多个 |
rpush(String key , String vn) |
key对应list右边插入元素 |
lrange(String key,int i,int j) | 获取key对应list区间[i,j]的元素,注:从左边0开始,包头包尾 |
ltrim(String key,int i,int j) | 删除list区间[i,j] 之外的元素 |
lpop(String key) |
左弹出一个key对应的元素 |
rpop(String key) |
右弹出一个key对应的元素 |
llen(String key) |
获取key对应list的长度 |
lset(String key,int index,String val) |
修改key对应的list指定下标index的元素 |
lindex(String key,int index) | 获取key对应list下标为index的元素 |
- 5、Jedis中的集合操作
语法 | 描述 |
---|---|
sadd(String key,String v1,String v2,…) |
添加一个set |
smenbers(String key) |
获取key对应set的所有元素 |
srem(String key,String val) | 删除集合key中值为val的元素 |
srem(String key, Sting v1, String v2,…) | 删除值为v1, v2 , …的元素 |
sinter(String key1, String key2) |
获取集合key1和集合key2的交集 |
sunion(String key1, String key2) |
获取集合key1和集合key2的并集 |
sdiff(String key1, String key2) |
获取集合key1和集合key2的差集 |
- 6、Jedis中的有序集合操作
语法 | 描述 |
---|---|
zadd(String key,Map map) |
添加一个ZSet |
hset(String key,int score , int val) |
往 ZSet插入一个元素(Score-Val) |
zrange(String key, int i , int j) | 获取ZSet 里下表[i,j] 区间元素Val |
zscore(String key,String value) | 获取ZSet里value元素的Score |
zrem(String key,String value) | 删除ZSet里的value元素 |
zcard(String key) | 获取ZSet的元素个数 |
zcount(String key , int i ,int j) | 获取ZSet总score在[i,j]区间的元素个数 |
zincrby(String key,int n , String value) | 把ZSet中value元素的score+=n |
- 7、Jedis中的哈希操作
语法 | 描述 |
---|---|
hmset(String key,Map map) |
添加一个Hash |
hset(String key , String key, String value) |
向Hash中插入一个元素(K-V) |
hgetAll(String key) |
获取Hash的所有(K-V) 元素 |
hkeys(String key) |
获取Hash所有元素的key |
hvals(String key) |
获取Hash所有元素 的value |
hdel(String key , String k1, String k2,…) |
从Hash中删除一个或多个元素 |
hlen(String key) |
获取Hash中元素的个数 |
hexists(String key,String K1) |
判断Hash中是否存在指定key对应的元素 |
hmget(String key,String K1,String K2) |
获取Hash中一个或多个元素value |
1.1.3 序列化
将对象存储在Redis时,常用的方式是:将Java对象序列化为二进制;从Redis中获取对象时,再将二进制反序列化为对象。这个过程中可能用到的接口:
public String set(final String key, String value)
public String set(final byte[] key, final byte[] value)
public byte[] get(final byte[] key)
public String get(final String key)
Jedis本身没有提供序列化的工具,开发时需要再引入序列化工具,以下面以protostuff为例:
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.0.11</version>
</dependency>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.0.11</version>
</dependency>
接下来就要创建实体类、序列化工具类和测试类,示例:
//实体类
public class Club {
private int id; // id
private String name; // 名称
private String info; // 描述
private Date createDate; // 创建日期
private int rank; // 排名
public Club(int id,String name,String info,Date createDate,int rank) {
this.id = id;
this.name = name;
this.info = info;
this.createDate = createDate;
this.rank = rank;
}
@Override
public String toString() {
return "id:"+id+",name:"+name+",info:"+info+",createDate:"+createDate+",rank:"+rank;
}
}
//工具类
public class ProtostuffSerializer {
private Schema<Club> schema = RuntimeSchema.createFrom(Club.class);
public byte[] serialize(final Club club) {
final LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
try {
return serializeInternal(club, schema, buffer);
} catch (final Exception e) {
throw new IllegalStateException(e.getMessage(), e);
} finally {
buffer.clear();
}
}
public Club deserialize(final byte[] bytes) {
try {
Club club = deserializeInternal(bytes, schema.newMessage(), schema);
if (club != null ) {
return club;
}
} catch (final Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
return null;
}
private <T> byte[] serializeInternal(final T source, final Schema<T>schema, final LinkedBuffer buffer) {
return ProtostuffIOUtil.toByteArray(source, schema, buffer);
}
private <T> T deserializeInternal(final byte[] bytes, final T result, final
Schema<T> schema) {
ProtostuffIOUtil.mergeFrom(bytes, result, schema);
return result;
}
}
//测试类
public class AppMain {
public static void main(String[] args) {
//序列化工具类
ProtostuffSerializer protostuffSerializer = new ProtostuffSerializer();
Jedis jedis = new Jedis("127.0.0.1", 6379);
//序列化
String key = "club:1";
Club club = new Club(1, "AC", " 米兰 ", new Date(), 1);
byte[] clubBtyes = protostuffSerializer.serialize(club);
jedis.set(key.getBytes(), clubBtyes);
//反序列化
byte[] resultBtyes = jedis.get(key.getBytes());
Club resultClub = protostuffSerializer.deserialize(resultBtyes);
System.out.print(resultClub);
}
}
测试结果为:
id:1,name:AC,info: 米兰 ,createDate:Sat Sep 25 09:46:13 CST 2021,rank:1
1.1.4 在Redis中存对象的几种方式
1、原生字符串类型:每个属性一个键
set user:1:name tom
set user:1:age 23
set user:1:city beijing
优点:简单直观,每个属性都支持更新操作。
缺点:占用过多的键,内存占用量较大,同时用户信息内聚性比较差,所以此种方案一般不会在生产环境使用。
2、序列化字符串类型:将用户信息序列化后用一个键保存
set user:1 serialize(userInfo)
优点:简化编程,如果合理的使用序列化可以提高内存的使用效率。
缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到Redis中。
将对象转换为json字符串后进行存储的方式,也是类似。
3、哈希类型:每个用户属性使用一对field-value,但是只用一个键保存
hmset user:1 name tomage 23 city beijing
优点:简单直观,如果使用合理可以减少内存空间的使用。
缺点:要控制哈希在ziplist和hashtable两种内部编码的转换,hashtable会消耗更多内存。
1.2 Jedis连接池
客户端连接Redis使用的是TCP协议,直连的方式每次需要建立TCP连接,而连接池的方式是可以预先初始化好Jedis连接,每次只需要从Jedis连接池借用即可,只有少量的并发同步开销,远远小于新建TCP连接的开销。两者的比较:
优点 | 缺点 | |
---|---|---|
直连 | 简单方便,适用于少量长期连接的场景 | 1、存在每次新建/关闭TCP连接开销 2、资源无法控制,可能会出现连接泄露 3、Jedis对象线程不安全 |
连接池 | 1、无需每次连接都生成Jedis对象,降低开销 2、使用连接池控制开销 |
使用较麻烦,要熟悉各个参数的意义 |
连接池的基本使用:
//使用默认配置
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
//初始化Jedis连接池
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);
Jedis jedis = null;
try {
//从连接池获取jedis对象
jedis = jedisPool.getResource();
System.out.println(jedis.get("hello")); //world
} catch (Exception e) {
} finally {
if (jedis != null) {
//close操作不是关闭连接,代表归还连接池
jedis.close();
}
}
在GenericObjectPoolConfig中,可以设置很多关于Redis连接池的属性,一些较常用的设置:
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
// 设置最大连接数为默认值的 5 倍
poolConfig.setMaxTotal(GenericObjectPoolConfig.DEFAULT_MAX_TOTAL * 5);
// 设置最大空闲连接数为默认值的 3 倍
poolConfig.setMaxIdle(GenericObjectPoolConfig.DEFAULT_MAX_IDLE * 3);
// 设置最小空闲连接数为默认值的 2 倍
poolConfig.setMinIdle(GenericObjectPoolConfig.DEFAULT_MIN_IDLE * 2);
// 设置开启 jmx 功能
poolConfig.setJmxEnabled(true);
// 设置连接池没有连接后客户端的最大等待时间 ( 单位为毫秒 )
poolConfig.setMaxWaitMillis(3000);
GenericObjectPoolConfig的重要属性:
参数名 | 含义 | 默认值 |
---|---|---|
maxActive | 连接池中最大连接数 | 8 |
maxIdle | 连接池中最大空闲的连接数 | 8 |
minIdle | 连接池中最少空闲的连接数 | 0 |
maxWaitMillis | 当连接池资源耗尽后,调用者的最大等待时间(单位为毫秒),一般不建议使用默认值 | -1永远不超时,一直等: |
jmxEnabled | 是否开启jmx监控,如果应用开启了jmx端口,并且jmxEnabled设置为true,就可以通过jconsole或jvisualvm看到关于连接池的相关统计,有助于了解连接池的使用情况,并且可以针对做监控统计 | true |
minEvictableIdleTimeMillis | 连接的最小空闲时间,达到此值后空闲连接将被移除 | 1000L x 60L x 30毫秒 = 30分钟 |
numTestsPerEvictionRun | 做空闲连接检测时,每次的采样数 | 3 |
testOnBorrow | 向连接池借用连接时是否做连接有效性检测(ping),无效连接将被移除,每次借用多执行一次ping命令 | false |
testOnReturn | 是否做周期性空闲检测 | false |
testWhileIdle | 向连接池借用连接时是否做连接空闲检测,空闲超时的连接会被移除 | false |
timeBetweenEvictionRunsMillis | 空闲连接的检测周期(单位为毫秒) | -1:表示不做检测 |
blockWhenExhausted | 当连接池用尽后,调用者是否要等待,这个参数和maxWaitMillis对应,当此参数为true时,maxWaitMillis 才生效 | false |
1.3 客户端管理
1.3.1 client list
client list
命令能列出与Redis服务端相连的所有客户端连接信息,示例:
输出结果的每一行代表一个客户端的信息。
- 1、id
客户端连接的唯一标识,这个id是随着Redis的连接自增的,重启Redis后会重置为0。 - 2、addr
客户端连接的ip和端口。 - 3、fd
socket的文件描述符。 - 4、name
客户端的名字。 - 5、qbuf、qbuf-free
这两个属性都表述输入缓冲区相关的信息。
Redis服务端为每个Redis客户端分配了输入缓冲区,它的作用是将客户端发送的命令临时保存,同时服务端从会输入缓冲区拉取命令并执行
。图示:
qbuf
代表这个缓冲区的总容量,qbuf-free
代表这个缓冲区的剩余容量。Redis没有提供相应的配置来规定每个缓冲区的大小,输入缓冲区会根据输入内容大小的不同动态调整,只是要求每个客户端缓冲区的大小不能超过1G,超过后客户端将被关闭
。
对于Redis服务端而言,假设一个Redis实例设置了maxmemory(Redis服务端的最大内存)为4G,已经存储了2G数据,但是如果此时输入缓冲区使用了3G,已经超过maxmemory限制,可能会产生数据丢失、键值淘汰、OOM等情况。
查看Redis服务端内存配置的命令是info memory
,示例:
造成输入缓冲区过大的原因有哪些?
1、主要是因为Redis的处理速度跟不上输入缓冲区的输入速度,并且每次进入输入缓冲区的命令包含了大量bigkey。
2、Redis发生了阻塞,短期内不能处理命令,造成客户端输入的命令积压在了输入缓冲区。
监控输入缓冲区异常的方法有两种:
- 通过定期执行
client list
命令,收集qbuf和qbuf-free找到异常的连接记录并分析,最终找到可能出问题的客户端。 - 通过
info clients
命令,找到最大的输入缓冲区,下面命令中的其中client_biggest_input_buf代表最大的输入缓冲区,例如可以设置超过10M就进行报警:
client list和info clients的对比:
命令 | 优点 | 缺点 |
---|---|---|
client list | 能精准分析每个客户端来定位问题 | 执行速度较慢,频繁执行存在阻塞Redis的可能 |
info clients | 执行速度比client list快,分析过程较为简单 | 不能精确定位到客户端; 不能显示所有输入缓冲区的总量,只能显示最大量 |
- 6、obl、oll、omem
这三个属性都表述输出缓冲区相关的信息
。
Redis服务端为每个Redis客户端分配了输出缓冲区,它的作用是保存命令执行的结果返回给客户端,为服务端和客户端交互返回结果提供缓冲,图示:
输出缓冲区的容量可以通过参数client-output-buffer-limit
来进行设置,按照客户端的不同分为三种:普通客户端、发布订阅客户端、slave客户端,图示:
client-output-buffer-limit命令的使用:client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
。参数:
1、
<class>
:客户端类型,分为三种。
normal:普通客户端;
slave:slave客户端,用于复制;
pubsub:发布订阅客户端。
2、<hard limit>
:如果客户端使用的输出缓冲区大于<hard limit>
,客户端会被立即关闭。
3、<soft limit>
和<soft seconds>
:如果客户端使用的输出缓冲区超过了<soft limit>
并且持续了<soft seconds>
秒,客户端会被立即关闭。
client-output-buffer-limit的默认配置:
输出缓冲区由两部分组成:固定缓冲区(16KB)和动态缓冲区,其中固定缓冲区返回比较小的执行结果,而动态缓冲区返回比较大的结果,例如大的字符串、hgetall、smembers命令的结果等。
固定缓冲区使用的是字节数组,动态缓冲区使用的是列表
。当固定缓冲区存满后会将Redis新的返回结果存放在动态缓冲区的队列中,队列中的每个对象就是每个返回结果。
client list中的obl代表固定缓冲区的长度,oll代表动态缓冲区列表的长度,omem代表使用的字节数。例如下面代表当前客户端的固定缓冲区的长度为0,动态缓冲区有4869个对象,两个部分共使用了133081288字节=126M内存:
监控输出缓冲区的方法依然有两种:
- 通过定期执行client list命令,收集obl、oll、omem找到异常的连接记录并分析,最终找到可能出问题的客户端。
- 通过info命令的info clients模块,找到输出缓冲区列表最大对象数。示例:
client_longest_output_list代表输出缓冲区列表最大对象数
。
如何预防输出缓冲区出现异常呢?主要方法有以下几种:
1、监控并设置阀值,超过阀值及时处理。
2、根据client-output-buffer-limit
命令对普通缓冲区设置,示例:client-output-buffer-limit normal 20mb 10mb 120
。
3、及时监控内存,一旦发现内存抖动频繁,可能就是输出缓冲区过大。
- 7、age和idle
age
代表当前客户端已经连接的时间,idle
代表当前客户端最近一次的空闲时间。示例:
上面这条记录代表当期客户端连接Redis的时间为8888581秒,其中空闲了8888581秒,实际上这种就属于不太正常的情况,当age等于idle时,说明连接一直处于空闲状态
。 - 8、和maxclients/timeout配合使用
Redis提供了maxclients参数来限制最大客户端连接数,一旦连接数超过maxclients,新的连接将被拒绝。maxclients默认值是10000,可以通过info clients
来查询当前Redis的连接数:
可以通过config set maxclients对最大客户端连接数进行动态设置:
一般来说maxclients=10000在大部分场景下已经绝对够用。同时,Redis提供了timeout(单位为秒)参数来限制连接的最大空闲时间,一旦客户端连接的idle时间超过了timeout,连接将会被关闭。Redis 默认的 timeout 是 0 ,也就是不会检测客户端的空闲
。
将timeout设置为30秒示例:
在实际开发和运维中,需要将timeout设置成大于0,例如可以设置为300秒,这样可以避免Redis的客户端使用不当或者客户端本身的一些问题,造成没有及时释放客户端连接的问题。 - 9、flags
flags
是用于标识当前客户端的类型,例如flags=S代表当前客户端是slave客户端、flags=N代表当前是普通客户端。客户端类型:
客户端类型 | 说明 |
---|---|
N | 普通客户端 |
M | master节点 |
S | slave节点 |
o | 正在执行monitor命令 |
x | 正在执行事务 |
b | 正在等待阻塞时间 |
u | 客户端未被阻塞 |
A | 尽可能快地关闭连接 |
- 10、client list所有参数
参数 | 含义 |
---|---|
id | 客户端连接id |
addr | 客户端连接IP和端口 |
fd | socket的文件描述符 |
name | 客户端连接名 |
age | 客户端连接存活时间 |
idle | 客户端连接空闲时间 |
flags | 客户端连接标识 |
db | 当前客户端正在使用的数据库索引下标 |
sub/psub | 当前客户端订阅的频道数或模式数 |
multi | 当前事务中已执行命令个数 |
qbuf | 输入缓冲区总容量 |
qbuf-ree | 输入缓冲区剩余容量 |
obl | 固定缓冲区的长度 |
oll | 动态缓冲区列表的长度 |
omem | 固定缓冲区和动态缓存区使用的容量 |
cmd | 当前客户端最后一次执行的命令 |
1.3.2 client setName和client getName
用于给当前客户端设置和获取名称,示例:
1.3.3 client kill
用法为client kill ip:port
,此命令用于杀掉指定IP地址和端口的客户端。
1.3.4 client pause
用法为’client pause timeout’,表示阻塞客户端timeout毫秒数,在此期间客户端连接将被阻塞。
client pause
只对普通和发布订阅客户端有效,对于主从复制(从节点内部伪装了一个客户端)是无效的,所以此命令可以用来让主从复制保持一致。client pause
可以用一种可控的方式将客户端连接从一个Redis节点切换到另一个Redis节点。
1.3.5 monitor
monitor
命令用于监控Redis正在执行的命令。示例:
1.3.6 config set
该命令用于设置客户端属性。
- 1、timeout
检测客户端空闲连接的超时时间,一旦idle时间达到了timeout,客户端将会被关闭,如果设置为0就不进行检测。示例:
- 2、maxclients
客户端最大连接数。示例:
- 3、tcp-keepalive
检测TCP连接活性的周期,默认值为0,也就是不进行检测,如果需要设置,建议为60,那么Redis会每隔60秒对它创建的TCP连接进行活性检测,防止大量死连接占用系统资源。示例:
- 4、tcp-backlog
TCP三次握手后,会将接受的连接放入队列中,tcp-backlog就是队列的大小,它在Redis中的默认值是511。通常来讲这个参数不需要调整,示例:
1.3.7 info stats
info stats
可以统计一些Redis总的状态,示例:
total_connections_received
表示Redis自启动以来处理的客户端连接数总
数。
rejected_connections
表示Redis自启动以来拒绝的客户端连接数。
1.4 常见异常
1.4.1 无法从连接池获取到连接
1、假设JedisPool中的Jedis对象个数是8个,8个Jedis对象被占用,并且没有归还,此时还要从JedisPool中借用Jedis,就需要进行等待(例如设置maxWaitMillis>0),如果在maxWaitMillis时间内仍然无法获取到Jedis对象就会抛出JedisConnectionException
。
2、设置了blockWhenExhausted=false,那么调用者发现池子中没有Jedis资源时,会立即抛出异常不进行等待。
该属性的作用是:连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true。
一些可能会造成连接池中资源被耗尽的原因:
1、高并发下连接池设置过小。
2、没有正确使用连接池,比如没有进行释放。
3、存在慢查询操作,这些慢查询持有的Jedis对象归还速度会比较慢。
4、客户端是正常的,但是Redis服务端由于一些原因造成了客户端命令执行过程的阻塞。
1.4.2 客户端读写超时
SocketTimeoutException
,原因有以下几种:
1、读写超时间设置得过短。
2、命令本身执行就比较慢。
3、客户端与服务端网络不正常。
4、Redis服务端自身发生阻塞。
1.4.3 客户端连接超时
JedisConnectionException
,原因有以下几种:
1、连接超时设置得过短,修改示例:
jedis.getClient().setConnectionTimeout(time);
,单位毫秒。
2、Redis服务端发生阻塞,造成tcp-backlog已满,造成新的连接失败。
3、客户端与服务端网络不正常。
1.4.4 客户端缓冲区异常
Jedis在调用Redis时,如果出现客户端数据流异常,会出现JedisConnectionException
。原因有以下几种:
1、输出缓冲区满。
2、长时间闲置连接被服务端主动断开。
3、不正常并发读写:Jedis对象同时被多个线程并发操作,可能会出现该异常。
1.4.5 JedisDataException
Jedis调用Redis时,如果Redis正在加载持久化文件,会出现JedisDataException
。
1.4.6 Redis使用的内存超过maxmemory配置
Jedis执行写操作时,如果Redis的使用内存大于maxmemory的设置,会报如下异常:
1.4.7 客户端连接数过大
如果客户端连接数超过了maxclients,新申请的连接就会出现如下异常:
二、持久化
Redis 是内存型数据库,为了之后重用数据(比如重启机器、机器故障之后回复数据),或者是为了防止系统故障而将数据备份到一个远程位置,需要将内存中的数据持久化到硬盘上。
Redis 提供了RDB(快照)和AOF(只追加文件)两种持久化方式,默认是只开启RDB。
2.1 RDB
2.1.1 RDB是什么
RDB(Redis DataBase)是Redis默认的持久化方式。RDB持久化的方式是:按照一定的时间规律将(某个时间点的)内存的数据以快照(快照是数据存储的某一时刻的状态记录)的形式保存到硬盘中,对应产生的数据文件为dump.rdb(二进制文件)
。可以通过配置文件(redis.conf,在Windows系统上是redis.windows.conf)中的save参数来定义快照的周期。
Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。
2.1.2 RDB触发机制
总的来说,RDB持久化过程的触发方式分为手动触发和自动触发。
RDB的三种主要触发机制:
- 1、save命令(同步数据到磁盘上)
由于save 命令是同步命令,会占用Redis的主进程
。若Redis数据非常多时,save命令执行速度会非常慢,并且该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。因此很少在生产环境直接使用SAVE 命令,可以使用BGSAVE 命令代替
。如果在BGSAVE命令的保存数据的子进程发生错误的时,用 SAVE命令保存最新的数据是最后的手段。示例:
redis 127.0.0.1:6379> SAVE
OK
- 2、bgsave命令(异步保存数据到磁盘上)
Redis使用Linux系统的fock()生成一个子进程来将DB数据保存到磁盘,主进程继续提供服务以供客户端调用
。如果操作成功,可以通过客户端命令LASTSAVE来检查操作结果。示例:
127.0.0.1:6379> BGSAVE
Background saving started
127.0.0.1:6379> LASTSAVE
(integer) 1632563411
- 3、自动生成RDB
除了手动执行 save 和 bgsave 命令实现RDB持久化以外,Redis还提供了自动自动生成RDB的方式。
你可以通过配置文件对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个key改动”这一条件被满足时, 自动进行数据集保存操作(其实Redis本身也是这样做的
)。比如:
save 300 10
该命令表示“如果距离上一次创建RDB文件已经过去了300秒,并且服务器的所有数据库总共已经发生了不少于10次修改,那么系统自动执行BGSAVE命令”。
自动触发RDB持久化不仅仅有save m n
这一种方式,以下方式均能自动触发RDB持久化:
1)使用save相关配置,如
save m n
,表示m秒内数据集存在n次修改时,自动触发bgsave。
2)如果从节点执行全量复制
操作,主节点自动执行bgsave生成RDB文件并发送给从节点。
3)执行debug reload命令重新加载Redis时,也会自动触发save操作。
4)默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave
。
在redis.conf配置文件中默认有此下配置:
#在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 900 1
#在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 300 10
#在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 60 10000
根据配置,快照将被写入dbfilename(默认值是dump.rdb)
指定的文件里面,并存储在dir(默认值是./)
选项指定的路径上。
举个例子:假设Redis的上一个快照是2:35开始创建的,并且已经创建成功。下午3:06时,Redis又开始创建新的快照,并且在下午3:08快照创建完毕之前,有35个键进行了更新。如果在下午3:06到3:08期间,系统发生了崩溃,导致Redis无法完成新快照的创建工作,那么Redis将丢失下午2:35之后写入的所有数据。另一方面,如果系统恰好在新的快照文件创建完毕之后崩溃,那么Redis将丢失35个键的更新数据。
结论:RDB的持久化方式会丢失部分更新数据
。
2.1.3 RDB持久化流程
bgsave命令的执行流程:
1)执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进程,如RDB/AOF子进程,如果存在bgsave命令直接返回。
2)父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞,通过info stats命令查看latest_fork_usec选项,可以获取最近一个fork操作的耗时,单位为微秒。
3)父进程fork完成后
,bgsave命令返回“Background saving started”信息并不再阻塞父进程,继续响应其他命令
。
4)子进程创建RDB文件,根据父进程内存生成临时快照文件
,完成后对原有文件进行原子替换。
5)进程发送信号给父进程表示完成,父进程更新统计信息。
简单来说,当 Redis 需要保存 dump.rdb 文件时, 服务器执行以下操作:
1、Redis 调用forks。同时拥有父进程和子进程。
2、子进程将数据集写入到一个临时 RDB 文件中。
3、当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。
RDB文件保存在dir配置指定的目录下,文件名通过dbfilename配置指定。可以通过执行config set dir {newDir}
和config set dbfilename {newFileName}
运行期动态执行,当下次运行时RDB文件会保存到新目录。
Redis默认采用LZF算法对生成的RDB文件做压缩处理,压缩后的文件远远小于内存大小,默认开启,可以通过参数config set rdbcompression {yes|no}
动态修改。
2.1.4 RDB的优缺点
- 1、RDB的优点:
- RDB文件是紧凑的二进制文件,比较适合做冷备,
全量复制
的场景;相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复Redis进程,更加快速
;- RDB对Redis对外提供的读写服务,影响非常小,
可以让Redis保持高性能
,因为Redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可。
- 2、RDB的缺点:
- 如果想要在Redis故障时,尽可能少的丢失数据,那么RDB没有AOF好;
一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦Redis进程宕机,那么会丢失最近
5分钟的数据
。 - RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒;
RDB无法实现实时或者秒级持久化
。
如果系统真的发生崩溃,用户将丢失最近一次生成快照之后更改的所有数据。因此,快照持久化只适用于即使丢失一部分数据也不会造成一些大问题的应用程序。不能接受这个缺点的话,可以考虑AOF持久化。
2.2 AOF
2.2.1 AOF是什么
AOF 持久化:将写命令添加到 AOF 文件(Append Only File)的末尾。
与RDB持久化通过保存数据库中键值对来保存数据库的状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库的状态
。当重启Redis会重新将持久化的日志中文件恢复数据。
与快照持久化相比,AOF持久化 的实时性更好
,因此已成为主流的持久化方案。
开启AOF功能需要设置配置:appendonly yes
,默认不开启。AOF文件名通过appendfilename
设置,默认文件名是appendonly.aof。
当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复
。
2.2.2 AOF的工作流程
AOF的工作流程操作:命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load)。
- 所有的写入命令会追加到aof_buf(缓冲区)中。
- AOF缓冲区根据对应的策略向硬盘做同步操作。
- 随着AOF文件越来越大,需要
定期对AOF文件进行重写,达到压缩的目的
。- 当Redis服务器重启时,可以加载AOF文件进行数据恢复。
1、命令写入
AOF命令写入的内容直接是文本格式。如set hello world这条命令,在AOF缓冲区会追加如下文本:*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n
。
Redis使用单线程响应命令,把命令先写入缓存,比起直接写入硬盘,性能更好。2、文件同步
Redis提供了多种AOF缓冲区同步文件策略,由Redis的配置文件(redis.conf,在Windows系统上是redis.windows.conf)中的参数appendfsync控制(其实就是AOF持久化的三种策略):
- appendfsync always
每次有新命令追加到 AOF 文件时就执行一次 fsync :非常慢,也非常安全。 - appendfsync everysec
每秒 fsync 一次:足够快(和使用 RDB 持久化差不多),并且在故障时只会丢失 1 秒钟的数据。推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性
。 每秒钟同步一次,显式地将多个写命令同步到硬盘。 - appendfsync no
从不 fsync :将数据交给操作系统来处理,由操作系统来决定什么时候同步数据
。更快,也更不安全的选择。
这三种同步方式特点如下:
1)appendfsync always
可以实现将数据丢失减到最少,不过这种方式需要对硬盘进行大量的写入而且每次只写入一个命令,十分影响Redis的速度。另外使用固态硬盘的用户谨慎使用appendfsync always选项,因为这会明显降低固态硬盘的使用寿命。
2)appendfsync everysec
为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec选项 ,让Redis每秒同步一次AOF文件,Redis性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
3)appendfsync no
选项一般不推荐,这种方案会使Redis丢失不定量的数据而且如果用户的硬盘处理写入操作的速度不够的话,那么当缓冲区被等待写入的数据填满时,Redis的写入操作将被阻塞,这会导致Redis的请求速度变慢。
3、重写
随着命令不断写入AOF,文件会越来越大,为了解决这个问题,Redis引入AOF重写机制压缩文件体积。AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。
AOF重写过程可以手动触发和自动触发:
1、手动触发:直接调用bgrewriteaof命令。
2、自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数确定自动触发时机。
BGREWRITEAOF命令会通过移除AOF文件中的冗余命令来重写(rewrite)AOF文件来减小AOF文件的体积。
auto-aof-rewrite-min-size
:表示运行AOF重写时文件最小体积,默认为64MB。
auto-aof-rewrite-percentage
:代表当前AOF文件空间(aof_current_size)和上一次重写后AOF文件空间(aof_base_size)的比值。
自动触发时机:
aof_current_size > auto-aof-rewrite-min-size
&&(aof_current_size-aof_base_size)/aof_base_size >= auto-aof-rewrite-percentage
具体例子,假如上面两个属性为以下配置:
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
那么当AOF文件体积大于64mb,并且AOF的体积比上一次重写之后的体积大了至少一倍(100%)的时候,Redis将执行BGREWRITEAOF命令。
AOF重写运作流程:
1)执行AOF重写请求。
如果当前进程正在执行AOF重写,请求不执行。
如果当前进程正在执行bgsave操作,重写命令延迟到bgsave完成之后再执行。
2)父进程执行fork创建子进程,开销等同于bgsave过程。
3)主进程fork操作完成后,继续响应其他命令。所有修改命令依然写入AOF缓冲区并根据appendfsync策略同步到硬盘,保证原有AOF机制正确性。由于fork操作运用写时复制技术,子进程只能共享fork操作时的内存数据。由于父进程依然响应命令,Redis使用“AOF重写缓冲区”保存这部分新数据,防止新AOF文件生成期间丢失这部分数据。
4)子进程根据内存快照,按照命令合并规则写入到新的AOF文件。每次批量写入硬盘数据量由配置aof-rewrite-incremental-fsync控制,默认为32MB,防止单次刷盘数据过多造成硬盘阻塞。
5)新AOF文件写入完成后,子进程发送信号给父进程,父进程更新统计信息。父进程把AOF重写缓冲区的数据写入到新的AOF文件。使用新AOF文件替换老文件,完成AOF重写。
4、重启加载
AOF和RDB文件都可以用于服务器重启时的数据恢复,恢复流程:
5、文件校验
加载损坏的AOF文件时会拒绝启动,日志:
对于错误格式的AOF文件,先进行备份,然后采用redis-check-aof–fix命令进行修复,修复后使用diff-u对比数据的差异,找出丢失的数据,有些可以人工修改补全。
AOF文件可能存在结尾不完整的情况,比如机器突然掉电导致AOF尾部文件命令写入不全。Redis为我们提供了aof-load-truncated配置来兼容这种情况,默认开启。
2.2.3 AOF的优缺点
AOF的优点:
1、该机制可以带来更高的数据安全性,即数据持久性
。Redis中提供了3中同步策略,即每秒同步、每次修改同步和不同步
。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。
2、由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性
的问题。
3、AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令 进行合并重写),可以删除其中的某些命令(比如误操作的 flushall))
AOF的缺点:
1、AOF 文件比 RDB 文件大,且恢复速度慢
。
2、数据集大的时候,比 rdb 启动效率低
。
2.3 AOF和RDB对比
RDB | AOF | |
---|---|---|
启动优先级 | 低 | 高 |
体积 | 小 | 大 |
恢复速度 | 快 | 慢 |
数据安全性 | 丢数据 | 根据策略决定 |
2.4 Redis 4.0 对持久化机制的优化
Redis 4.0 开始支持 RDB 和 AOF 的混合持久化
,该功能通过aof-use-rdb-preamble
配置参数控制,yes则表示开启,no表示禁用,默认是禁用的,可通过config set修改。
如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据
。当然缺点也是有的, AOF 里面的 RDB 部分就是压缩格式不再是 AOF 格式,可读性较差。
2.5 如何选择合适的持久化方式
一般来说, 如果想达到足以媲美PostgreSQL的数据安全性,你应该同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整
。
如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失,那么你可以只使用RDB持久化
。
有很多用户都只使用AOF持久化,但并不推荐这种方式,因为定时生成RDB快照非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比AOF恢复的速度要快。
如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式。
为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec
选项 ,让Redis每秒同步一次AOF文件,Redis性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。
三、事务
3.1 Redis事务简介
Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合
。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令
。
在 Redis 中,事务具有一致性和隔离性,并且当 Redis 运行在某种特定的持久化模式下时,事务也具有持久性。但是,Redis中的事务不具有一致性,可能部分命令会执行成功,而另一部分命令执行失败。
Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:
1、批量操作在发送 EXEC 命令前被放入队列缓存。
2、收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
3、在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
一个事务从开始到执行会经历以下三个阶段:开始事务、命令入队、执行事务。
事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
3.2 Redis事务命令
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的。事务功能有以下特点:
- Redis会将一个事务中的所有命令序列化,然后按顺序执行。
redis 不支持回滚
,“Redis 在事务失败时不进行回滚,而是继续执行余下的命令
”, 所以 Redis 的内部可以保持简单且快速。如果在一个事务中的命令出现错误,那么所有的命令都不会执行
;如果在一个事务中出现运行错误,那么正确的命令会被执行
。
- 1、Multi
Multi用于开启一个事务
。 Multi执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被按照先后顺序放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。 - 2、Exec
Exec命令用于执行所有事务块内的命令
。这些命令按先后顺序排列。 当操作被打断时,返回空值 nil 。
前两个命令的使用示例:
- 3、Discard
discard 命令用于取消事务(清空事务队列)
,放弃执行事务块内的所有命令。示例:
- 4、Watch
Watch命令是一个乐观锁,可以为 Redis 事务提供(CAS)行为。其功能是:可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令
。示例:
- 5、Unwatch
Unwatch命令用于取消 WATCH 命令对所有 key 的监视
。示例:
3.3 Redis事务保证原子性吗,支持回滚吗
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行
。
如果想实现回滚,就需要用WATCH命令
,具体的做法是:
需要在MULTI之前使用WATCH来监控某些键值对,然后使用MULTI命令来开启事务,执行对数据结构操作的各种命令,此时这些命令入队列。
当使用EXEC执行事务时,首先会比对WATCH所监控的键值对,如果没发生改变,它会执行事务队列中的命令,提交事务;如果发生变化,将不会执行事务中的任何命令,同时事务回滚。当然无论是否回滚,Redis都会取消执行事务前的WATCH命令。
四、阿里云Redis开发规范小结
4.1 键值设计
4.1.1 key名设计
- 【建议】: 可读性和可管理性
以业务名(或数据库名)为前缀(防止key冲突),用冒号分隔,比如业务名:表名:id,示例:ugc:video:1
。 - 【建议】:简洁性
保证语义的前提下,控制key的长度,当key较多时,内存占用也不容忽视,示例:user:{uid}:friends:messages:{mid}
简化为u:{uid}:fr:m:{mid}
。 - 【强制】:不要包含特殊字符
反例:包含空格、换行、单双引号以及其他转义字符。
4.1.2 value设计
- 【强制】:拒绝太大的字符串
string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000。 - 【推荐】:选择适合的数据类型。
反例:
set user:1:name tom
set user:1:age 19
set user:1:favor football
正例:
hmset user:1 name tom age 19 favor football
4.1.3 控制key的生命周期
建议使用expire设置过期时间(条件允许可以打散过期时间,防止集中过期),不过期的数据重点关注idletime(空闲时间)。
4.2 命令使用
- 【推荐】 O(N)命令关注N的数量
例如hgetall、lrange、smembers、zrange、sinter等并非不能使用,但是需要明确N的值。有遍历的需求可以使用hscan、sscan、zscan代替
。 - 【推荐】:禁用命令
禁止线上使用keys(时间复杂度较高的遍历)、flushall(清空当前数据库)、flushdb(清空当前数据库)等,通过redis的rename机制禁掉命令,或者使用scan的方式渐进式处理。 - 【推荐】使用批量操作提高效率
原生命令:例如mget、mset。
非原生命令:可以使用pipeline提高效率。
但要注意控制一次批量操作的元素个数(例如500以内,实际也和元素字节数有关)。注意两者不同:
- 原生是原子操作,pipeline是非原子操作。
- pipeline可以打包不同的命令,原生做不到
- pipeline需要客户端和服务端同时支持。
- 【建议】Redis事务功能较弱,不建议过多使用
Redis的事务功能较弱(不支持回滚),而且集群版本(自研和官方)要求一次事务操作的key必须在一个slot上(可以使用hashtag功能解决)。 - 【建议】必要情况下使用monitor命令时,要注意不要长时间使用。
4.3 客户端使用
- 【推荐】
避免多个应用使用一个Redis实例。
正例:不相干的业务拆分,公共数据做服务化。 - 【推荐】
使用带有连接池的数据库,可以有效控制连接,同时提高效率,标准使用方式:
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//具体的命令
jedis.executeCommand()
} catch (Exception e) {
logger.error("op key {} error: " + e.getMessage(), key, e);
} finally {
//注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
if (jedis != null)
jedis.close();
}
- 【建议】
高并发下建议客户端添加熔断功能(例如netflix hystrix)。 - 【推荐】
设置合理的密码,如有必要可以使用SSL加密访问。 - 【建议】
根据自身业务类型,选好maxmemory-policy(最大内存淘汰策略),设置好过期时间。