Redis问题记录

强制把redis快照关闭了导致不能持久化的问题

现象

1
2
3
4
5
6
7
127.0.0.1:6379> ping
(error) MISCONF Redis is configured to save RDB snapshots,
but it is currently not able to persist on disk. Commands
that may modify the data set are disabled, because this
instance is configured to report errors during writes if
RDB snapshotting fails (stop-writes-on-bgsave-error option).
Please check the Redis logs for details about the RDB error.

解决办法

通过stop-writes-on-bgsave-error值设置为no即可避免这种问题。

有两种修改方法,一种是通过redis命令行修改,另一种是直接修改redis.conf配置文件。

redis命令行修改示例:

1
127.0.0.1:6379> config set stop-writes-on-bgsave-error no

使用 redis-cli 过程中出现中文乱码

使用redis-cli时后面加上--raw就可以避免中文乱码。

1
$ redis-cli --raw -h host -p port -a password

NOAUTH Authentication required.

1
2
3
4
5
6
7
8
9
10
Exception in thread "main" redis.clients.jedis.exceptions.JedisDataException: NOAUTH Authentication required.
at redis.clients.jedis.Protocol.processError(Protocol.java:127)
at redis.clients.jedis.Protocol.process(Protocol.java:161)
at redis.clients.jedis.Protocol.read(Protocol.java:215)
at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:340)
at redis.clients.jedis.Connection.getStatusCodeReply(Connection.java:239)
at redis.clients.jedis.BinaryJedis.ping(BinaryJedis.java:196)
at org.sang.RedisTest.main(RedisTest.java:12)

Process finished with exit code 1

进行密码验证或者把密码置空。

在多线程场景下使用redis时,可能会出现各种类型转换异常:如:B cannot be cast to java.util.List

原因

在多线程场景下,如果使用jedis连接池获取连接,在各个地方调用,且使用完之后没有释放,则可能在某次使用的时候,拿到的是其他线程的连接,则会出现类型转换异常。

解决方案

在每次使用完连接后,将其返回连接池。具体操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

/**
* return resource to pool
* @paran isBroken 是否产生异常
*/
public void returnResource(Jedis shardedJedis, boolean isBroken) {
// starting from Jedis 3.0 this method will not be exposed. Resource cleanup should be done using @see {@link redis.clients.jedis.Jedis#close()}
   // returnBrokenResource | returnResource 在Jedis 3.0之后已经被废弃,释放shardedJedis调用Jedis的close()方法即可。
if (isBroken) {
jedisPool.returnBrokenResource(shardedJedis);
} else {
jedisPool.returnResource(shardedJedis);
}
}

线上Redis禁止使用Keys正则匹配操作

背景

  1. redis是单线程的,其所有操作都是原子的,不会因并发产生数据异常
  2. 使用高耗时的Redis命令是很危险的,会占用唯一的一个线程的大量处理时间,导致所有的请求都被拖慢。

案例

  1. 运维人员进行keys *操作,该操作比较耗时,又因为redis是单线程的,所以redis被锁住。
  2. 此时QPS比较高,又来了几万个对redis的读写请求,因为redis被锁住,所以全部Hang在那。
  3. 因为太多线程Hang在那,CPU严重飙升,造成redis所在的服务器宕机。
  4. 所有的线程在redis那取不到数据,一瞬间全去数据库取数据,数据库就宕机了。

其他危险命令

但凡发现时间复杂度为O(N)的命令,都要慎重,不要在生产上随便使用。例如hgetalllrangesmemberszrangesinter等命令,它们并非不能使用,但这些命令的时间复杂度都为O(N),使用这些命令需要明确N的值,否则也会出现缓存宕机。

  1. flushdb 命令用于清空当前数据库中的所有 key
  2. flushall 命令用于清空整个 Redis 服务器的数据(删除所有数据库的所有 key )
  3. config 客户端连接后可配置服务器

如何禁用危险命令

在redis.conf中,在SECURITY这一项中,我们新增以下命令:

1
2
3
4
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG ""
rename-command KEYS ""

另外,对于flushall命令,需要设置配置文件中appendonly no,否则服务器是无法启动。
请注意,更改[记录到AOF文件或传输到副本的命令]的名称可能会导致问题。

改良建议

使用scan命令来改良keyssmembers命令

redis2.8版本以后有了一个新命令scan,可以用来分批次扫描redis记录,这样肯定会导致整个查询消耗的总时间变大,但不会影响redis服务卡顿,影响服务使用。更多信息可参考redis scan命令

Redis Big Key问题

