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; } }代码只是作为学习参考使用,具体的使用方法还需要根据自己的业务逻辑情况进行具体实现。