redis水平扩展实践,完全配置,无需代码改动

设计思路

思路很简单,就是基于用户ID进行分库,将用户的ID字符串按照byte逐个计算ID对应的hash原值(一个数字,取绝对值,因为原始值可能过大溢出,变成负数),然后,再用这个hash原值对库的个数进行求模,这个模值就是库列表的索引值,也就选择好了用什么库。

hash算法

 1 /**
 2  * Created by chengsh05 on 2017/12/22.
 3  */
 4 public class BKDRHashUtil {
 5     public static int BKDRHash(char[] str) {
 6         int seed = 131; // 31 131 1313 13131 131313 etc..
 7         int hash = 0;
 8         for (int i = 0; i < str.length; i++) {
 9             hash = hash * seed + (str[i]);
10         }
11         return (hash & 0x7FFFFFFF);
12     }
13 }

对于redis的水平扩容,做到完全基于配置实现扩容,最好选择通过xml的配置的方式实现,因为配置在线上只需要改改配置信息,既可以重启服务器实现更新扩容。其实,其他中间件的扩容一样可以这么个逻辑实现。

XML配置

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"
 3        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4        xmlns:context="http://www.springframework.org/schema/context"
 5        xsi:schemaLocation="http://www.springframework.org/schema/beans
 6                           http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
 7                           http://www.springframework.org/schema/context
 8                           http://www.springframework.org/schema/context/spring-context-4.3.xsd">
 9 
