Linux系统环境:CentOS 7
前提:已经掌握了单机Redis的安装、配置以及使用
至于为什么要使用redis、集群是什么、为什么要使用redis集群,在这里就不废话了,直接步入正题:
redis集群理论
redis-cluster 结构图
redis-cluster 投票:容错
架构细节:
1. 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
2. 节点的fail是通过集群中超过半数的节点检测失效时才生效(所以一个集群中至少要有三个节点)。
3. 客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
4. 集群中每一个节点都存放不同的内容,每一个节点都应有备份机。
5. redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->value
Redis 集群中内置了16384 个哈希槽,当需要在Redis 集群中放置一个key-value 时,redis先对 key 使用 crc16 算法算出一个结果,然后把结果对16384 求余数,这样每个key 都会对应一个编号在0-16383 之间的哈希槽,redis会根据节点数量大致均等的将哈希槽映射到不同的节点
Redis集群的搭建
Redis集群中至少应该有三个节点。要保证集群的高可用,需要每个节点有一个备份机。这样我们就至少需要6台服务器。
因为我的电脑开6台虚拟机会吃力,因为已经开了好多了,这里我们就搭建伪分布式。可以使用一台虚拟机运行6个redis实例。这就需要我们修改redis的端口号为7001-7006。
搭建环境
1. 使用ruby脚本搭建集群。需要ruby的运行环境。
安装ruby:运行命令yum install ruby 和 yum installrubygems
2. 安装ruby脚本运行使用的包。
下载包:https://pan.baidu.com/s/1tJmqibjSLedefHutvkWycw 然后传到Linux系统上,安装:
搭建步骤
1. 复制6份redis:将/usr/local/bin目录复制6份到redis-cluster文件夹下,命名为redis01-redis06,当然,你的这个目录不一定在这里,视具体情况而定,就是这个目录:
复制结果:
2. 修改每一份的配置文件
a) 端口号:port 7001 — port 7006
b) cluster-enabledyes
c) 注释掉bind配置(默认请情况下就是注释掉的)
3.开启所有的redis,可以一台一台手动开启,也可以编写一个批处理文件:
a) vim start-all.sh
b) 修改该文件的权限: chomd u+x start-all.sh 即为当前用户添加执行权限
4. 运行 ./start-all.sh 运行之后查看启动状态:
5. 将搭建集群所需的ruby脚本文件复制到/usr/local/redis-cluster下
a) 文件位置:/usr/local/redis/src
b)
c) 复制
6. 使用ruby脚本搭建集群
a) 命令:./redis-trib.rb create --replicas 1 192.168.25.129:7001 192.168.25.129:7002 192.168.25.129:7003 192.168.25.129:7004 192.168.25.129:7005 192.168.25.129:7006
b) 含义:create表示创建集群,--replicas 1 表示每一个节点有一台备份机,然后它就会根据后面的节点计算分配槽。如果后面跟的是奇数个节点则会报错。
7. 执行结果
注:如果真的是在6太服务器上面搭建redis集群,第6步的命令在任意一台机器上面执行一次就可以了。(一定要关闭每一台的防火墙,当然,不嫌麻烦的话可以开启每一台上的对应端口,这样最少需要开:6*5=30次)
集群的使用
使用redis-cli连接集群,连接到任意一台均可:
redis01/redis-cli -p 7002 –c
-c:表示连接的是集群,这样就可以自动重定向到其他的节点(当前操作的key对应的槽不一定在当前节点)
然后就可以执行增删改查操作了,方法和普通的单机版redis一样。
Jedis连接集群版Redis
第一步:使用JedisCluster对象。需要一个Set<HostAndPort>参数。Redis节点的列表。
第二步:直接使用JedisCluster对象操作redis。在系统中单例存在。
第三步:打印结果
第四步:系统关闭前,关闭JedisCluster对象。
@Test
publicvoidtestJedisCluster()throwsException{
// 创建一个JedisCluster对象,参数:set类型的nodes,包含若干个HostAndPort对象。
Set<HostAndPort>nodes=newHashSet<>();
nodes.add(newHostAndPort("192.168.25.129",7001));
nodes.add(newHostAndPort("192.168.25.129",7002));
nodes.add(newHostAndPort("192.168.25.129",7003));
nodes.add(newHostAndPort("192.168.25.129",7004));
nodes.add(newHostAndPort("192.168.25.129",7005));
nodes.add(newHostAndPort("192.168.25.129",7006));
JedisClusterjedisCluster=newJedisCluster(nodes);
// 直接使用JedisCluster对象操作redis,每次操作不需要关闭(自带连接池)。
jedisCluster.set("test","123");
Stringstring=jedisCluster.get("test");
System.out.println(string);
// 系统结束前关闭JedisCluster对象。
jedisCluster.close();
}
Spring中使用Redis集群:
定义接口,接口中定义了常用的一些操作。
至于为什么定义接口:因为下面给出了两套接口的实现,一个是单机版的实现,一个是集群版的实现。本地开发的时候使用redis单机版进行开发测试,项目上线的时候使用集群版,这样的话在单机版和集群版之间的切换只需要更改spring的配置文件即可。
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);
}
单机版实现类(jedis连接池):
public class JedisClientPool implements JedisClient {
private JedisPool jedisPool;
public JedisPool getJedisPool() {
return jedisPool;
}
public void setJedisPool(JedisPool jedisPool) {
this.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;
}
}
集群版实现类:
public class JedisClientCluster implements JedisClient {
private JedisCluster jedisCluster;
public JedisCluster getJedisCluster() {
return jedisCluster;
}
public void setJedisCluster(JedisCluster jedisCluster) {
this.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配置文件中进行配置:
单机版和集群版的配置只能同时存在一个,不然自动注入的时候就会报异常ununique.
<!-- 连接redis单机版 -->
<bean class="cn.e3mall.common.jedis.JedisClientPool">
<property name="jedisPool" ref="jedisPool"/>
</bean>
<bean class="redis.clients.jedis.JedisPool" id="jedisPool">
<constructor-arg name="host" value="192.168.25.129"/>
<constructor-arg name="port" value="6379"/>
</bean>
<!-- 连接redis集群 -->
<bean id="jedisClientCluster" class="cn.e3mall.common.jedis.JedisClientCluster">
<property name="jedisCluster" ref="jedisCluste"/>
</bean>
<bean id="jedisCluste" class="redis.clients.jedis.JedisCluster">
<constructor-arg name="nodes">
<set>
<!-- 这里配置集群中的任意一台节点即可 -->
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="192.168.25.129"/>
<constructor-arg name="port" value="7001"/>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="192.168.25.129"/>
<constructor-arg name="port" value="7002"/>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="192.168.25.129"/>
<constructor-arg name="port" value="7003"/>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="192.168.25.129"/>
<constructor-arg name="port" value="7004"/>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="192.168.25.129"/>
<constructor-arg name="port" value="7005"/>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="192.168.25.129"/>
<constructor-arg name="port" value="7006"/>
</bean>
</set>
</constructor-arg>
</bean>
具体使用:
查询的时候先查缓存,查到直接返回,未查到查询数据库,然后放入缓存中。
增删改的时候删除对应的缓存。
@Service
public class ContentServiceImpl2 implements ContentService {
@Value("${IMAGE_SERVER_URL}")
private String IMAGE_SERVER_URL; // 图片服务器地址
@Value("${CONTENT_LIST}")
private String CONTENT_LIST; // redis缓存中内容列表的key
@Autowired
private TbContentMapper contentMapper;
@Autowired // 根据类型注入
private JedisClient jedisClient;
@Override
public int addContent(TbContent content) {
Date nowDate = new Date();
content.setCreated(nowDate);
content.setUpdated(nowDate);
// 插入到数据库
int rows = contentMapper.insertSelective(content);
// 缓存同步,删除缓存中对应的数据
Long categoryId = content.getCategoryId();
jedisClient.hdel(CONTENT_LIST, categoryId.toString());
return rows;
}
@Override
public int updateContent(TbContent content) {
content.setUpdated(new Date());
int rows = contentMapper.updateByPrimaryKeySelective(content);
// 缓存同步,删除缓存中对应的数据
Long categoryId = content.getCategoryId();
jedisClient.hdel(CONTENT_LIST, categoryId.toString());
return rows;
}
@Override
public E3Result deleteContent(List<Long> ids) throws Exception {
// 删除图片服务器上图片
FastDFSClient fastDFSClient = new FastDFSClient("classpath:conf/client.conf"); // 初始化FastDFS客户端
TbContentExample example = new TbContentExample();
example.createCriteria().andIdIn(ids);
List<TbContent> Contents = contentMapper.selectByExample(example);
// 遍历删除图片
for (TbContent tbContent : Contents) {
if (tbContent.getPic() != null) {
String url1 = tbContent.getPic().replace(IMAGE_SERVER_URL, "");
fastDFSClient.deleteFile1(url1);
}
if (tbContent.getPic2() != null) {
String url2 = tbContent.getPic2().replace(IMAGE_SERVER_URL, "");
fastDFSClient.deleteFile1(url2);
}
}
// 删除数据库数据
contentMapper.deleteByExample(example);
// 缓存同步,删除缓存中对应的数据
Long categoryId = Contents.get(0).getCategoryId();
jedisClient.hdel("CONTENT_LIST", categoryId.toString());
return E3Result.ok();
}
// 查询时先查询缓存,查不到再查询数据库,并将结果放入redis中
@Override
public List<TbContent> getContentByCid(long categoryId) {
// 查询缓存
try {
// 如果缓存中有就直接响应结果
String json = jedisClient.hget(CONTENT_LIST, categoryId + "");
if (StringUtils.isNotBlank(json)) {
List<TbContent> list = JsonUtils.jsonToList(json, TbContent.class);
return list;
}
} catch (Exception e) {
e.printStackTrace();
}
// 没有则查询数据库
TbContentExample example = new TbContentExample();
Criteria criteria = example.createCriteria();
// 设置条件
criteria.andCategoryIdEqualTo(categoryId);
// 执行查询
List<TbContent> list = contentMapper.selectByExample(example);
// 把结果添加到缓存
try {
jedisClient.hset(CONTENT_LIST, categoryId + "", JsonUtils.objectToJson(list));
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
}