設計思路
思路很簡單,就是基於用戶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; }
整個思路和實現過程,其實非常通俗易懂,非常方便的用於各種中間件的場景,當然,若有特殊需求,也無外乎類似的邏輯。
若有什么不妥,歡迎探討,若有更好的巧妙方案,也可以探討!
轉載請指明出處,謝謝!歡迎加關注!