10     <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
11         <property name="maxIdle" value="${spring.redis.pool.maxIdle}"></property>
12         <property name="minIdle" value="${spring.redis.pool.minIdle}"></property>
13         <property name="maxTotal" value="${spring.redis.pool.maxActive}"></property>
14         <property name="maxWaitMillis" value="${spring.redis.pool.maxWait}"></property>
15     </bean>
16 
17     <!--第一组主从redis-->
18     <bean id="jedisConnectionFactoryMaster1" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy" primary="true">
19         <property name="poolConfig" ref="jedisPoolConfig"></property>
20         <property name="hostName" value="${spring.master1.redis.hostName}"></property>
21         <property name="port" value="${spring.master1.redis.port}"></property>
22         <property name="database" value="${spring.redis.database}"></property>
23         <property name="timeout" value="${spring.redis.timeout}"></property>
24     </bean>
25     <bean id="redisTemplateMaster1" class="org.springframework.data.redis.core.RedisTemplate">
26         <property name="connectionFactory" ref="jedisConnectionFactoryMaster1"></property>
27     </bean>
28     <bean id="jedisConnectionFactorySlave1" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy">
29         <property name="poolConfig" ref="jedisPoolConfig"></property>
30         <property name="hostName" value="${spring.slave1.redis.hostName}"></property>
31         <property name="port" value="${spring.slave1.redis.port}"></property>
32         <property name="database" value="${spring.redis.database}"></property>
33         <property name="timeout" value="${spring.redis.timeout}"></property>
34     </bean>
35     <bean id="redisTemplateSlave1" class="org.springframework.data.redis.core.RedisTemplate">
36         <property name="connectionFactory" ref="jedisConnectionFactorySlave1"></property>
37     </bean>
38 
39     <!-- 第2组主从redis -->
40     <bean id="jedisConnectionFactoryMaster2" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy">
41         <property name="poolConfig" ref="jedisPoolConfig"></property>
42         <property name="hostName" value="${spring.master2.redis.hostName}"></property>
43         <property name="port" value="${spring.master2.redis.port}"></property>
44         <property name="database" value="${spring.redis.database}"></property>
45         <property name="timeout" value="${spring.redis.timeout}"></property>
46     </bean>
47     <bean id="redisTemplateMaster2" class="org.springframework.data.redis.core.RedisTemplate">
48         <property name="connectionFactory" ref="jedisConnectionFactoryMaster2"></property>
49     </bean>
50     <bean id="jedisConnectionFactorySlave2" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy">
51         <property name="poolConfig" ref="jedisPoolConfig"></property>
52         <property name="hostName" value="${spring.slave2.redis.hostName}"></property>
53         <property name="port" value="${spring.slave2.redis.port}"></property>
54         <property name="database" value="${spring.redis.database}"></property>
55         <property name="timeout" value="${spring.redis.timeout}"></property>
56     </bean>
57     <bean id="redisTemplateSlave2" class="org.springframework.data.redis.core.RedisTemplate">
58         <property name="connectionFactory" ref="jedisConnectionFactorySlave2"></property>
59     </bean>
60 
61     <bean id="commonRedisService" class="org.whuims.web.service.AutoScaleRedisService">
62         <!--
63               Modified: shihuc, 2017-12-21
64                         shihuc, 2018-01-02 全配置,无需计算中间参数,目的就是提升性能。
65               注意,这里的改造,是方便redis的水平扩展,目的是为了在增加redis主从服务器的时候,只需要修改一下此处的配置文件,然后重启应用即可。
66                  这里配置相对多了点,目的是换取性能。
67               另外:1. readRedisTemplateKeyInstancePairs,writeRedisTemplateKeyInstancePairs两个主要的键值结构配置读写实例表。
68                   2. 读写redis,主从关系,必须配对填写好,不要出现主从的错位配置。例如rw1、rr1表示第一组的写读关系。
69                   3. readRedisKeys列表取值必须和readRedisTemplateKeyInstancePairs的key值一样,writeRedisKeys列表的取值必须
70                   和writeRedisTemplateKeyInstancePairs的key值一样。
71         -->
72         <property name="readRedisTemplateKeyInstancePairs">
73             <map key-type="java.lang.String">
74                 <entry key="rr1" value-ref="redisTemplateSlave1"></entry>
75                 <entry key="rr2" value-ref="redisTemplateSlave2"></entry>
76             </map>
77         </property>
78         <property name="readRedisKeys">
79             <list>
80                 <value>rr1</value>
81                 <value>rr2</value>
82             </list>
83         </property>
84         <property name="writeRedisTemplateKeyInstancePairs">
85             <map key-type="java.lang.String">
86                 <entry key="rw1" value-ref="redisTemplateMaster1"></entry>
87                 <entry key="rw2" value-ref="redisTemplateMaster2"></entry>
88             </map>
89         </property>
90         <property name="writeRedisKeys">
91             <list>
92                 <value>rw1</value>
93                 <value>rw2</value>
94             </list>
95         </property>
96     </bean>
97 </beans>

其中用到的参数,通过spring的占位符逻辑,redis的配置数据来自配置文件,这里配置文件信息,简要示例(springboot的配置文件app.properties里面的局部):

#common part used in redis configuration for below multi redis
spring.redis.pool.maxActive=100
spring.redis.pool.maxWait=-1
spring.redis.pool.maxIdle=8
spring.redis.pool.minIdle=0
spring.redis.timeout=0
spring.redis.database=3

#how many redis group to use is depended your business. but, at least one master/slave configuration needed
#below is for master1 redis configuration
spring.master1.redis.hostName=100.126.22.177
spring.master1.redis.port=6379
#below is for slave1 redis configuration
spring.slave1.redis.hostName=100.126.22.178
spring.slave1.redis.port=6379

#below is for master2 redis configuration
spring.master2.redis.hostName=100.126.22.189
spring.master2.redis.port=6379
#below is for slave1 redis configuration
spring.slave2.redis.hostName=100.126.22.190
spring.slave2.redis.port=6379

springboot中Java启用配置

/**
 * Created by chengsh05 on 2017/12/22.
 *
 * 通过XML的方式进行redis的配置管理,目的在于方便容量扩缩的时候,只需要进行配置文件的变更即可,这样
 * 可以做到容量的水平管理,不需要动业务逻辑代码。
 *
 * 上线的时候,改改配置文件,再重启一下应用即可完成扩缩容。
 */
