分布式技术入坑指南(二)

分布式技术入坑指南(二)

10.Redis集群的搭建及业务实现

Redis集群的搭建及业务实现

redis集群(Redis-cluster)架构图

Alt text

环境准备

至少3个节点,为了集群的高可用,为每一个节点增加一个备份机。(6台服务器)。
搭建伪分布式集群方案:在一台机器里面运行6个redis实例。端口需要不同(7001-7006)

1、使用ruby脚本搭建集群。需要ruby的运行环境。
安装ruby:

1
2
yum install ruby
yum install rubygems

2、从官网下载 redis-3.0.4.gem 并上传到 linux 中

地址:https://rubygems.org/gems/redis/versions

3、安装ruby运行时所使用的包

1
gem install redis-3.0.0.gem

搭建步骤

需要6台redis服务器。搭建伪分布式。
需要6个redis实例。
需要运行在不同的端口7001-7006
注意:搭建前 如果节点里有数据,需要删除(rdb文件,aof文件)。

第一步:创建6个redis实例,每个实例运行在不同的端口。需要修改redis.conf配置文件。配置文件中还需要把cluster-enabled yes前的注释去掉。

1
2
3
4
5
6
7
8
9
10
[root@localhost redis-cluster]# pwd
/usr/local/redis-cluster
[root@localhost redis-cluster]# ll
总用量 80
drwxr-xr-x. 3 root root 4096 7月 10 21:12 redis01
drwxr-xr-x. 3 root root 4096 7月 10 21:53 redis02
drwxr-xr-x. 3 root root 4096 7月 10 21:53 redis03
drwxr-xr-x. 3 root root 4096 7月 10 21:53 redis04
drwxr-xr-x. 3 root root 4096 7月 10 21:53 redis05
drwxr-xr-x. 3 root root 4096 7月 10 21:54 redis06
每个实例内容如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@localhost redis-cluster]# cd redis01
[root@localhost redis01]# ll
总用量 4
drwxr-xr-x. 2 root root 4096 7月 10 22:39 bin
[root@localhost redis01]# cd bin
[root@localhost bin]# ll
总用量 15508
-rw-r--r--. 1 root root 18 7月 10 22:39 dump.rdb
-rw-r--r--. 1 root root 769 7月 10 22:02 nodes.conf
-rwxr-xr-x. 1 root root 4589155 7月 10 21:12 redis-benchmark
-rwxr-xr-x. 1 root root 22217 7月 10 21:12 redis-check-aof
-rwxr-xr-x. 1 root root 45435 7月 10 21:12 redis-check-dump
-rwxr-xr-x. 1 root root 4693114 7月 10 21:12 redis-cli
-rw-r--r--. 1 root root 41391 7月 10 21:14 redis.conf
lrwxrwxrwx. 1 root root 12 7月 10 21:12 redis-sentinel -> redis-server
-rwxr-xr-x. 1 root root 6466389 7月 10 21:12 redis-server
实际上一个实例就是一个redis安装文件(不是安装包),为每个实例复制一份redis.conf,分别配置不同的端口、cluster-enabled yes注释打开。

第二步:启动每个redis实例。

通过脚本批量启动全部服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
vim  redis-cluster-start-all.sh 

添加如下:
cd /usr/local/redis-cluster/redis01/bin
./redis-server redis.conf

cd /usr/local/redis-cluster/redis02/bin
./redis-server redis.conf

cd /usr/local/redis-cluster/redis03/bin
./redis-server redis.conf

cd /usr/local/redis-cluster/redis04/bin
./redis-server redis.conf

cd /usr/local/redis-cluster/redis05/bin
./redis-server redis.conf

cd /usr/local/redis-cluster/redis06/bin
./redis-server redis.conf
然后授予可执行权限、执行即可。
1
2
3
4
5
6
7
8
9
[root@localhost redis-cluster]# ./redis-cluster-start-all.sh 
[root@localhost redis-cluster]# ps -ef |grep redis
root 4001 1 10 22:14 ? 00:00:00 ./redis-server *:7001 [cluster]
root 4003 1 8 22:14 ? 00:00:00 ./redis-server *:7002 [cluster]
root 4009 1 7 22:14 ? 00:00:00 ./redis-server *:7003 [cluster]
root 4013 1 2 22:14 ? 00:00:00 ./redis-server *:7004 [cluster]
root 4017 1 2 22:14 ? 00:00:00 ./redis-server *:7005 [cluster]
root 4021 1 2 22:14 ? 00:00:00 ./redis-server *:7006 [cluster]
root 4028 3302 0 22:14 pts/0 00:00:00 grep redis

