redis analyst (1)- first try

项目上第一次引入redis来解决并发问题,所以记录下使用中体会的一些要领以备忘(以下行文以jedis作为客户端为案例):

(1)jedis最好设置下clientname,以便于trouble shooting, 但是spring-data-redis并没有提供设置方法(已提交pull request去支持:https://github.com/spring-projects/spring-data-redis/pull/219, 已merged),这点还是直接调用jedis方便。

public JedisSentinelPool(String masterName, Set sentinels,
final GenericObjectPoolConfig poolConfig, final int connectionTimeout, final int soTimeout,
final String password, final int database, final String clientName) {
this.clientName = clientName;
….
}

note: jedis的经常用的pool都支持clientname, 但是ShardedJedisPool还不支持clientname设置,也已提交pull request: https://github.com/xetorthio/jedis/pull/1383,不知道何时可以merge(至今未merge,倒是merge了cluster模式的支持)。

这样可以通过client list来获取client的name,对以后的troubleshooting必然有所帮助,例如:获悉某时刻连接数最多的app是哪个,每个app都在执行什么命令等。


10.224.38.23:0>client list

"id=8 addr=10.224.38.30:26636 fd=11 name=sentinel-ba1e0dae-pubsub age=1350878 idle=0 flags=N db=0 sub=1 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=subscribe //set client name

id=18 addr=10.140.201.34:51637 fd=12 name= age=85650 idle=85619 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=scan // not set

(2)清理keys及key的设置规则

清理keys一般不会删除所有的,否则公用的也被删除了,所以如果一般删除的话,要求keys有规则,这要求keys的设置符合一定的规则。

以下删除了所有的keys:

@Override
public void evictAll() {
Set keySet = redisTemplate.keys("*"); //匹配了所有的key
if (keySet == null ||keySet.size()==0) {
logger.debug("no keys are found");
return;
}

this.redisTemplate.delete(keySet);
}

key设置要具备一定的规则(以:风格),这样便于管理:

set user:id:895689 “fujian”

redis001

(3)有文章评论常用的setnx执行完后,如果定义过期时间,可能会失败,导致数据永远删除不了,所以推荐pipeline之类一步将命令发出。但是目前已经有新的set方法来合并这2步操作:

原先方法:

return redisClient.execute(jedis -> {
Long setnx = jedis.setnx(meetingKey, ip);
jedis.expire(meetingKey, 30); //这边可能会出错
if (setnx == 0)
return jedis.get(meetingKey);
return ip;
});

新的方法:

return redisClient.execute(jedis -> {
String value = jedis.set(meetingKey, ip, "NX", "EX", 30);//原子操作
if (value == null)//返回的是OK或者null,区别于setnx的1和0.
return jedis.get(meetingKey);
return ip;
});

(4)db的选择,默认有16个db, 默认采用的是db0, 如果需要更改,可以修改databaseid, 有文章评论说,每次操作都需要切换,实际上只做一次就可以了。

@Override
public void activateObject(PooledObject pooledJedis) throws Exception {
final BinaryJedis jedis = pooledJedis.getObject();
if (jedis.getDB() != database) {
jedis.select(database); //仅仅select 1次足够
}

}

 @Override
  public String select(final int index) {
    checkIsInMultiOrPipeline();
    client.select(index);
    String statusCodeReply = client.getStatusCodeReply();
    client.setDb(index);

    return statusCodeReply;
  }

但是分库一定要协商好,否则贸然使用非默认的,例如2,结果配置的database数目不是3个,则和预想的不同,且需要注意,默认是16,但是可以只配置1个,不分库.

参考配置文件:

# Set the number of databases. The default database is DB 0, you can select
# a different one on a per-connection basis using SELECT where
# dbid is a number between 0 and 'databases'-1
databases 16

CMD切换方法:
select 0 #打开id为0的数据库,也就是第一个库。
redis02

源码分析:

db.c

void selectCommand(client *c) {
    long id;

    if (getLongFromObjectOrReply(c, c->argv[1], &id,
        "invalid DB index") != C_OK)
        return;

    if (server.cluster_enabled && id != 0) {
        addReplyError(c,"SELECT is not allowed in cluster mode");
        return;
    }
    if (selectDb(c,id) == C_ERR) {
        addReplyError(c,"DB index is out of range");
    } else {
        addReply(c,shared.ok);
    }
}


int selectDb(client *c, int id) {
    if (id < 0 || id >= server.dbnum)
        return C_ERR;
    c->db = &server.db[id];
    return C_OK;
}

robj *lookupKeyWriteOrReply(client *c, robj *key, robj *reply) {
    robj *o = lookupKeyWrite(c->db, key);
    if (!o) addReply(c,reply);
    return o;
}

server.c  初始化


//取默认配置多少个db
void initServerConfig(void) {
    server.dbnum = CONFIG_DEFAULT_DBNUM;  
}

//申请空间
server.db = zmalloc(sizeof(redisDb)*server.dbnum); 

//初始化
for (j = 0; j < server.dbnum; j++) {  
        server.db[j].dict = dictCreate(&dbDictType,NULL);
        server.db[j].expires = dictCreate(&keyptrDictType,NULL);
        server.db[j].blocking_keys = dictCreate(&keylistDictType,NULL);
        server.db[j].ready_keys = dictCreate(&objectKeyPointerValueDictType,NULL);
        server.db[j].watched_keys = dictCreate(&keylistDictType,NULL);
        server.db[j].id = j;
        server.db[j].avg_ttl = 0;
}

(5)redis可以配置最大memory以保护自己,超过最大memory使用的清理策略可以参考配置文件(http://download.redis.io/redis-stable/redis.conf):

# maxmemory

# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
# is reached. You can select among five behaviors:
#
# volatile-lru; remove the key with an expire set using an LRU algorithm
# allkeys-lru; remove any key according to the LRU algorithm
# volatile-random; remove a random key with an expire set
# allkeys-random; remove a random key, any key
# volatile-ttl; remove the key with the nearest expire time (minor TTL)
# noeviction; don't expire at all, just return an error on write operations
#
# Note: with any of the above policies, Redis will return an error on write
# operations, when there are no suitable keys for eviction.
#
# At the date of writing these commands are: set setnx setex append
# incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd
# sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby
# zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby
# getset mset msetnx exec sort
#
# The default is:
#
# maxmemory-policy noeviction

(6)redis最多支持多少连接? 及客户端pool的配置应该如何?

redis server默认最多支持1万连接。

# Set the max number of connected clients at the same time. By default
# this limit is set to 10000 clients, however if the Redis server is not
# able to configure the process file limit to allow for the specified limit
# the max number of allowed clients is set to the current file limit
# minus 32 (as Redis reserves a few file descriptors for internal uses).
#
# Once the limit is reached Redis will close all the new connections sending
# an error 'max number of clients reached'.
#
# maxclients 10000

而默认的jedis pool配置如下:可知即使不启动eviction, 所有机器满负载情况下,1W/8是最多能部署的机器; 如果启用eviction的前提下,负载不大的情况下,可以部署的机器>1W/8, 所以基本可以认为,1W/maxTotal是所能部署的最多机器。如果想在负载可控的情况下提高部署机器的数量,可以启用eviction.

#pool configure
##pool basic configure
pool.maxTotal=8
pool.maxIdle=8

pool.testOnCreate=false
pool.testOnBorrow=false
pool.testOnReturn=false

pool.blockWhenExhausted=true
pool.maxWaitMillis=10000  //一定要设置,否则可能永久blocked.


## idle related configure
###timeBetweenEvictionRunsMillis -1 is not allow evict
pool.timeBetweenEvictionRunsMillis=30000
###only when timeBetweenEvictionRunsMillis>0, minIdle can work, and will close idle connection number util to minIdle
pool.minIdle=0
pool.testWhileIdle=true
###concurrent check for eviction,When negative, the number of tests performed will be ceil(getNumIdle/abs(getNumTestsPerEvictionRun)
pool.numTestsPerEvictionRun=-1
###eviction not evict idle time < minEvictableIdleTimeMillis
pool.minEvictableIdleTimeMillis=60000

##JMX
pool.jmx=true
pool.jmxNamePrefix=pool

(7) redis performance

可使用自带工具redis-benchmark
local test:

[root@wbxperf001 src]# ./redis-benchmark -p 30002 -q -n 1000000 -d 4000 -t set,get -r 100000
SET: 84402.43 requests per second
GET: 90694.72 requests per second

remote test:

[root@wbxperf001 src]# ./redis-benchmark -h 10.224.2.142 -q -n 1000000 -d 4000 -t set,get -r 100000
SET: 50782.04 requests per second
GET: 50423.56 requests per second