@Configuration
@ImportResource(value = {"file:${user.dir}/resources/spring-redis.xml"})
public class RedisXmlConfig {
}

分库服务

  1 /**
  2  * Created by chengsh05 on 2017/12/22.
  3  *
  4  * 方便redis组件的水平扩展,扩展的时候,主要改改spring-redis.xml以及app.properties配置文件,不需要动java
  5  * 代码,重启应用,即可实现扩容。
  6  */
  7 public class AutoScaleRedisService {
  8 
  9     Logger logger = Logger.getLogger(AutoScaleRedisService.class);
 10 
 11     /**
 12      * Added by shihuc, 2017-12-22
 13      * redis水平扩展,中间层抽象逻辑
 14      *
 15      * Modified by shihuc 2018-01-02
 16      * 将redis水平扩展部分,改成完全基于配置,不需要计算,应用层面,对于源的选取,完全就是读的操作,没有计算了,对于计算性能的提升有好处,配置相对麻烦一点。
 17      *
 18      * Key: rw1,rr1, and so on
 19      * value: RedisTemplate instance
 20      */
 21     private Map<String, RedisTemplate<String, Object>> readRedisTemplateKeyInstancePairs;
 22 
 23     private Map<String, RedisTemplate<String, Object>> writeRedisTemplateKeyInstancePairs;
 24 
 25     private List<String> readRedisKeys;
 26 
 27     private List<String> writeRedisKeys;
 28 
 29     public Map<String, RedisTemplate<String, Object>> getReadRedisTemplateKeyInstancePairs() {
 30         return readRedisTemplateKeyInstancePairs;
 31     }
 32 
 33     public void setReadRedisTemplateKeyInstancePairs(Map<String, RedisTemplate<String, Object>> readRedisTemplateKeyInstancePairs) {
 34         this.readRedisTemplateKeyInstancePairs = readRedisTemplateKeyInstancePairs;
 35     }
 36 
 37     public Map<String, RedisTemplate<String, Object>> getWriteRedisTemplateKeyInstancePairs() {
 38         return writeRedisTemplateKeyInstancePairs;
 39     }
 40 
 41     public void setWriteRedisTemplateKeyInstancePairs(Map<String, RedisTemplate<String, Object>> writeRedisTemplateKeyInstancePairs) {
 42         this.writeRedisTemplateKeyInstancePairs = writeRedisTemplateKeyInstancePairs;
 43     }
 44 
 45     public List<String> getReadRedisKeys() {
 46         return readRedisKeys;
 47     }
 48 
 49     public void setReadRedisKeys(List<String> readRedisKeys) {
 50         this.readRedisKeys = readRedisKeys;
 51     }
 52 
 53     public List<String> getWriteRedisKeys() {
 54         return writeRedisKeys;
 55     }
 56 
 57     public void setWriteRedisKeys(List<String> writeRedisKeys) {
 58         this.writeRedisKeys = writeRedisKeys;
 59     }
 60 
 61     /**
 62      * @author shihuc
 63      * @param userId
 64      * @return
 65      */
 66     private String getReadKey(String userId) {
 67         int hash = BKDRHashUtil.BKDRHash(userId.toCharArray());
 68         int abs = Math.abs(hash);
 69         int idx = abs % getReadRedisCount();
 70         logger.info("userId: " + userId + ", hash: " + hash + ", idx: " + idx);
 71         String insKey = getReadRedisKeys().get(idx);
 72         return insKey;
 73     }
 74 
 75     /**
 76      * @author shihuc
 77      * @param userId
 78      * @return
 79      */
 80     private String getWriteKey(String userId) {
 81         int hash = BKDRHashUtil.BKDRHash(userId.toCharArray());
 82         int abs = Math.abs(hash);
 83         int idx = abs % getWriteRedisCount();
 84         logger.info("userId: " + userId + ", hash: " + hash + ", idx: " + idx);
 85         String insKey = getWriteRedisKeys().get(idx);
 86         return insKey;
 87     }
 88 
 89     /**
 90      * @author shihuc
 91      * @return the count of read redis instance
 92      */
 93     public int getReadRedisCount() {
 94         return readRedisKeys.size();
 95     }
 96 
 97     /**
 98      * @author shihuc
 99      * @return the count of write redis instance
100      */
101     public int getWriteRedisCount() {
102         return writeRedisKeys.size();
103     }
104 
105     /**
106      * @author shihuc
107      * @param userId
108      * @param type
109      * @param log
110      * @return
111      */
112     public RedisTemplate<String, Object> getRedisTemplate(String userId, String type, boolean log){
113         return getRedisTemplate(userId, type, log, null);
114     }
115 
116     /**
117      * 获取redisTemplate实例
118      * @author shihuc
119      * @param userId
120      * @param type
121      * @param log
122      * @return
123      */
124     public RedisTemplate<String, Object> getRedisTemplate(String userId, String type, boolean log, String info){
125         String insKey = null;
126         RedisTemplate<String, Object> redisTemplate = null;
127         if(Constants.REDIS_TYPE_READ.equalsIgnoreCase(type)){
128             insKey = getReadKey(userId);
129             redisTemplate = readRedisTemplateKeyInstancePairs.get(insKey);
130         }else {
131             insKey = getWriteKey(userId);
132             redisTemplate = writeRedisTemplateKeyInstancePairs.get(insKey);
133         }
134         if (log) {
135             if(info != null) {
136                 logger.info("userId: " + userId + ", redis: " + insKey + ", type: " + type + ", info: " + info);
137             }else{
138                 logger.info("userId: " + userId + ", redis: " + insKey + ", type: " + type);
139             }
140         }
141         return redisTemplate;
142     }
143 
144     /**
145      * 用于校验配置的时候,读写实例的key值和键值列表的value之间是否是对应的关系。
146      */
147     @PostConstruct
148     public void init() throws Exception {
149         int ridx = 0;
150         for(Map.Entry<String, RedisTemplate<String, Object>> rele: readRedisTemplateKeyInstancePairs.entrySet()) {
151             String rkey = rele.getKey();
152             String trkey = readRedisKeys.get(ridx);
153             if(!rkey.equals(trkey)){
154                 throw new Exception("[read] redis group configuration error, order is not matched");
155             }
156             ridx++;
157         }
158         int widx = 0;
159         for(Map.Entry<String, RedisTemplate<String, Object>> wele: writeRedisTemplateKeyInstancePairs.entrySet()) {
160             String wkey = wele.getKey();
161             String twkey = writeRedisKeys.get(widx);
162             if(!wkey.equals(twkey)){
163                 throw new Exception("[write] redis group configuration error, order is not matched");
164             }
165             widx++;
166         }
167     }
168 }

使用案例

    @RequestMapping("/redischeck")
    @ResponseBody
    public String redisCheck(@RequestParam(value = "query") String query) {
        System.out.println("check:" + query);
        int rdc = autoScaleRedisService.getReadRedisCount();
        int wtc = autoScaleRedisService.getWriteRedisCount();

        RedisTemplate redisTemplate = autoScaleRedisService.getRedisTemplate(query, Constants.REDIS_TYPE_READ, true, "buildSession");
        RedisTemplate redisTemplate2 = autoScaleRedisService.getRedisTemplate(query, Constants.REDIS_TYPE_WRITE, true, "buildSession");
        return "rdc: " + rdc + ", wtc: " + wtc;
    }

整个思路和实现过程,其实非常通俗易懂,非常方便的用于各种中间件的场景,当然,若有特殊需求,也无外乎类似的逻辑。

若有什么不妥,欢迎探讨,若有更好的巧妙方案,也可以探讨!

转载请指明出处,谢谢!欢迎加关注!

原文地址:https://www.cnblogs.com/shihuc/p/8186370.html