共计 6864 个字符,预计需要花费 18 分钟才能阅读完成。
前言
在业务中,会经常使用 redis 作为后端缓存、存储。如果结构规划不合理、命令使用不规范,会造成系 统性能达到瓶颈、活动高峰系统可用性下降,也会增大运维难度。这里总结了一些使用规范,希望能从 源头上避免上述问题的出现。
存储选型
Redis 是一个单进程、基于内存、弱事务(单个命令可以保证原子性,多命令无法保证)的 NoSql 存储系 统,适用于高 QPS、低延迟、弱持久化的场景,适宜用作缓存。
从经验出发: 在 qps>5000、容量 <50G、存储高频数据时考虑 redis;在 qps<1000、存储大量低频数 据、需要事务时考虑 Mysql。
使用场景
前提声明:
- 严禁在 redis 中存储需要持久化的数据;
- 只缓存热点数据
- 高并发场景下,热点数据缓存
高并发场景下,合理的使用缓存不仅能够提升网站访问速度,还能降低后端数据库的压力。
- 排行榜类场景
关系型数据库在排行榜类场景的查询速度普遍偏慢,借助 Redis 提供的 list 和 sorted sets 结构能实现各种复杂的排行榜应用。
- 限时业务的运用
利用 expire 命令可以运用在限时优惠活动信息、订单库存过期、手机验证码等业务场景。
- 计数器
Redis 天然支持计数功能而且计数的性能也非常好,在高并发场景下优于传统的关系型数据库,常运用于商品的浏览数、视频的播放数、限制调用等。
- 社交网络
点赞、踩、关注 / 被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis 提供的哈希、集合等数据结构能很方便的的实现这些功能。
- 分布式锁
在高并发场景中,利用数据库锁来控制资源的并发访问,性能不理想,可以利用 Redis 的 setnx 功能来编写分布式的锁。
一 键值设计
1. key 名设计
- 【建议】:可读性和可管理性
以业务名 (或数据库名) 为前缀(防止 key 冲突),必须使用冒号分隔,便于 RDM 查看,比如应用名称:租户号:DD_CODE。
APPKEY:TENANT_CODE:DD_CODE
- 【建议】:简洁性
保证语义的前提下,控制 key 的长度,当 key 较多时,内存占用也不容忽视,例如:
user:{uid}:friends:messages:{mid}简化为 u:{uid}:fr:m:{mid}。
- 【强制】:长度 50 个字符以内,不要包含空格、换行,引号和一些转义字符
反例:包含空格、换行、单双引号以及其他转义字符
- 【强制】:控制 key 的总数量
redis 实例包含的键个数建议控制在 1 千万内,单实例的键个数过大,可能导致过期键的回收不及时。
2. value 设计
- 【强制】:拒绝 bigkey(防止网卡流量、慢查询)
string 类型控制在 10KB 以内,hash、list、set、zset 元素个数不要超过 5000。反例:一个包含 200 万个元素的 list。非字符串的 bigkey,不要使用 del 删除,使用 hscan、sscan、zscan 方式渐进式删除,同时要注意防止 bigkey 过期时间自动删除问题(例如一个 200 万的 zset 设置 1 小时过期,会触发 del 操作,造成阻塞,而且该操作不会不出现在慢查询中(latency 可查)),查找方法和删除方法
- 【推荐】:选择适合的数据类型。
例如:实体类型(要合理控制和使用数据结构内存编码优化配置, 例如 ziplist,但也要注意节省内存和性能之间的平衡) 反例:
set user:1:name tom
set user:1:age 19
set user:1:favor football
正例:
hmset user:1 name tom age 19 favor football
- 【强制】:控制 key 生命周期,redis 不是垃圾站
建议使用 expire 设置过期时间(条件允许可以打散过期时间,防止集中过期),不过期的数据重点关注 idletime。如果业务强制需求不过期,请说明具体原因。
二 命令使用
1.【推荐】O(N)命令关注 N 的数量
例如 hgetall、lrange、smembers、zrange、sinter 等并非不能使用,但是需要明确 N 的值。有
遍历的需求可以使用 hscan、sscan、zscan 代替。
2.【推荐】:禁用命令
禁止线上使用 keys、flushall、flushdb 等,通过 redis 的 rename 机制禁掉命令,或者使用 scan
的方式渐进式处理。
3.【推荐】避免使用 select , 使用登录上去默认的 db0
redis 的多数据库较弱,使用数字进行区分,很多客户端支持较差,同时多业务用多数据库实际还是单线程处理,会有干扰。哨兵模式中不建议使用多 db,毕竟集群模式已经不能使用多 db。
4.【推荐】使用批量操作提高效率
- 原生命令是原子操作,pipeline 是非原子操作
- pipeline 可以打包不同的命令,原生不支持
- pipeline 需要客户端和服务端同时支持
- 原生命令: 如 mget、mset。
- 非原生命令: 可以使用 pipeline 提高效率。
- 但要注意控制一次批量操作的元素个数(例如 500 以内,实际也和元素字节数有关)。
5.【建议】Redis 事务功能较弱,不建议过多使用
Redis 事务功能不支持回滚,cluster 要求事务操作的 key 必须在一个 slot 上面。
6.【建议】Redis 集群版本在使用 Lua 上有特殊要求
- 所有 key 都应该由 KEYS 数组来传递,redis.call/pcall 里面调用的 redis 命令,key 的位置,必须是 KEYS array, 否则直接返回 error,
-ERR bad lua script for redis cluster, all the keys that the script uses should be passed using the KEYS array
- 所有 key,必须在 1 个 slot 上,否则直接返回 error,“-ERR eval/evalsha command keys must in same slot”
7.【建议】必要情况下使用 Monitor 命令时,要注意不要长时间使用,造成缓冲区溢出,尽而内存抖动
三 客户端使用
1.【推荐】
使用带有连接池的数据库,可以有效控制连接,同时提高效率,标准使用方式:
// 执行命令如下: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();}
2.【建议】
高并发下建议客户端添加熔断功能(例如 netflix hystrix)
3.【推荐】
设置合理的密码,如有必要可以使用 SSL 加密访问
4.【建议】
设置合理的密码,如有必要可以使用 SSL 加密访问
5.【建议】
根据自身业务类型,选好 maxmemory-policy(最大内存淘汰策略),设置好过期时间。
默认策略是 volatile-lru,即超过最大内存后,在过期键中使用 lru 算法进行 key 的剔除,保证不过期 数据不被删除,但是可能会出现 OOM 问题。
其他策略如下:
allkeys-lru:根据 LRU 算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。allkeys-random:随机删除所有键,直到腾出足够空间为止。volatile-random: 随机删除过期键,直到腾出足够空间为止。volatile-ttl:根据键值对象的 ttl 属性,删除最近将要过期数据。如果没有,回退到 noeviction
策略。noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息 "(error) OOM
command not allowed when used memory",此时 Redis 只响应读操作。
四 合理使用
1.【推荐】冷热数据分离,不要将所有数据全部都放到 Redis 中
虽然 Redis 支持持久化,但是 Redis 的数据存储全部都是在内存中的,成本昂贵。建议根据业务只将高频热数据存储到 Redis 中【QPS 大于 5000】,对于低频冷数据可以使用 MySQL/ElasticSearch/MongoDB 等基于磁盘的存储方式,不仅节省内存成本,而且数据量小在操作时速度更快、效率更高!
2.【推荐】不同的业务数据要分开存储
不要将不相关的业务数据都放到一个 Redis 实例中,建议新业务申请新的单独实例。因为 Redis 为单线程处理,独立存储会减少不同业务相互操作的影响,提高请求响应速度;同时也避免单个实例内存数据量膨胀过大,在出现异常情况时可以更快恢复服务!
3.【推荐】存储的 Key 一定要设置超时时间
如果应用将 Redis 定位为缓存 Cache 使用,对于存放的 Key 一定要设置超时时间!因为若不设置,这些 Key 会一直占用内存不释放,造成极大的浪费,而且随着时间的推移会导致内存占用越来越大,直到达到服务器内存上限!另外 Key 的超时长短要根据业务综合评估,而不是越长越好!
4.【推荐】对于必须要存储的大文本数据一定要压缩后存储
对于大文本【超过 500 字节】写入到 Redis 时,一定要压缩后存储!大文本数据存入 Redis,除了带来极大的内存占用外,在访问量高时,很容易就会将网卡流量占满,进而造成整个服务器上的所有服务不可用,并引发雪崩效应,造成各个系统瘫痪!
5.【强制】线上 Redis 禁止使用 Keys 正则匹配操作
Redis 是单线程处理,在线上 KEY 数量较多时,操作效率极低【时间复杂度为 O(N)】,该命令一旦执行会严重阻塞线上其它命令的正常请求,而且在高 QPS 情况下会直接造成 Redis 服务崩溃!如果有类似需求,请使用 scan 命令代替!
6.【推荐】谨慎全量操作 Hash、Set 等集合结构
在使用 HASH 结构存储对象属性时,开始只有有限的十几个 field,往往使用 HGETALL 获取所有成员,效率也很高,但是随着业务发展,会将 field 扩张到上百个甚至几百个,此时还使用 HGETALL 会出现效率急剧下降、网卡频繁打满等问题【时间复杂度 O(N)】, 此时建议根据业务拆分为多个 Hash 结构;或者如果大部分都是获取所有属性的操作, 可以将所有属性序列化为一个 STRING 类型存储!同样在使用 SMEMBERS 操作 SET 结构类型时也是相同的情况!
7.【建议】根据业务场景合理使用不同的数据结构类型
目前 Redis 支持的数据库结构类型较多:字符串(String),哈希(Hash),列表(List),集合(Set),有序集合(Sorted Set), Bitmap, HyperLogLog 和地理空间索引(geospatial)等, 需要根据业务场景选择合适的类型,常见的如:String 可以用作普通的 K -V、计数类;Hash 可以用作对象如商品、经纪人等,包含较多属性的信息;List 可以用作消息队列、粉丝 / 关注列表等;Set 可以用于推荐;Sorted Set 可以用于排行榜等!
五 相关工具
1.【推荐】:数据同步
redis 间数据同步可以使用:redis-port
2.【推荐】:big key 搜索
对于 Redis 主从版本可以通过 scan 命令进行扫描,对于集群版本提供了 ISCAN 命令进行扫描,命令规则 如下, 其中节点个数 node 可以通过 info 命令来获取到:
3.【推荐】:热点 key 寻找(内部实现使用 monitor,所以建议短时间使用,生产环境一般不建议使用)
六 附录:删除 bigkey
1. 下面操作可以使用 pipeline 加速。2. redis 4.0 已经支持 key 的异步删除,建议使用。
1. Hash 删除: hscan + hdel
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);
}
2. List 删除: ltrim
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);
}
3. Set 删除: sscan + srem
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);
}
4. SortedSet 删除: zscan + zrem
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);
}
参考《Redis 阿里云规范》总结整理
本文由博客一文多发平台 OpenWrite 发布!