第三步:使用ruby脚本搭建集群。

  • 从Redis解压目录下的src下的拷贝redis-trib.rb文件到redis-cluster目录中

  • 执行创建

1
./redis-trib.rb create --replicas 1 192.168.184.130:7001 192.168.184.130:7002 192.168.184.130:7003 192.168.184.130:7004 192.168.184.130:7005  192.168.184.130:7006

第四步 创建关闭集群的脚本:(不是必须的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vim 命令创建一个文件 redis-cluster-stop-all.sh 

cd /usr/local/redis-cluster/redis01/bin
./redis-cli -p 7001 shutdown

cd /usr/local/redis-cluster/redis02/bin
./redis-cli -p 7002 shutdown

cd /usr/local/redis-cluster/redis03/bin
./redis-cli -p 7003 shutdown

cd /usr/local/redis-cluster/redis04/bin
./redis-cli -p 7004 shutdown

cd /usr/local/redis-cluster/redis05/bin
./redis-cli -p 7005 shutdown

cd /usr/local/redis-cluster/redis06/bin
./redis-cli -p 7006 shutdown
然后授予可执行权限、执行即可。

集群使用

Redis-cli连接集群。-c:代表连接的是redis集群

1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost bin]# ./redis-cli -p 7001 -c
127.0.0.1:7001> set adsadsafd jkljlkjl
-> Redirected to slot [16142] located at 192.168.184.130:7006
OK
192.168.184.130:7006> get adsadsafd
"jkljlkjl"
192.168.184.130:7006> set key2 v2
-> Redirected to slot [4998] located at 192.168.184.130:7001
OK
192.168.184.130:7001> get key2
"v2"
192.168.184.130:7001>

Jedis客户端连接Redis集群服务器

注意:开放集群的7001-7006端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void testJedisCluster() throws Exception {
// 第一步:使用JedisCluster对象。需要一个Set<HostAndPort>参数。Redis节点的列表。
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("192.168.184.130", 7001));
nodes.add(new HostAndPort("192.168.184.130", 7002));
nodes.add(new HostAndPort("192.168.184.130", 7003));
nodes.add(new HostAndPort("192.168.184.130", 7004));
nodes.add(new HostAndPort("192.168.184.130", 7005));
nodes.add(new HostAndPort("192.168.184.130", 7006));
JedisCluster jedisCluster = new JedisCluster(nodes);
// 第二步:直接使用JedisCluster对象操作redis。在系统中单例存在。
jedisCluster.set("hello", "100");
String result = jedisCluster.get("hello");
// 第三步:打印结果
System.out.println(result);
// 第四步:系统关闭前,关闭JedisCluster对象。
jedisCluster.close();
}

实现业务

因为集群是比较消耗成本的,所以在实际开发中,一般生产环境使用集群,开发环境使用单机版。
我们在项目整合中都需要有。
可以开发一个接口,有单机版的实现类和集群版的实现类。使用时可以面向接口开发,不影响业务逻辑,使用spring管理实现类,部署时切换实现类即可。

接口封装

常用的操作redis的方法抽取出一个接口,分别对应单机版和集群版创建两个实现类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package xyz.taotao.content.service;

/**
* Created with IntelliJ IDEA.
* Jedis接口
* @Author: yu_zh
* @DateTime: 2018/07/14 23:32
*/
public interface JedisClient {

String set(String key, String value);
String get(String key);
Boolean exists(String key);
Long expire(String key, int seconds);
Long ttl(String key);
Long incr(String key);
Long hset(String key, String field, String value);
String hget(String key, String field);
Long hdel(String key, String... field);

}

单机版实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package xyz.taotao.content.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import xyz.taotao.content.service.JedisClient;

