之前做了一个Redis的集群方案,跑了小半年,线上运行的很稳定
差不多可以跟大家分享下经验,前面写了一篇文章 数据在线服务的一些探索经验,可以做为背景阅读
应用
我们的Redis集群主要承担了以下服务:
1. 实时推荐
2. 用户画像
3. 诚信分值服务
集群状况
集群峰值QPS 1W左右,RW响应时间999线在1ms左右
整个集群:
1. Redis节点: 8台物理机;每台128G内存;每台机器上8个instance
2. Sentienl:3台虚拟机
集群方案
Redis Node由一组Redis Instance组成,一组Redis Instatnce可以有一个Master Instance,多个Slave Instance
Redis官方的cluster还在beta版本,参看Redis cluster tutorial在做调研的时候,曾经特别关注过KeepAlived+VIP 和 Twemproxy
不过最后还是决定基于Redis Sentinel实现一套,整个项目大概在1人/1个半月
整体设计
1. 数据Hash分布在不同的Redis Instatnce上
2. M/S的切换采用Sentinel
3. 写:只会写master Instance,从sentinel获取当前的master Instane
4. 读:从Redis Node中基于权重选取一个Redis Instance读取,失败/超时则轮询其他Instance
5. 通过RPC服务访问,RPC server端封装了Redis客户端,客户端基于jedis开发
6. 批量写/删除:不保证事务
RedisKey
1. public class RedisKey implements Serializable{
2. private static final long serialVersionUID = 1L;
3.
4. //每个业务不同的family
5. private String family;
6.
7. private String key;
8.
9. ......
10. //物理保存在Redis上的key为经过MurmurHash之后的值
11. private String makeRedisHashKey(){
12. return String.valueOf(MurmurHash.hash64(makeRedisKeyString()));
13. }
14.
15. //ReidsKey由family.key组成
16. private String makeRedisKeyString(){
17. return family +":"+ key;
18. }
19.
20. //返回用户的经过Hash之后RedisKey
21. public String getRedisKey(){
22. return makeRedisHashKey();
23. }
24. .....
25. }
Family的存在时为了避免多个业务key冲突,给每个业务定义自己独立的Faimily
出于性能考虑,参考Redis存储设计,实际保存在Redis上的key为经过hash之后的值
接口
目前支持的接口包括:
1. public interface RedisUseInterface{
2. /**
3. * 通过RedisKey获取value
4. *
5. * @param redisKey
6. * redis中的key
7. * @return
8. * 成功返回value,查询不到返回NULL
9. */
10. public String get(final RedisKey redisKey) throws Exception;
11.
12. /**
13. * 插入<k,v>数据到Redis
14. *
15. * @param redisKey
16. * the redis key
17. * @param value
18. * the redis value
19. * @return
20. * 成功返回"OK",插入失败返回NULL
21. */
22. public String set(final RedisKey redisKey, final String value) throws Exception;
23.
24. /**
25. * 批量写入数据到Redis
26. *
27. * @param redisKeys
28. * the redis key list
29. * @param values
30. * the redis value list
31. * @return
32. * 成功返回"OK",插入失败返回NULL
33. */
34. public String mset(final ArrayList<RedisKey> redisKeys, final ArrayList<String> values) throws Exception;
35.
36.
37. /**
38. * 从Redis中删除一条数据
39. *
40. * @param redisKey
41. * the redis key
42. * @return
43. * an integer greater than 0 if one or more keys were removed 0 if none of the specified key existed
44. */
45. public Long del(RedisKey redisKey) throws Exception;
46.
47. /**
48. * 从Redis中批量删除数据
49. *
50. * @param redisKey
51. * the redis key
52. * @return
53. * 返回成功删除的数据条数
54. */
55. public Long del(ArrayList<RedisKey> redisKeys) throws Exception;
56.
57. /**
58. * 插入<k,v>数据到Redis
59. *
60. * @param redisKey
61. * the redis key
62. * @param value
63. * the redis value
64. * @return
65. * 成功返回"OK",插入失败返回NULL
66. */
67. public String setByte(final RedisKey redisKey, final byte[] value) throws Exception;
68.
69. /**
70. * 插入<k,v>数据到Redis
71. *
72. * @param redisKey
73. * the redis key
74. * @param value
75. * the redis value
76. * @return
77. * 成功返回"OK",插入失败返回NULL
78. */
79. public String setByte(final String redisKey, final byte[] value) throws Exception;
80.
81. /**
82. * 通过RedisKey获取value
83. *
84. * @param redisKey
85. * redis中的key
86. * @return
87. * 成功返回value,查询不到返回NULL
88. */
89. public byte[] getByte(final RedisKey redisKey) throws Exception;
90.
91. /**
92. * 在指定key上设置超时时间
93. *
94. * @param redisKey
95. * the redis key
96. * @param seconds
97. * the expire seconds
98. * @return
99. * 1:success, 0:failed
100. */
101. public Long expire(RedisKey redisKey, int seconds) throws Exception;
102. }
写Redis流程
1. 计算Redis Key Hash值
2. 根据Hash值获取Redis Node编号
3. 从sentinel获取Redis Node的Master
4. 写数据到Redis
1. //获取写哪个Redis Node
2. int slot = getSlot(keyHash);
3. RedisDataNode redisNode = rdList.get(slot);
4.
5. //写Master
6. JedisSentinelPool jp = redisNode.getSentinelPool();
7. Jedis je = null;
8. boolean success = true;
9. try {
10. je = jp.getResource();
11. return je.set(key, value);
12. } catch (Exception e) {
13. "Maybe master is down", e);
14. e.printStackTrace();
15. false;
16. if (je != null)
17. jp.returnBrokenResource(je);
18. throw e;
19. } finally {
20. if (success && je != null) {
21. jp.returnResource(je);
22. }
23. }
读流程
1. 计算Redis Key Hash值
2. 根据Hash值获取Redis Node编号
3. 根据权重选取一个Redis Instatnce
4. 轮询读
1. //获取读哪个Redis Node
2. int slot = getSlot(keyHash);
3. RedisDataNode redisNode = rdList.get(slot);
4.
5. //根据权重选取一个工作Instatnce
6. int rn = redisNode.getWorkInstance();
7.
8. //轮询
9. int cursor = rn;
10. do {
11. try {
12. JedisPool jp = redisNode.getInstance(cursor).getJp();
13. return getImpl(jp, key);
14. catch (Exception e) {
15. "Maybe a redis instance is down, slot : [" + slot + "]" + e);
16. e.printStackTrace();
17. 1) % redisNode.getInstanceCount();
18. if(cursor == rn){
19. throw e;
20. }
21. }
22. } while (cursor != rn);
权重计算
初始化的时候,会给每个Redis Instatnce赋一个权重值weight
根据权重获取Redis Instance的代码:
1. public int getWorkInstance() {
2. //没有定义weight,则完全随机选取一个redis instance
3. if(maxWeight == 0){
4. return (int) (Math.random() * RANDOM_SIZE % redisInstanceList.size());
5. }
6.
7. //获取随机数
8. int rand = (int) (Math.random() * RANDOM_SIZE % maxWeight);
9. int sum = 0;
10.
11. //选取Redis Instance
12. for (int i = 0; i < redisInstanceList.size(); i++) {
13. sum += redisInstanceList.get(i).getWeight();
14. if (rand < sum) {
15. return i;
16. }
17. }
18.
19. return 0;
20. }
21.