数据量大的 key ,由于其数据大小远大于其他key,导致经过分片之后,某个具体存储这个 big key 的实例内存使用量远大于其他实例,造成内存不足,拖累整个集群的使用。big key 在不同业务上,通常体现为不同的数据,比如:

  1. 论坛中的大型持久盖楼活动;
  2. 聊天室系统中热门聊天室的消息列表; ……

带来的问题

bigkey 通常会导致内存空间不平衡,超时阻塞,如果 key 较大,redis 又是单线程,操作 bigkey 比较耗时,那么阻塞 redis 的可能性增大。每次获取 bigKey 的网络流量较大,假设一个 bigkey 为 1MB,每秒访问量为 1000,那么每秒产生 1000MB 的流量,对于普通千兆网卡,按照字节算 128M/S 的服务器来说可能扛不住。而且一般服务器采用单机多实例方式来部署,所以还可能对其他实例造成影响。

  1. 如果是集群模式下,无法做到负载均衡,导致请求倾斜到某个实例上,而这个实例的QPS会比较大,内存占用也较多;对于Redis单线程模型又容易出现CPU瓶颈,当内存出现瓶颈时,只能进行纵向库容,使用更牛逼的服务器。
  2. 涉及到大key的操作,尤其是使用hgetall、lrange、get、hmget 等操作时,网卡可能会成为瓶颈,也会到导致堵塞其它操作,qps 就有可能出现突降或者突升的情况,趋势上看起来十分不平滑,严重时会导致应用程序连不上,实例或者集群在某些时间段内不可用的状态。
  3. 假如这个key需要进行删除操作,如果直接进行DEL 操作,被操作的实例会被Block住,导致无法响应应用的请求,而这个Block的时间会随着key的变大而变长。

什么是 big key

  1. 字符串类型:一般认为超过 10k 的就是 bigkey,但是这个值和具体的 OPS 相关。
  2. 非字符串类型:体现在哈希,列表,集合类型元素过多。

寻找big key

  1. redis-cli自带--bigkeys

    1
    2
    $ redis-cli -p 999 --bigkeys -i 0.1
    #Scanning the entire keyspace to find biggest keys as well as average sizes per key type. You can use -i 0.1 to sleep 0.1 sec per 100 SCAN commands (not usually needed).
  2. 获取生产Redis的rdb文件,通过rdbtools分析rdb生成csv文件,再导入MySQL或其他数据库中进行分析统计,根据size_in_bytes统计bigkey

    1
    2
    3
    4
    $ git clone https://github.com/sripathikrishnan/redis-rdb-tools
    $ cd redis-rdb-tools
    $ sudo python setup.py install
    $ rdb -c memory dump-10030.rdb > memory.csv
  3. 通过python脚本,迭代scan key,每次scan 1000,对扫描出来的key进行类型判断,例如:string长度大于10K,list长度大于10240认为是big bigkeys

  4. 其他第三方工具,例如:redis-rdb-cli

优化big key

优化big key的原则就是string减少字符串长度,list、hash、set、zset等减少成员数。

  1. string类型的big key,建议不要存入redis,用文档型数据库MongoDB代替或者直接缓存到CDN上等方式优化。有些 key 不只是访问量大,数据量也很大,这个时候就要考虑这个 key 使用的场景,存储在redis集群中是否是合理的,是否使用其他组件来存储更合适;如果坚持要用 redis 来存储,可能考虑迁移出集群,采用一主一备(或1主多备)的架构来存储。

  2. 单个简单的key存储的value很大

    该对象需要每次都整存整取: 可以尝试将对象分拆成几个key-value, 使用multiGet获取值,这样分拆的意义在于分拆单次操作的压力,将操作压力平摊到多个redis实例中,降低对单个redis的IO影响;
    该对象每次只需要存取部分数据: 可以像第一种做法一样,分拆成几个key-value,也可以将这个存储在一个hash中,每个field代表一个具体的属性,使用hget,hmget来获取部分的value,使用hset,hmset来更新部分属性。

  3. hash, set,zset,list 中存储过多的元素

    可以将这些元素分拆。以hash为例,原先的正常存取流程是 hget(hashKey, field) ; hset(hashKey, field, value)
    现在,固定一个桶的数量,比如 10000, 每次存取的时候,先在本地计算field的hash值,模除 10000,确定了该field落在哪个key上。

1
2
3
newHashKey  =  hashKey + (hash(field) % 10000);   
hset(newHashKey, field, value) ;
hget(newHashKey, field)

set, zset, list 也可以类似上述做法。但有些不适合的场景,比如,要保证 lpop 的数据的确是最早push到list中去的,这个就需要一些附加的属性,或者是在 key的拼接上做一些工作(比如list按照时间来分拆)。