/**
* Created with IntelliJ IDEA.
* 单机版实现
* @Author: yu_zh
* @DateTime: 2018/07/14 23:35
*/
public class JedisClientPool implements JedisClient {

@Autowired
private JedisPool jedisPool;

@Override
public String set(String key, String value) {
Jedis jedis = jedisPool.getResource();
String result = jedis.set(key, value);
jedis.close();
return result;
}

@Override
public String get(String key) {
Jedis jedis = jedisPool.getResource();
String result = jedis.get(key);
jedis.close();
return result;
}

@Override
public Boolean exists(String key) {
Jedis jedis = jedisPool.getResource();
Boolean result = jedis.exists(key);
jedis.close();
return result;
}

@Override
public Long expire(String key, int seconds) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.expire(key, seconds);
jedis.close();
return result;
}

@Override
public Long ttl(String key) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.ttl(key);
jedis.close();
return result;
}

@Override
public Long incr(String key) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.incr(key);
jedis.close();
return result;
}

@Override
public Long hset(String key, String field, String value) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.hset(key, field, value);
jedis.close();
return result;
}

@Override
public String hget(String key, String field) {
Jedis jedis = jedisPool.getResource();
String result = jedis.hget(key, field);
jedis.close();
return result;
}

@Override
public Long hdel(String key, String... field) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.hdel(key, field);
jedis.close();
return result;
}

}

集群版实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package xyz.taotao.content.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import redis.clients.jedis.JedisCluster;
import xyz.taotao.content.service.JedisClient;

/**
* Created with IntelliJ IDEA.
* 集群版实现类
* @Author: yu_zh
* @DateTime: 2018/07/14 23:44
*/
public class JedisClientCluster implements JedisClient {

@Autowired
private JedisCluster jedisCluster;

@Override
public String set(String key, String value) {
return jedisCluster.set(key, value);
}

@Override
public String get(String key) {
return jedisCluster.get(key);
}

@Override
public Boolean exists(String key) {
return jedisCluster.exists(key);
}

@Override
public Long expire(String key, int seconds) {
return jedisCluster.expire(key, seconds);
}

@Override
public Long ttl(String key) {
return jedisCluster.ttl(key);
}

@Override
public Long incr(String key) {
return jedisCluster.incr(key);
}

@Override
public Long hset(String key, String field, String value) {
return jedisCluster.hset(key, field, value);
}

@Override
public String hget(String key, String field) {
return jedisCluster.hget(key, field);
}

@Override
public Long hdel(String key, String... field) {
return jedisCluster.hdel(key, field);
}
}

配置:spring-redis.xml
注意:单机版和集群版只能放开一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!-- 用于激活已经注册的Bean -->
<context:annotation-config></context:annotation-config>

<!-- 配置单机版的连接 -->
<bean id="jedisPool" class="redis.clients.jedis.JedisPool">
<constructor-arg name="host" value="192.168.184.130"></constructor-arg>
<constructor-arg name="port" value="6379"></constructor-arg>
</bean>
<!-- 创建单机版的实现类 -->
<bean id="jedisClientPool" class="xyz.taotao.content.service.impl.JedisClientPool"/>

<!-------------------------------------------------------->

<!-- 配置集群版 -->
<bean class="redis.clients.jedis.JedisCluster">
<constructor-arg name="nodes">
<set>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="192.168.184.130"></constructor-arg>
<constructor-arg name="port" value="7001"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="192.168.184.130"></constructor-arg>
<constructor-arg name="port" value="7002"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="192.168.184.130"></constructor-arg>
<constructor-arg name="port" value="7003"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="192.168.184.130"></constructor-arg>
<constructor-arg name="port" value="7004"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="192.168.184.130"></constructor-arg>
<constructor-arg name="port" value="7005"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="192.168.184.130"></constructor-arg>
<constructor-arg name="port" value="7006"></constructor-arg>
</bean>
</set>
</constructor-arg>
</bean>
<!-- 创建集群版的实现类 -->
<bean class="xyz.taotao.content.service.impl.JedisClientCluster"></bean>

封装代码测试

redis客户端Bean只在spring-redis.xml中注册,所以只需加载这个配置文件,注意:需要激活已经注册的Bean才能注入到属性。

