Redis集群方案使用
标签(空格分隔): 分享
Server端
非集群的方式
- 主从分离
- 分库
- 使用hash
- hash的缺点
- 物理节点的增删,会导致所有的key都重新分布。
Redis官方集群基本原理 详细介绍
Redis集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,redis 先对key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点
使用去中心化的思想,使用hash slot方式,将16348个hash slot,覆盖到所有节点上 对于存储的每个key值,使用CRC16(KEY)%16348=slot得到他对应的hash slot,并在访问key时就去找他的hash slot在哪一个节点上,然后由当前访问节点从实际被分配了这个hash slot的节点去取数据 节点之间使用轻量协议通信 减少带宽占用 性能很高 自动实现负载均衡与高可用,自动实现failover,并且支持动态扩展,官方已经玩到可以1000个节点,实现的复杂度低。
- hash slot 方式
- 虚拟槽位与物理节点解耦,不受节点数量影响
- slot-物理节点映射方式更灵活
- 投票机制
-所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽 - 灵活的增加或减少节点(动态的槽位分配)
- 高可用
- 主从
- 动态的给节点分配槽
- 部分节点fail,不影响其他节点
- 什么时候节点不可用
- 投票过程是集群中所有master参与,如果半数以上master节点与master节点通信超时(cluster-node-timeout),认为当前master节点挂掉。
- 什么时候整个集群不可用(cluster_state:fail)
- 如果集群任意master挂掉,且当前master没有slave.集群进入fail状态,也可以理解成集群的slot映射[0-16383]不完整时进入fail状态.
- 如果集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态.
集群管理
- 集群信息
略
- 集群启动与关闭
# 启动
/data/redis-4.0.1/src/redis-server /data/redis-4.0.1/cluster/30305/redis.conf > /data/redis-4.0.1/cluster/30305/redis-30305.log 2>&1 &
# 关闭
/data/redis-4.0.1/src/redis-cli -p 30305 -h 172.16.116.51 -a 123456 shutdown
- 连接集群
redis-cli -c -p 30301 -h 172.16.116.51 -a 123456
- 集群(cluster)
CLUSTER INFO 打印集群的信息
CLUSTER NODES 列出集群当前已知的所有节点(node),以及这些节点的相关信息。
- 节点(node)
CLUSTER MEET <ip> <port> 将 ip 和 port 所指定的节点添加到集群当中,让它成为集群的一份子。
CLUSTER FORGET <node_id> 从集群中移除 node_id 指定的节点。
CLUSTER REPLICATE <node_id> 将当前节点设置为 node_id 指定的节点的从节点。
CLUSTER SAVECONFIG 将节点的配置文件保存到硬盘里面。
- 槽(slot)
CLUSTER ADDSLOTS <slot> [slot ...] 将一个或多个槽(slot)指派(assign)给当前节点。
CLUSTER DELSLOTS <slot> [slot ...] 移除一个或多个槽对当前节点的指派。
CLUSTER FLUSHSLOTS 移除指派给当前节点的所有槽,让当前节点变成一个没有指派任何槽的节点。
CLUSTER SETSLOT <slot> NODE <node_id> 将槽 slot 指派给 node_id 指定的节点,如果槽已经指派给另一个节点,那么先让另一个节点删除该槽>,然后再进行指派。
CLUSTER SETSLOT <slot> MIGRATING <node_id> 将本节点的槽 slot 迁移到 node_id 指定的节点中。
CLUSTER SETSLOT <slot> IMPORTING <node_id> 从 node_id 指定的节点中导入槽 slot 到本节点。
CLUSTER SETSLOT <slot> STABLE 取消对槽 slot 的导入(import)或者迁移(migrate)。
//键 (key)
CLUSTER KEYSLOT <key> 计算键 key 应该被放置在哪个槽上。
CLUSTER COUNTKEYSINSLOT <slot> 返回槽 slot 目前包含的键值对数量。
CLUSTER GETKEYSINSLOT <slot> <count> 返回 count 个 slot
新增一个节点
# 启动新实例(一主、一丛)
/data/redis-4.0.1/src/redis-server /data/redis-4.0.1/cluster/30305/redis.conf > /data/redis-4.0.1/cluster/30305/redis-30305.log 2>&1 &
# 连接任何一个节点
redis-cli -c -p 30301 -h 172.16.116.52 -a 123456
# 查看集群信息
cluster info
#查看节点信息
cluster nodes
# 添加主节点到集群
/data/redis-4.0.1/src/redis-trib.rb add-node 172.16.116.52:30305 172.16.116.52:30304
# 添加从节点到集群
/data/redis-4.0.1/src/redis-trib.rb add-node --slave --master-id 73911503bbf5347bdab1f973d90f3b29409ca567 172.16.116.51:30305 172.16.116.52:30304
# 重新分配slot
/data/redis-4.0.1/src/redis-trib.rb reshard 172.16.116.52:30301
删除一个节点
/data/redis-4.0.1/src/redis-trib.rb del-node 172.16.116.52:30301 'hash'
cluster forget hash
视频教程
链接: https://pan.baidu.com/s/1s-nKXGR9hC2cP-hw0tEKFw 密码: chd9
Client端
Redis key 设计技巧
- 分组(redis,命名空间)
与Mysql 表的映射关系
- 把表名转换为key前缀 如, tag:
- 第2段放置用于区分区key的字段–对应mysql中的主键的列名,如userid
- 第3段放置主键值,如2,3,4…., a , b ,c
- 第4段,写要存储的列名
例子: 用户表 user , 转换为key-value存储
userid | username | passworde | |
---|---|---|---|
9 | Lisi | 1111111 | lisi@163.com |
set user:userid:9:username lisi
set user:userid:9:password 111111
set user:userid:9:email lisi@163.com
keys user:userid:9[username|password|email]
集群与单点共存的配置
<!--读取配置-->
<context:property-placeholder location="classpath:redis.properties"/>
<!--redis 密码-->
<bean name="password" class="org.springframework.data.redis.connection.RedisPassword">
<constructor-arg value="${redis.cluster.password}"/>
</bean>
<!--单点配置-->
<bean id="sentinelConfiguration" class="org.springframework.data.redis.connection.RedisSentinelConfiguration">
<constructor-arg name="master" value="${redis.masterName}"/>
<constructor-arg name="sentinelHostAndPorts">
<set>
<value>${redis.sentinels}</value>
<value>${redis.sentinels2}</value>
<value>${redis.sentinels3}</value>
</set>
</constructor-arg>
<property name="password" ref="password"/>
</bean>
<!--集群配置-->
<bean id="clusterConfiguration" class="org.springframework.data.redis.connection.RedisClusterConfiguration">
<constructor-arg name="clusterNodes">
<set>
<value>${redis.cluster.node1}</value>
<value>${redis.cluster.node2}</value>
<value>${redis.cluster.node3}</value>
<value>${redis.cluster.node4}</value>
</set>
</constructor-arg>
<property name="maxRedirects" value="${redis.cluster.redirect}" />
<property name="password" ref="password"/>
</bean>
<!-- 集群工厂 -->
<bean id="jedisClusterConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<constructor-arg index="0" ref="clusterConfiguration"/>
</bean>
<!-- 单点工厂 -->
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<constructor-arg index="0" ref="sentinelConfiguration"/>
</bean>
<!-- 单点客户端 -->
<bean id="myRedisClient" class="com.shangde.greatbear.redis.MyRedisClient" init-method="springInit">
<property name="redisPath">
<value>redis.properties</value>
</property>
</bean>
<!--集群客户端-->
<bean id="redisClusterTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
<property name="connectionFactory" ref="jedisClusterConnectionFactory" />
</bean>
StringRedisTemplate 使用 参见单元测试
@Autowired
@Qualifier("redisClusterTemplate")
StringRedisTemplate clusterTemplate;
@Autowired
@Qualifier("redisStandaloneTemplate")
StringRedisTemplate standaloneTemplate;
package com.greatbear.test.common;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.connection.RedisClusterNode;
import org.springframework.data.redis.connection.RedisZSetCommands;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(locations = {
"classpath:test/test-redis-cluster.xml"
})
/**
* lettuce 集群 单元测试
*/
public class TestRedisCluster {
@Autowired
//@Qualifier("redisStandaloneTemplate")
@Qualifier("redisClusterTemplate")
StringRedisTemplate clusterTemplate;
/**
* get set del has getAndSet getIfAbsent increment expire expireAt
*/
@Test
public void redisKeyCommandTest() {
ValueOperations<String, String> ops = clusterTemplate.opsForValue();
// del
clusterTemplate.delete("getAndSet");
//dump
ops.set("dumpKey", "dumpValue");
ops.get("dumpKey");
//exist
clusterTemplate.hasKey("dumpKey");
// expire
clusterTemplate.expire("dumpKey", 10, TimeUnit.MINUTES);
clusterTemplate.getExpire("dumpKey", TimeUnit.SECONDS);
//getAndSet
ops.getAndSet("getAndSet", getRandom());
clusterTemplate.randomKey();
// keys type ** 慎用 **
clusterTemplate.keys("*").forEach(k -> System.out.println(clusterTemplate.type(k)));
}
/**
* String Command
*/
@Test
public void redisStringCommandTest() {
ValueOperations<String, String> ops = clusterTemplate.opsForValue();
String key = "opsForValueSimpleKeyValueTest";
//set
ops.set(key, getRandom());
//get
System.out.println(ops.get(key));
//getRange
System.out.println(ops.get(key, 1, 6));
//getSet
ops.getAndSet(key, getRandom());
//getBit setBite
ops.getBit(key, 1);
ops.setBit(key, 1, true);
//mget mset
ops.multiSet(getMap());
ops.multiGet(Arrays.asList("a", "b"));
//setex
ops.set(key, "1", 1, TimeUnit.SECONDS);
//setnx
ops.setIfAbsent(key, getRandom());
//setRange
ops.set(key, "asdasd", 1);
//strlen
ops.size(key);
//msetnx
ops.multiSetIfAbsent(getMap());
//incr
ops.set(key, "1");
ops.increment(key, 1d);
//append
ops.append(key, "suffix");
}
/**
* HASH
* 支持多个键值对的操作
*/
@Test
public void redisHashTest() {
HashOperations<String, Object, Object> ops = clusterTemplate.opsForHash();
String key = "redisHashTest";
//HMSET
ops.putAll(key, getMap());
//HEDL
ops.delete(key, "key1", "key2");
//HEXISTS
ops.hasKey(key, "key1");
//HGETALL
ops.entries(key);
//HINCRBY
ops.increment(key, "key1", 1f);
//HKEYS
ops.keys(key);
//HLEN
ops.size(key);
//HMGET
ops.multiGet(key, Arrays.asList("key1", "key2"));
//HVALS
ops.values(key);
}
/**
* 链表
* 支持阻塞操作
*/
@Test
public void redisListTest() {
ListOperations<String, String> ops = clusterTemplate.opsForList();
String key = "redisListTest1";
String key2 = "redisListTest2123123123";
//BLPOP 阻塞
ops.leftPop(key, 1, TimeUnit.SECONDS);
//BRPOP
ops.rightPop(key, 1, TimeUnit.SECONDS);
//BLPOPRPUSH
ops.rightPopAndLeftPush(key, key2, 1, TimeUnit.SECONDS);
// LINDEX
ops.index(key, 1);
// LINSERT
ops.leftPush(key, "1");
ops.set(key, 0, "insertValue");
//LLEN
ops.size(key);
//LPOP
ops.leftPop(key);
//LPUSH
ops.leftPush(key, "1");
//LPUSH BEFORE
ops.leftPush(key, "1", "2");
//LPUSH 多个
ops.leftPushAll(key, "v1", "v2", "v3");
//LPUSHX 存在才PUSH
ops.leftPushIfPresent(key, "4");
//LRANGE
ops.range(key, 0, 1);
//LREM
ops.remove(key, 1, "4");
//LTRIM
ops.trim(key, 0, 3);
//RPOP
ops.rightPop(key);
}
/**
* SET操作
*/
@Test
public void redisSetTest() {
SetOperations<String, String> ops = clusterTemplate.opsForSet();
String key = "redisSetTest";
String key2 = "redisSetTest2";
//SADD 多个
ops.add(key, "v1", "v2");
ops.add(key2, "v1", "v3");
//SCARD
ops.size(key);
//SDIFF
ops.difference(key, key2);
//SDIFFSTORE
ops.differenceAndStore(key, key2, "temp");
//SINNER
ops.intersect(key, key2);
//SINNERSTORE
ops.intersectAndStore(key, key2, "temp2");
//SISMEMBER
ops.isMember(key, "v1");
//SMEMBERS ** 慎用 **
ops.members(key);
//SMOVE
ops.move(key, "v1", key2);
//SPOP
ops.pop(key);
//SRANDOMMEMBER
ops.randomMember(key);
//SREM
ops.remove(key, key2);
//SUNION
ops.union(key, key2);
//SUNIONSTORE
ops.add(key, "v1", "v2", "v3");
ops.add(key2, "v11", "v22", "v33");
ops.unionAndStore(key, key2, "temp2");
//SCAN
ops.add(key, "v1", "v2", "v3");
ScanOptions options = ScanOptions.scanOptions().build();
Cursor<String> scan = ops.scan(key, options);
while (scan.hasNext()) {
System.out.println(scan.next());
}
}
/**
* ZSET 操作
*/
@Test
public void redisZsetTest() {
String key = "redisZsetTest";
String key2 = "redisZsetTest2";
ZSetOperations<String, String> ops = clusterTemplate.opsForZSet();
//ZADD
ops.add(key, "v1", 1d);
ops.add(key, "v2", 2d);
ops.add(key, "v3", 3d);
ops.add(key, "v4", 4d);
ops.add(key2, "v1", 1d);
ops.add(key2, "v3", 3d);
ops.add(key2, "v5", 5d);
//ZCARD
ops.zCard(key);
//ZCOUNT
ops.count(key, 1d, 2d);
//ZINCRBY
ops.incrementScore(key, "v4", 1.5d);
//ZLEXCOUNT 字母序
//ZRANGE 位置
ops.range(key, 1, 3);
//ZRANGEBYLEX 字母序
ops.rangeByLex(key, RedisZSetCommands.Range.range().gt("v1").lte("v2"));
//ZRANGEBYSCORE 分数
ops.rangeByScore(key, 1d, 4d);
//ZRANK 排名
ops.rank(key, "v1");
//ZREM
ops.remove(key, "v1");
//ZREMRANGEBYLEX
//ZREMRANGEBYSCORE
ops.removeRangeByScore(key, 2d, 3d);
//ZREVRANGE
ops.reverseRange(key, 0, -1);
//ZREVRANGEBYSCORE
ops.removeRangeByScore(key, 1d, 5d);
//ZREVRANK
ops.reverseRank(key, "v1");
//ZSCORE
ops.score(key, "v1");
//ZZSCAN
ops.scan(key, ScanOptions.scanOptions().build());
}
/**
* 基数统计结构
*/
@Test
public void redisHyperLogLogTest() {
HyperLogLogOperations<String, String> ops = clusterTemplate.opsForHyperLogLog();
String key = "redisHyperLogLogTestKey";
String key2 = "redisHyperLogLogTestKey2";
ops.add(key, "asd");
ops.add(key2, "asdasd");
ops.size(key);
ops.union("redisHyperLogLogTestTemp", key, key2);
}
/**
* 发布
*/
@Test
public void redisPublishTest() {
String channle = "testChannel";
clusterTemplate.execute((RedisCallback<Object>) connection -> {
connection.publish(channle.getBytes(), "message1".getBytes());
return null;
});
}
/**
* 订阅
*/
@Test
public void redisSubscribeTest() {
String channle = "testChannel";
clusterTemplate.execute((RedisCallback<Object>) connection -> {
connection.subscribe((message, pattern) -> {
System.out.println(new String(message.getBody()));
}, channle.getBytes());
return null;
});
}
/**
* 事务 集群不支持
* - 批量操作在发送 EXEC 命令前被放入队列缓存。
* - 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
* - 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
*/
@Test
public void redisTransactionTest() {
clusterTemplate.setEnableTransactionSupport(true);
//开启事务
clusterTemplate.multi();
redisKeyCommandTest();
redisStringCommandTest();
//提交事务
clusterTemplate.exec();
//取消事务
//clusterTemplate.discard();
//监视key
clusterTemplate.watch("watchKey");
//取消监视
clusterTemplate.unwatch();
}
/**
* 脚本
* 集群不支持脚本
*/
@Test
public void redisScriptTest() {
String lua = "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}";
DefaultRedisScript<List> script = new DefaultRedisScript<>(lua, List.class);
String sha1 = script.getSha1();
System.out.println(sha1);
System.out.println(clusterTemplate.execute(script, Arrays.asList("key1", "key2"), "arg1", "arg2"));
}
private String getRandom() {
return String.valueOf(Math.random());
}
private Map<String, String> getMap() {
Map<String, String> map = new HashMap<>();
map.put("a", "1");
map.put("b", "2");
map.put("c", "3");
map.put("d", "4");
return map;
}
@Test
public void test() {
clusterTemplate.opsForZSet().intersectAndStore("key1", "key2", "zSetTempKey");
}
/**
* 不支持
*/
@Test
public void testTransaction() {
// 事务
clusterTemplate.multi();
redisKeyCommandTest();
redisStringCommandTest();
clusterTemplate.exec();
}
/**
* 不支持
*/
@Test
public void testMove() {
clusterTemplate.opsForValue().set("a", "1");
//move
clusterTemplate.move("a", 1);
}
/**
* 不支持
*/
@Test
public void testScan() {
//scan
clusterTemplate.execute((RedisCallback<Object>) connection -> {
Cursor<byte[]> scan = connection.scan(ScanOptions.scanOptions().build());
while (scan.hasNext()) {
System.out.println(scan.next());
}
return null;
});
}
/**
* 不支持
*/
@Test
public void testHypLogLogUnion() {
//hyploglog union
clusterTemplate.opsForHyperLogLog().union("redisHyperLogLogTestTemp", "key", "key2");
}
/**
* 不支持
*/
@Test
public void testZsetInner() {
//ZINTERSTORE 交集分数相加
clusterTemplate.opsForZSet().intersectAndStore("key1", "key2", "zSetTempKey");
}
/**
* 不支持
*/
@Test
public void testZSetunion() {
//ZUNIONSTORE
clusterTemplate.opsForZSet().unionAndStore("key", "key2", "zSetTempKey2");
}
}
集群不支持的命令
- 事务
- pipeline
- 脚本
- move
- scan
- 多key操作
- HypLogLogUnion
- ZINTERSTORE
- ZUNIONSTORE
性能测试
- 不再建议使用 MyRedisClient
- 监控 redis
top -p $(pgrep -f -d, redis)
谨慎使用的命令
- keys *
- SMEMBERS
扩展
持久化 与Redis的rehash
https://blog.csdn.net/a600423444/article/details/8944601
https://blog.csdn.net/jsjwk/article/details/7964108
关于 缓存穿透、缓存雪崩、缓存更新、缓存预热、缓存降级
- 分布式锁
public class RedisLockUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(RedisLockUtil.class);
private static final String UNLOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
/**
* @Description 获取锁并设置失效时间(毫秒),返回锁uuid,null表示获取锁失败
* @Author shenwei
* @Date 2018/6/26 18:24
*/
public static String tryLock(String key, long milliseconds) {
return tryLock(key, milliseconds, getUUID());
}
/**
* @Description 获取锁并设置失效时间(毫秒),返回锁uuid,null表示获取锁失败
* @Author shenwei
* @Date 2018/8/15 14:05
*/
public static String tryLock(String key, long milliseconds, String value) {
if (StringUtils.isBlank(key) || StringUtils.isBlank(value)) {
return null;
}
boolean isLock = false;
try {
isLock = MyRedisClient.getClient().set(key, value, "nx", "px", milliseconds) != null;
} catch (Exception e) {
LOGGER.error("tryLock fail !", e);
}
if (isLock) {
return value;
} else {
return null;
}
}
/**
* @Description 释放锁(私有锁),需要传入获取锁时拿到的uuid,由此操作释放的锁,则返回true,否则返回false
* @Author shenwei
* @Date 2018/6/26 18:26
*/
public static boolean unlock(String key, String uuid) {
if (StringUtils.isBlank(key) || StringUtils.isBlank(uuid)) {
return false;
}
List<String> params = new ArrayList<>();
params.add(uuid);
try {
Long result = (Long) MyRedisClient.getClient().eval(UNLOCK_SCRIPT, key, params);
return result != null && result == 1;
} catch (Exception e) {
LOGGER.error("unlock fail !", e);
return false;
}
}
/**
* @Description 生成全局唯一UUID
* @Author shenwei
* @Date 2018/6/26 18:22
*/
private static String getUUID() {
return UUID.randomUUID().toString();
}
}
集群的其他实现方式
(1)Twitter开发的twemproxy
(2)豌豆荚开发的codis
为什么哈希槽的数量固定为16384
由于使用CRC16算法,该算法可以产生2^16-1=65535个值,可是为什么哈希槽的数量设置成了16384?
Normal heartbeat packets carry the full configuration of a node, that can be replaced in an idempotent way with the old in order to update an old config. This means they contain the slots configuration for a node, in raw form, that uses 2k of space with 16k slots, but would use a prohibitive 8k of space using 65k slots.
At the same time it is unlikely that Redis Cluster would scale to more than 1000 mater nodes because of other design tradeoffs.
So 16k was in the right range to ensure enough slots per master with a max of 1000 maters, but a small enough number to propagate the slot configuration as a raw bitmap easily. Note that in small clusters the bitmap would be hard to compress because when N is small the bitmap would have slots/N bits set that is a large percentage of bits set.
总结一下:
1、redis的一个节点的心跳信息中需要携带该节点的所有配置信息,而16K大小的槽数量所需要耗费的内存为2K,但如果使用65K个槽,这部分空间将达到8K,心跳信息就会很庞大。
2、Redis集群中主节点的数量基本不可能超过1000个。
3、Redis主节点的配置信息中,它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中,会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话,bitmap的压缩率就很低,所以N表示节点数,如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。而16K个槽当主节点为1000的时候,是刚好比较合理的,既保证了每个节点有足够的哈希槽,又可以很好的利用bitmap。
4、选取了16384是因为crc16会输出16bit的结果,可以看作是一个分布在0-2^16-1之间的数,redis的作者测试发现这个数对2^14求模的会将key在0-2^14-1之间分布得很均匀,因此选了这个值。