上一節我們介紹了mariaDB集群的搭建,這一節我們介紹一下redis集群的搭建以及在springboot中使用redis集群。
一、redis集群的搭建
redis集群的搭建我們同樣沒有使用operator的形式,而是同樣手動搭建,基本上是按照下面兩篇博文中的步驟來的,在此再次感謝相關的作者:
https://blog.csdn.net/zhutongcloud/article/details/90768390
https://www.jianshu.com/p/a34790c730cf
總結起來的步驟就是下面幾步:
第一步:前期准備階段,跟上一節mariaDB集群一樣,使用nfs存儲來作為Redis的后端存儲,NFS的路徑設置為/appl/install(完全可以自行更改);創建StorageClass指定為nfs以便動態創建PVC和PV。
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: nfs
provisioner: mynfs
reclaimPolicy: Delete
volumeBindingMode: Immediate
第二步:創建redis專用的namespace以及用到的ConfigMap:
namespace:
kind: Namespace
apiVersion: v1
metadata:
name: demo-redis
ConfigMap:
kind: ConfigMap
apiVersion: v1
metadata:
name: redis-cluster-configmap
namespace: demo-redis
data:
redis.conf: |
appendonly yes
protected-mode no
cluster-enabled yes
cluster-config-file /var/lib/redis/nodes.conf
cluster-node-timeout 5000
dir /var/lib/redis
port 6379
注意:redis有兩種持久化存儲的格式,一種是AOF一種是RDB,我這里使用的是AOF,因此在redis.conf中添加了“appendonly yes”。
第三步:使用StatefulSet來創建redis的pod(注意:這里沒有遵從上面兩個鏈接博文中的先創建pv,pvc然后關聯pod,而是直接動態創建pvc和pv):
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-app
namespace: demo-redis
spec:
serviceName: redis-headless-service
replicas: 6
selector:
matchLabels:
app: redis
appCluster: redis-cluster
template:
metadata:
labels:
app: redis
appCluster: redis-cluster
spec:
terminationGracePeriodSeconds: 20
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- redis
topologyKey: kubernetes.io/hostname
containers:
- name: redis
image: "redis"
command:
- "redis-server"
args:
- "/etc/redis/redis.conf"
- "--protected-mode"
- "no"
resources:
requests:
cpu: 1000m
memory: 1Gi
limits:
cpu: 1000m
memory: 1Gi
ports:
- name: redis
containerPort: 6379
protocol: "TCP"
- name: cluster
containerPort: 16379
protocol: "TCP"
volumeMounts:
- name: redis-conf
mountPath: /etc/redis
- name: redis-data
mountPath: /var/lib/redis
volumes:
- name: redis-conf
configMap:
name: redis-cluster-configmap
items:
- key: redis.conf
path: redis.conf
volumeClaimTemplates:
- metadata:
name: redis-data
annotations:
volume.beta.kubernetes.io/storage-class: "nfs"
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
注意,我們創建了6個副本,符合我們redis集群3+3的配置。
第四步:創建headless service,同時創建提供與其他微服務或者外網訪問的service:
Headless service:
apiVersion: v1
kind: Service
metadata:
name: redis-headless-service-------與上面StatefulSet中的serviceName一致。
namespace: demo-redis
labels:
app: redis
spec:
ports:
- name: redis-port
port: 6379
clusterIP: None
selector:
app: redis
appCluster: redis-cluster
與其他微服務互通的sevice:
apiVersion: v1
kind: Service
metadata:
name: redis-access-service
namespace: demo-redis
labels:
app: redis
spec:
type: NodePort
ports:
- name: redis-port
protocol: TCP
port: 6379
targetPort: 6379
nodePort: 32379
selector:
app: redis
appCluster: redis-cluster
注意:這里如果redis集群不會對外提供服務的話,沒有必要使用NodePort這種方式,只分配內網訪問的port即可。
完成上面的4步並且執行這些yaml文件之后,我們僅僅是創建並啟動了6個redis的pod, 但是它們並沒有形成一個集群,下面的第五步才是通過配置將這6個pod構建成一個集群。
第五步:進入當前任何一個redis的pod中開始搭建cluster集群:
1. 進入pod中,執行命令類似於“kubectl exec -it redis-app-2 -n demo-redis /bin/bash”
2. 因為構建的時候需要使用dig命令,由於我們的yaml文件中使用的redis鏡像中沒有這個命令,需要自行安裝,如果你跟我一樣使用了公司服務器,訪問外網需要代理,那請首先在redis容器(ubuntu系統)中配置代理,修改/etc/apt/apt.conf添加:
Acquire::http::Proxy "xxx";
Acquire::https::Proxy "xxx";
3. 然后執行apt-get update以及apt-get install dnsutils安裝dig命令。
4. 在redis5之后的版本中,手動搭建redis集群已經不需要安裝redis-trib.rb,已經集成到redis-cli中(如果您安裝的redis鏡像版本是redis5之前的,那就需要自行安裝redis-trib.rb,使用redis-trib命令),因此確保下面的命令正確運行:
redis-cli --cluster create `dig +short redis-app-0.redis-headless-service.demo-redis.svc.cluster.local`:6379 `dig +short redis-app-1.redis-headless-service.demo-redis.svc.cluster.local`:6379 `dig +short redis-app-2.redis-headless-service.demo-redis.svc.cluster.local`:6379 `dig +short redis-app-3.redis-headless-service.demo-redis.svc.cluster.local`:6379 `dig +short redis-app-4.redis-headless-service.demo-redis.svc.cluster.local`:6379 `dig +short redis-app-5.redis-headless-service.demo-redis.svc.cluster.local`:6379 --cluster-replicas 1
注意:類似與`dig +short redis-app-0.redis-headless-service.demo-redis.svc.cluster.local`這種命令指的是動態獲得每一個pod的ip地址,因為pod的ip每次重啟都是不同的,因此一定不能hardcode,而使用StatefulSet以及headless service能為我們創建的每個pod分配一個固定的dns域名,格式為$podName.$headlessServiceName.$namespace.svc.cluster.local,這樣是不是就很好理解了呢?所以整個上面這條命令就是將每個pod的動態ip和端口構建成一個集群,后面的參數“--cluster-replicas 1”代表是一主一從的方式,所以最終就是三主三從。
5. 此時redis集群已經創建成功,為了驗證,登陸任何一個pod,執行下面的操作就能看到當前pod的角色以及整個cluster的信息:
/usr/local/bin/redis-cli -c
127.0.0.1:6379> cluster nodes
97a7d4758a3cd17a5a7a88cb0858e049d7116836 10.244.1.236:6379@16379 slave 2cad20ff68341b6fd77b617265922cc9f16ef26d 0 1636509315819 2 connected
3e18ab8e60d3f9189e34268ad68db6f8b9a763d7 10.244.2.219:6379@16379 master - 0 1636509317326 3 connected 10923-16383
fcdd840cc2e621e4ea720b5bf8f50b3b0b3f2e9a 10.244.1.240:6379@16379 myself,slave 3e18ab8e60d3f9189e34268ad68db6f8b9a763d7 0 1636509316000 3 connected
094ad7e577e813abc1596f47e57867f517f2920b 10.244.2.220:6379@16379 slave ee85f7ef9256585308195b494770d76721823702 0 1636509317828 1 connected
ee85f7ef9256585308195b494770d76721823702 10.244.1.235:6379@16379 master - 0 1636509316823 1 connected 0-5460
2cad20ff68341b6fd77b617265922cc9f16ef26d 10.244.1.241:6379@16379 master - 0 1636509316522 2 connected 5461-10922
127.0.0.1:6379> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:3
cluster_stats_messages_ping_sent:991
cluster_stats_messages_pong_sent:1063
cluster_stats_messages_meet_sent:1
cluster_stats_messages_sent:2055
cluster_stats_messages_ping_received:1063
cluster_stats_messages_pong_received:992
cluster_stats_messages_received:2055
127.0.0.1:6379> role
1) "slave"
2) "10.244.2.219"
3) (integer) 6379
4) "connected"
5) (integer) 840
最后補充一點我們創建的這套redis集群的信息,包括3個master和3個slave,3個master的slot(槽位,redis用0~16383個槽位來存儲key)分別為0~5460,5461~10922,10923~16383, 添加key的時候就會通過算法計算后加入對應槽位的master中並且同步到該master對應的slave中。
二、在springboot中訪問redis集群
Springboot中訪問redis集群也是相當簡單,首先在application.properties中配置redis集群的node信息、訪問的服務名以及訪問端口:
spring.redis.cluster.nodes=redis-app-0.redis-headless-service.demo-redis.svc.cluster.local:6379,redis-app-1.redis-headless-service.demo-redis.svc.cluster.local:6379,redis-app-2.redis-headless-service.demo-redis.svc.cluster.local:6379,redis-app-3.redis-headless-service.demo-redis.svc.cluster.local:6379,redis-app-4.redis-headless-service.demo-redis.svc.cluster.local:6379,redis-app-5.redis-headless-service.demo-redis.svc.cluster.local:6379
spring.redis.host=redis-access-service.demo-redis
spring.redis.port=6379
其次在pom.xml中加入redis的依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
再次,配置redis的全局配置文件RedisConfig.java,其中重寫了RedisTemplate的實例方法,方法中定義了redis的key和value的序列化器。
package com.example.demo.configuration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(factory); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.afterPropertiesSet(); return redisTemplate; } }
再再次,按照處理此類中間件的慣例,我們應該將redis的常用操作封裝成一個工具類,此類工具類網上比比皆是,大家自行搜索,我列出我使用的工具類:
RedisClientUtil.java
package com.example.demo.utils; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; @Component public class RedisClientUtil { @Autowired private RedisTemplate<String, Object> redisTemplate;--------注意這里使用的就是上面重寫的實例方法 /** * 指定緩存失效時間 */ public boolean expire(String key, long time) { try { if(time > 0) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } return true; } catch(Exception e) { e.printStackTrace(); return false; } } /** * 根據key獲取過期時間 */ public long getExpire(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } /** * 判斷key是否存在 * * @param key 鍵 * @return true 存在 false不存在 */ public boolean hasKey(String key) { try { return redisTemplate.hasKey(key); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 刪除緩存 */ @SuppressWarnings("unchecked") public void del(String... key) { if(key != null && key.length > 0) { if(key.length == 1) { redisTemplate.delete(key[0]); } else { redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key)); } } } /** * 普通緩存獲取 * * @param key 鍵 * @return 值 */ public Object get(String key) { return key == null ? null : redisTemplate.opsForValue().get(key); } /** * 普通緩存放入 * * @param key 鍵 * @param value 值 * @return true成功 false失敗 */ public boolean set(String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 普通緩存放入並設置時間 * * @param key 鍵 * @param value 值 * @param time 時間(秒) time要大於0 如果time小於等於0 將設置無限期 * @return true成功 false 失敗 */ public boolean set(String key, Object value, long time) { try { if (time > 0) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } else { set(key, value); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 遞增 * * @param key 鍵 * @param delta 要增加幾(大於0) * @return */ public long incr(String key, long delta) { if (delta < 0) { throw new RuntimeException("遞增因子必須大於0"); } return redisTemplate.opsForValue().increment(key, delta); } /** * 遞減 * * @param key 鍵 * @param delta 要減少幾(小於0) * @return */ public long decr(String key, long delta) { if (delta < 0) { throw new RuntimeException("遞減因子必須大於0"); } return redisTemplate.opsForValue().increment(key, -delta); } // ================================Map================================= /** * HashGet * * @param key 鍵 不能為null * @param item 項 不能為null * @return 值 */ public Object hget(String key, String item) { return redisTemplate.opsForHash().get(key, item); } /** * 獲取hashKey對應的所有鍵值 * * @param key 鍵 * @return 對應的多個鍵值 */ public Map<Object, Object> hmget(String key) { return redisTemplate.opsForHash().entries(key); } /** * HashSet * * @param key 鍵 * @param map 對應多個鍵值 * @return true 成功 false 失敗 */ public boolean hmset(String key, Map<String, Object> map) { try { redisTemplate.opsForHash().putAll(key, map); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * HashSet 並設置時間 * * @param key 鍵 * @param map 對應多個鍵值 * @param time 時間(秒) * @return true成功 false失敗 */ public boolean hmset(String key, Map<String, Object> map, long time) { try { redisTemplate.opsForHash().putAll(key, map); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一張hash表中放入數據,如果不存在將創建 * * @param key 鍵 * @param item 項 * @param value 值 * @return true 成功 false失敗 */ public boolean hset(String key, String item, Object value) { try { redisTemplate.opsForHash().put(key, item, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一張hash表中放入數據,如果不存在將創建 * * @param key 鍵 * @param item 項 * @param value 值 * @param time 時間(秒) 注意:如果已存在的hash表有時間,這里將會替換原有的時間 * @return true 成功 false失敗 */ public boolean hset(String key, String item, Object value, long time) { try { redisTemplate.opsForHash().put(key, item, value); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 刪除hash表中的值 * * @param key 鍵 不能為null * @param item 項 可以使多個 不能為null */ public void hdel(String key, Object... item) { redisTemplate.opsForHash().delete(key, item); } /** * 判斷hash表中是否有該項的值 * * @param key 鍵 不能為null * @param item 項 不能為null * @return true 存在 false不存在 */ public boolean hHasKey(String key, String item) { return redisTemplate.opsForHash().hasKey(key, item); } /** * hash遞增 如果不存在,就會創建一個 並把新增后的值返回 * * @param key 鍵 * @param item 項 * @param by 要增加幾(大於0) * @return */ public double hincr(String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, by); } /** * hash遞減 * * @param key 鍵 * @param item 項 * @param by 要減少記(小於0) * @return */ public double hdecr(String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, -by); } // ============================set============================= /** * 根據key獲取Set中的所有值 * * @param key 鍵 * @return */ public Set<Object> sGet(String key) { try { return redisTemplate.opsForSet().members(key); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 根據value從一個set中查詢,是否存在 * * @param key 鍵 * @param value 值 * @return true 存在 false不存在 */ public boolean sHasKey(String key, Object value) { try { return redisTemplate.opsForSet().isMember(key, value); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 將數據放入set緩存 * * @param key 鍵 * @param values 值 可以是多個 * @return 成功個數 */ public long sSet(String key, Object... values) { try { return redisTemplate.opsForSet().add(key, values); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 將set數據放入緩存 * * @param key 鍵 * @param time 時間(秒) * @param values 值 可以是多個 * @return 成功個數 */ public long sSetAndTime(String key, long time, Object... values) { try { Long count = redisTemplate.opsForSet().add(key, values); if (time > 0) expire(key, time); return count; } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 獲取set緩存的長度 * * @param key 鍵 * @return */ public long sGetSetSize(String key) { try { return redisTemplate.opsForSet().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 移除值為value的 * * @param key 鍵 * @param values 值 可以是多個 * @return 移除的個數 */ public long setRemove(String key, Object... values) { try { Long count = redisTemplate.opsForSet().remove(key, values); return count; } catch (Exception e) { e.printStackTrace(); return 0; } } // ===============================list================================= /** * 獲取list緩存的內容 * * @param key 鍵 * @param start 開始 * @param end 結束 0 到 -1代表所有值 * @return */ public List<Object> lGet(String key, long start, long end) { try { return redisTemplate.opsForList().range(key, start, end); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 獲取list緩存的長度 * * @param key 鍵 * @return */ public long lGetListSize(String key) { try { return redisTemplate.opsForList().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 通過索引 獲取list中的值 * * @param key 鍵 * @param index 索引 index>0時, 0 表頭,1 第二個元素,依次類推;index<0時,-1,表尾,-2倒數第二個元素,依次類推 * @return */ public Object lGetIndex(String key, long index) { try { return redisTemplate.opsForList().index(key, index); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 將list放入緩存 * * @param key 鍵 * @param value 值 * @return */ public boolean lSet(String key, Object value) { try { redisTemplate.opsForList().rightPush(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 將list放入緩存 * * @param key 鍵 * @param value 值 * @param time 時間(秒) * @return */ public boolean lSet(String key, Object value, long time) { try { redisTemplate.opsForList().rightPush(key, value); if (time > 0) expire(key, time); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 將list放入緩存 * * @param key 鍵 * @param value 值 * @return */ public boolean lSet(String key, List<Object> value) { try { redisTemplate.opsForList().rightPushAll(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 將list放入緩存 * * @param key 鍵 * @param value 值 * @param time 時間(秒) * @return */ public boolean lSet(String key, List<Object> value, long time) { try { redisTemplate.opsForList().rightPushAll(key, value); if (time > 0) expire(key, time); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根據索引修改list中的某條數據 * * @param key 鍵 * @param index 索引 * @param value 值 * @return */ public boolean lUpdateIndex(String key, long index, Object value) { try { redisTemplate.opsForList().set(key, index, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 移除N個值為value * * @param key 鍵 * @param count 移除多少個 * @param value 值 * @return 移除的個數 */ public long lRemove(String key, long count, Object value) { try { Long remove = redisTemplate.opsForList().remove(key, count, value); return remove; } catch (Exception e) { e.printStackTrace(); return 0; } } }
最后,就是實際的訪問調用:
@RequestMapping(value = "/run", method = RequestMethod.POST) public Result Run(@RequestBody String jsonString) { try { String redisKey = UUID.randomUUID().toString().replaceAll("-", ""); JSONObject jsonObject = JSONObject.parseObject(jsonString); String caseName = jsonObject.getString("CaseName"); List<String> logList = new ArrayList<>(); logList.add("Ready to execute the case: "+caseName);
//insert into redis Map<String, Object>mapForRedis = new HashMap<>(); mapForRedis.put("jobID", redisKafkaKey); mapForRedis.put("caseName", caseName); mapForRedis.put("logList", logList); redisClientUtil.hmset(redisKey, mapForRedis); ......
//add redisKey into jsonObject and pass to other micro-service jsonObject.put("redisKey", redisKey ); String passStrToExecute = JSONObject.toJSONString(jsonObject); String executeUrl = "http://execute-service:9000/Execute/runSingleCase"; String resForExecute = OkHttpClientUtil.doPostJson(executeUrl, null, passStrToExecute); if(resForExecute == "") { redisClientUtil.del(redisKafkaKey); return ResultFactory.buildFailResult("Execute-service throw error!"); } return ResultFactory.buildSuccessResult(null); }
而其他微服務如果需要從redis集群中取數據也只需要簡單的調用即可:
OK,到此為止,您是否覺得redis集群的搭建和使用很簡單呢?其實主要是因為我們的應用場景簡單,讓我們過濾掉了一下redis集群更強大的功能比如說哨兵模式~~