1
2
3
4
5
6
7
8
9
10
@Test
public void testJedisClient() throws Exception {
//初始化Spring容器
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring-redis.xml");
//从容器中获得JedisClient对象(接口)
JedisClient jedisClient = applicationContext.getBean(JedisClient.class);
jedisClient.set("testJedis", "500");
String result = jedisClient.get("testJedis");
System.out.println(result);
}

为查询添加缓存

创建Json工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package xyz.taotao.utils;

import java.util.List;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
* 使用 jackson-databind 工具包封装Json
*/
public class JsonUtils {

// 定义jackson对象
private static final ObjectMapper MAPPER = new ObjectMapper();

/**
* 将对象转换成json字符串。
*
* @param data
* @return
*/
public static String objectToJson(Object data) {
try {
String string = MAPPER.writeValueAsString(data);
return string;
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}

/**
* 将json结果集转化为对象
*
* @param jsonData json数据
* @param beanType 对象中的object类型
* @return
*/
public static <T> T jsonToPojo(String jsonData, Class<T> beanType) {
try {
T t = MAPPER.readValue(jsonData, beanType);
return t;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

/**
* 将json数据转换成pojo对象list
*
* @param jsonData
* @param beanType
* @return
*/
public static <T> List<T> jsonToList(String jsonData, Class<T> beanType) {
JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType);
try {
List<T> list = MAPPER.readValue(jsonData, javaType);
return list;
} catch (Exception e) {
e.printStackTrace();
}

return null;
}

}

添加缓存,不能影响业务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package xyz.taotao.service.impl;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import xyz.taotao.mapper.TbItemMapper;
import xyz.taotao.pojo.EasyUIDataGridResult;
import xyz.taotao.pojo.TbItem;
import xyz.taotao.pojo.TbItemExample;
import xyz.taotao.redis.JedisClient;
import xyz.taotao.service.ItemService;
import xyz.taotao.utils.JsonUtils;

import java.util.List;

/**
* Created with IntelliJ IDEA.
*
* @Author: yu_zh
* @DateTime: 2018/07/05 19:02
*/
@Service
public class ItemServiceImpl implements ItemService {
@Autowired
TbItemMapper tbItemMapper;
@Autowired
private JedisClient jedisClient;
@Value("ITEM_LIST_KEY")
private String ITEM_LIST_KEY;

/**
* 为查询列表项添加缓存
* 结构:Hash
* <p>
* item_list_key
* |
* 1,50 —— EasyUIDataGridResult Json字符串
* 2,50 —— EasyUIDataGridResult Json字符串
* 3,50 —— EasyUIDataGridResult Json字符串
* <p>
* 缓存不能影响业务流程,用try-catch起来。
*
* @param page 查询页码
* @param rows 查询行数
* @return EasyUIDataGridResult
*/
@Override
public EasyUIDataGridResult getItemList(int page, int rows) {
//查询缓存
try {
String json = jedisClient.hget(ITEM_LIST_KEY, page + "-" + rows);
//判断json是否为空
if (StringUtils.isNotBlank(json)) {
//把json转换成list
System.out.println("获得缓存");
return JsonUtils.jsonToPojo(json, EasyUIDataGridResult.class);
}
} catch (Exception e) {
e.printStackTrace();
}

//设置分页信息
PageHelper.startPage(page, rows);
//查询数据,设置查询条件
TbItemExample example = new TbItemExample();
List<TbItem> tbItemList = tbItemMapper.selectByExample(example);
//封装分页结果集
PageInfo<TbItem> pageInfo = new PageInfo<>(tbItemList);
//创建分页对象
EasyUIDataGridResult result = new EasyUIDataGridResult((int) pageInfo.getTotal(), pageInfo.getList());

//向缓存中添加数据
try {
jedisClient.hset(ITEM_LIST_KEY, page + "-" + rows, JsonUtils.objectToJson(result));
System.out.println("放入redis -> " + page + "-" + rows);
System.out.println(JsonUtils.objectToJson(result));
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
}

控制台打印:
Alt text
Redis数据库:
Alt text

缓存同步

对添加了缓存的数据进行更新(增、删、改)之后,需要同步最新的数据放入缓存。做法就是删除对应缓存的key,再次获取数据会先查询数据库,再放入缓存。

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×