本文介绍如何玩转 Redis, 可以说是 Redis 开发规范, 也可以理解为 Redis 最佳实战.
以业务名(或数据库名)为前缀(防止key冲突), 用冒号(句号)分隔, 比如: 业务名:表名:id
保证语义的前提下, 控制 key 的长度, 当 key 较多时, 内存占用也不容忽视, 例如:
反例: 包含 空格 、换行 、单双引号 以及 其他转义字符
说明: 非字符串的bigkey,不要使用del删除,使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,造成阻塞,而且该操作不会不出现在慢查询中(latency可查))
正例: string 类型控制在 10KB 以内,hash、list、set、zset元素个数不要超过5000
反例: 一个包含 200万 个元素的 list
说明: 实体类型(要合理控制和使用数据结构 内存编码 优化配置, 例如 ziplist,但也要注意节省内存和性能之间的平衡)
正例:
反例:
说明: 建议使用 expire 设置过期时间(条件允许可以打散过期时间,防止集中过期),不过期的数据重点关注 idletime
说明: 例如 hgetall、lrange、smembers、zrange、sinter 等并非不能使用,但是需要 明确N 的值。有 遍历 的需求可以使用 hscan、sscan、zscan 代替
说明: 禁止线上使用 keys、flushall、flushdb等,通过 redis 的 rename 机制禁掉命令,或者使用 scan 的方式渐进式处理
说明: redis 的多数据库较弱,使用数字进行区分,很多客户端支持较差,同时多业务用多数据库实际还是单线程处理,会有干扰
说明: 但要注意控制一次批量操作的元素个数(例如500以内,实际也和元素字节数有关)
说明: Redis 的事务功能较弱(不支持回滚),而且集群版本(自研和官方)要求一次事务操作的 key 必须在一个 slot 上(可以使用hashtag功能解决)
正例: 不相干 的 业务拆分,公共数据 做 服务化
代码如下:
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();
}
默认策略是 volatile-lru,即超过最大内存后,在过期键中使用 lru 算法进行 key 的剔除,保证不过期数据不被删除,但是可能会出现 OOM 问题。
说明: redis 间数据同步可以使用:redis-port
redis大key搜索工具
facebook的redis-faina
public void delBigHash(String host, int port, String password, String bigHashKey) {
Jedis jedis = new Jedis(host, port);
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<Entry<String, String>> scanResult = jedis.hscan(bigHashKey, cursor, scanParams);
List<Entry<String, String>> entryList = scanResult.getResult();
if (entryList != null && !entryList.isEmpty()) {
for (Entry<String, String> entry : entryList) {
jedis.hdel(bigHashKey, entry.getKey());
}
}
cursor = scanResult.getStringCursor();
} while (!"0".equals(cursor));
//删除bigkey
jedis.del(bigHashKey);
}
public void delBigList(String host, int port, String password, String bigListKey) {
Jedis jedis = new Jedis(host, port);
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
long llen = jedis.llen(bigListKey);
int counter = 0;
int left = 100;
while (counter < llen) {
//每次从左侧截掉100个
jedis.ltrim(bigListKey, left, llen);
counter += left;
}
//最终删除key
jedis.del(bigListKey);
}
public void delBigSet(String host, int port, String password, String bigSetKey) {
Jedis jedis = new Jedis(host, port);
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<String> scanResult = jedis.sscan(bigSetKey, cursor, scanParams);
List<String> memberList = scanResult.getResult();
if (memberList != null && !memberList.isEmpty()) {
for (String member : memberList) {
jedis.srem(bigSetKey, member);
}
}
cursor = scanResult.getStringCursor();
} while (!"0".equals(cursor));
//删除bigkey
jedis.del(bigSetKey);
}
public void delBigZset(String host, int port, String password, String bigZsetKey) {
Jedis jedis = new Jedis(host, port);
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<Tuple> scanResult = jedis.zscan(bigZsetKey, cursor, scanParams);
List<Tuple> tupleList = scanResult.getResult();
if (tupleList != null && !tupleList.isEmpty()) {
for (Tuple tuple : tupleList) {
jedis.zrem(bigZsetKey, tuple.getElement());
}
}
cursor = scanResult.getStringCursor();
} while (!"0".equals(cursor));
//删除bigkey
jedis.del(bigZsetKey);
}
如果是单机,可以用 synchronized 或者 lock 来处理,如果是 分布式 环境可以用 分布式锁 就可以了(分布式锁,可以用 memcache 的add, redis 的setnx, zookeeper 的添加节点 操作)。
if (memcache.get(key) == null) {
// 3 min timeout to avoid mutex holder crash
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
value = db.get(key);
memcache.set(key, value);
memcache.delete(key_mutex);
} else {
sleep(50);
retry();
}
}
String get(String key) {
String value = redis.get(key);
if (value == null) {
if (redis.setnx(key_mutex, "1")) {
// 3 min timeout to avoid mutex holder crash
redis.expire(key_mutex, 3 * 60)
value = db.get(key);
redis.set(key, value);
redis.delete(key_mutex);
} else {
//其他线程休息50毫秒后重试
Thread.sleep(50);
get(key);
}
}
在 value 内部设置1个超时值(timeout1), timeout1 比实际的 memcache timeout(timeout2) 小。当从 cache 读取到 timeout1 发现它已经过期时候,马上 延长timeout1 并重新设置到 cache。然后再从数据库加载数据并设置到 cache 中。
v = memcache.get(key);
if (v == null) {
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
value = db.get(key);
memcache.set(key, value);
memcache.delete(key_mutex);
} else {
sleep(50);
retry();
}
} else {
if (v.timeout <= now()) {
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
// extend the timeout for other threads
v.timeout += 3 * 60 * 1000;
memcache.set(key, v, KEY_TIMEOUT * 2);
// load the latest value from db
v = db.get(key);
v.timeout = KEY_TIMEOUT;
memcache.set(key, value, KEY_TIMEOUT * 2);
memcache.delete(key_mutex);
} else {
sleep(50);
retry();
}
}
}
这里的“永远不过期”包含两层意思:
从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。
从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期
从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。
String get(final String key) {
V v = redis.get(key);
String value = v.getValue();
long timeout = v.getTimeout();
if (v.timeout <= System.currentTimeMillis()) {
// 异步更新后台异常执行
threadPool.execute(new Runnable() {
public void run() {
String keyMutex = "mutex:" + key;
if (redis.setnx(keyMutex, "1")) {
// 3 min timeout to avoid mutex holder crash
redis.expire(keyMutex, 3 * 60);
String dbValue = db.get(key);
redis.set(key, dbValue);
redis.delete(keyMutex);
}
}
});
}
return value;
}
利用 netflix 的 hystrix,可以做资源的隔离保护主线程池。
解决方案 | 优点 | 缺点 |
---|---|---|
简单分布式锁 | 1. 思路简单 2. 保证一致性 |
1. 代码复杂度增大 2. 存在死锁的风险 3. 存在线程池阻塞的风险 |
加另外一个过期时间 | 保证一致性 | 1. 代码复杂度增大 2. 存在死锁的风险 3. 存在线程池阻塞的风险 |
不过期 | 异步构建缓存,不会阻塞线程池 | 1. 不保证一致性 2. 代码复杂度增大(每个value都要维护一个timekey) 3. 占用一定的内存空间(每个value都要维护一个timekey) |
资源隔离组件hystrix | 1. hystrix技术成熟,有效保证后端 2. hystrix监控强大 |
1. 部分访问存在降级策略 |
下面是JedisPool优化方法的文章:
刚开始写博客, 希望大家支持, 如果有没疑问或不清楚的地方可以留言噢!
下周再见~