1、Redis的认知
为什么要使用Reids?
1、是一个独立的数据缓存,存储数据库中的数据
2、提升用户获取数据的效率
是一个中间件技术:介于tomcat服务和数据库服务之间
数据库的分类:
1、sql数据库:mysql、oracle、sqlserver、db2等
通过sql语句进行操作
2、nosql数据库:redis
-
Redis就是一款NoSQL。
-
NoSQL -> 非关系型数据库 -> Not Only SQL。
-
Key-Value:Redis。。。
-
文档型:ElasticSearch,Solr,Mongodb。。。
-
面向列:Hbase,Cassandra。。。
-
图形化:Neo4j
2、Redis的特点:
1、nosql数据库
2、key-value存储形式
3、内存存储数据
4、支持持久化
5、支持集群(适用于高并发)
3、Redis服务的搭建及访问
也可以使用Redis-cli.exe 工具
4、Redis常用的五大数据类型
1、string:以字符串的形式存储数据
【说明】单个字符、多个字符、整数、小数都是string
【扩展】研究Redis的string类型的底层是如何实现的?
2、list: value值可以为多个,使用逗号分隔,有顺序,可以重复
3、hash: 可以存储对象的属性名以及属性名所对应的属性值
4、set: value值可以为多个,使用逗号分隔,无序、唯一
5、zset/sortedset: value值可以为多个,使用逗号分隔,有序、唯一
顺序:按score分数排序
【补充】
-
HyperLogLog:计算近似值的。
-
GEO:地理位置。
-
BIT:一般存储的也是一个字符串,存储的是一个byte[]。
5、Redis的各种命令
命令文档地址:Redis 命令参考 — Redis 命令参考
Redis中的数据根据存活时间分为:
1、永久的数据---默认就是永久的
2、易失的数据---在存入的时候设置了存活时间,过了存活时间,Redis就会删除掉对应的数据
1、select 库名 进入到对应的库下
2、set key value 给key起名 并赋值
3、get key 获取到key中所有的值
key:
4、del key1 key2 删除key及key中所包含的值
如果后边输入一个不存在的key 例如nofoundstring 并不耽误删除存在的key
5、exists key 查看是否有这个key 有返回1 无返回0
6、expire key second 将key设成有效期,key必须存在 second为秒 ![](https://i-blog.csdnimg.cn/blog_migrate/ba58e7bd313200a4ea9cd7c3d6863752.png)
10秒后get stu2 就为nil了 nil在redis中表示为空
7、pexpire key millisecond 将key设成有效期,key必须存在 millisecond为毫秒
5000毫秒也就是5秒后 stu1就为nil了
8、keys * 查询当前库中一共有哪些key
所选值的前后如果有值 或者所选值为前后任意一端都要在有数据那边加上*
包含指定值(例:s)的
9、move key 库名 将当前库下指定的key移到其他库中
10、expire key second 设定存活时间 变为易失的 以秒为单位 key值必须先存在
11、ttl key 查看剩余的存活时间
12、persist key 将经过expire设定的key改为永久存活
-1为永久存活
13、randomkey 随机获取一个key
14、rename oldkey newkey 给key命名一个新名
如果rename改名改成了该库中已经有的名还是返回ok但是会把原来存在的那个删除,只留下这个rename 改的key
15、renamenx oldkey newkey 如果要改的名已经存在就不会进行修改保持原有
16、type key 返回该key所对应的类型
String:
17、append key 值 在原有值的基础上追加值
如果需要加空格那就加双引 如果不需要加空格就不用加索引 key值如果没有就会创建一个
18、decr key 将值进行减一
只针对int类型
19、incr key 将值进行加一
只针对int类型
20、decrby key num 将key中的值减num
只针对于int类型
21、incrby key num 将key中的值加num
只针对于int类型
22、incrbyfloat key float 将key中的值增加float
只有增加才有 decr不能减少float类型
23、set 赋值
set key value 什么也不加默认为永久有效
set key value EX second 创建的存活second秒
set key value PX millisecond 创建的key存活millisecond毫秒
NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
XX :只在键已经存在时,才对键进行设置操作。
24、get key 取值
只能针对于string类型的进行取值
25、mset key1 value1 key2 value2 ... 多个key-value进行赋值
26、mget key1 key2 ... 对多个key进行取值
27、msetnx key1 value key2 value ...
添加多个key中有一个key存在所有的key都拒绝添加否则能添加 ![](https://i-blog.csdnimg.cn/blog_migrate/71cd8d5dbd53337f938ecb29233052f9.png)
28、setex key secod value 对key添加有效时间
对key的存活事件进行设置有两种方式
29、setnx key value 当key不存在的时候会添加成功
30、setrange key index value
第一位从0开始算将原先的值从index位进行替换,替换成value
31、strlen key 返回该字符串中元素的个数
32、getset key value 给key赋一个新值,但是返回的是原来的 再查是新的
Hash(经常用来存对象):
33、hset key name value
给key进行复赋值 第一个为名 第二个为值 都在key下
34、hget key name 获取redis代表的key中 里边的Object Key里的值
35、hdel key name 删除key中的name以及value
36、hexists key name 查询key下是否有这个name有返回1没有返回0
37、hgetall key 获取该key下所有的name和所对应的value(一个key一个value)
38、hincrby key name num 将key下name对应的值加num
只是int类型才可以增加
39、hincrbyfloat key name float 将key下name对应的值加float
只是int类型才可以增加
40、hkeys key 将key下所有的name获取出来
41、hlen key 将key下的name个数进行返回
42、hmset key name1 value1 name2 value2 ... 存储多个对象
43、hmget key name1 name2 ... 获取多个对象的值
k
44、hsetnx key name value 当key不存在时才可以创建且只能创建一个name1
45、hvals key 查询出当前key下的所有value值
List:
46、lpush key value1 value2 ... 给list集合进行赋值依次在左侧也就是前边进行赋值
47、lrange key indexStart indexStop 获取list集合中的值
indexStart 0 就是第一个
indexStop -1 就是之后的所有一直到最后一个
index是从0开始的
48、rpush key value1 value2... 给list集合进行赋值依次在右侧也就是后边进行赋值
49、lpop key 从集合左侧的第一个开始删除
50、rpop key 从集合右侧的第一个开始删除
52、rpoplpush oldkey newkey 把oldkey的最后一个放到newkey的第一个
newkey没有时可以创建
53、lpushx key value 当key存在的情况下在头部添加value
54、rpushx key value 当key存在的情况下在尾部添加value
55、lindex key index 找当前key下对应的索引所存在的值
index 0 就是第一个 -1是最后一个
56、linsert key before|after oldvalue newvalue 在原有value值的 前|后 添加值
after 之后
before 之前
oldvalue没有的话插入不进去
57、llen key 查看当前key下有几个值
58、lrem key num value key中value从前往后删除num个
负数
0
正数是从前往后找 负数是从后往前找 0 所有的value都删除掉
59、lset key index newvalue 将key中的index值修改成newValue
index是从0开始的 -1是最后一个
60、ltrim key startindex stopindex 保留key中 startindex到stopindex中的值
startindex从0开始 stopindex最后为-1
Set(唯一 无序):
61、sadd key value1 value2 ... 向key中添加value value的值不可以重复且无序
重复的值就不往里添加
无序
62、smembers key 查询key中的值
63、scard key 返回key中元素的个数
64、sdiff key1 key2 返回key1中key2没有的数据
65、sdiffstore key key1 key2 将key1中key2没有的数据返回到key中
key要是没有就创建 key要是有的话就将以前的值进行替换
66、sinter key1 key2 返回key1中key2有的数据
67、sinterstore key key1 key2 将key1和key2共有的数据返回到key中
68、sismember key value 判断key中是否有value有的话返回1没有的话返回0
69、smove key1 key2 key1value 将key1中的值key1value放到key2中
将key2有的话往里添加没有的话就覆盖
70、spop key 将key中的值随机删除掉一个
71、srandmember key 随机获取key中的一个值
是获取不是删除
72、srem key value 移除key中的value
73、sunion key1 key2 返回key1中和key2中的并集
74、sunionstore key key1 key2 将key1和key2的并集返回到key中
ZSet/SortedSet(唯一 有序 通过score 根据score排序):
75、zadd key score1 value1 score2 value2 ... 把score1 value1 ...添加进key中
76、zrange key index1 index2 查询key中inedx1到index2中的值
只查询value值
index1 从0开始
index2 最后为-1
77、zrange key index1 index2 withscores
查询key中index1到index2中的值以及score
根据分数进行排序
78、zcard key 查询key中的元素个数
79、zcount key minscore maxscore
查询key中minscore到maxscore范围内的元素个数
6、Java操作Redis
往redis中赋值
package com.qf.redispro.Test;
import redis.clients.jedis.Jedis;
public class Test1 {
public static void main(String[] args) {
//1、创建和Redis服务的连接
Jedis jedis = new Jedis("127.0.0.1",6379);
//2、选择要操作的库
jedis.select(6);
//3、存入字符串
jedis.set("javastu","张三");
//4、关闭资源
jedis.close();
System.out.println("写入Redis成功!");
}
}
从redis中往外取值
package com.qf.redispro.Test;
import redis.clients.jedis.Jedis;
public class Test2 {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1",6379);
jedis.select(6);
String javastu = jedis.get("javastu");
System.out.println("从java中获取的数据:" + javastu);
jedis.close();
}
}
存取对象
package com.qf.redispro.Test;
import com.qf.redispro.pojo.Dept;
import redis.clients.jedis.Jedis;
import java.util.HashMap;
public class Test3 {
public static void main(String[] args) {
Dept dept = new Dept(1001,"软件开发部","北京市海淀区");
Jedis jedis = new Jedis("127.0.0.1",6379);
jedis.select(6);
//单个字段存储
jedis.hset("dept1","deptNo",String.valueOf(dept.getDeptNo()));
HashMap<String,String> map = new HashMap<String,String>();
map.put("dName", dept.getDName());
map.put("loc",dept.getLoc());
jedis.hmset("dept1",map);
//逐个读取
System.out.println("读取信息如下:");
System.out.println("部门编号:"+jedis.hget("dept1","deptNo"));
System.out.println("部门名称:"+jedis.hget("dept1","dName"));
System.out.println("部门位置:"+jedis.hget("dept1","loc"));
//关闭资源
jedis.close();
}
}
byte类型存取图片视频
package com.qf.redispro.Test;
import com.qf.redispro.pojo.Dept;
import org.springframework.util.SerializationUtils;
import redis.clients.jedis.Jedis;
public class Test4 {
public static void main(String[] args) {
//图片、音频、视频等
//以byte[]形式存取数据
Jedis jedis = new Jedis("127.0.0.1",6379);
//默认使用索引为0的库
Dept dept = new Dept(1001,"软件测试部","北京市朝阳区");
String key = dept.getDName();
//把key value 转换为 byte[]
byte[] byteKey = SerializationUtils.serialize(key);
byte[] byteValue = SerializationUtils.serialize(dept);
//存储
jedis.set(byteKey,byteValue);
///
//取出数据
//获取key
String getKey= dept.getDName();
//将key转换为byte类型
byte[] byteGetKey = SerializationUtils.serialize(getKey);
//利用转换后的key值形式读取
byte[] byteAry = jedis.get(byteGetKey);
//强转成对象
//将byte类型的转换成对象类型
Dept dept2 = (Dept) SerializationUtils.deserialize(byteAry);
System.out.println("读取到的内容如下:" + dept2.toString());
}
}
json格式存储数据
package com.qf.redispro.Test;
import com.alibaba.fastjson.JSON;
import com.qf.redispro.pojo.Dept;
import redis.clients.jedis.Jedis;
import java.util.ArrayList;
import java.util.List;
public class Test5 {
public static void main(String[] args) {
//以json格式存储数据
List<Dept> list = new ArrayList<Dept>();
Dept dept1=new Dept(1001,"软件开发部A","设计城906A");
Dept dept2=new Dept(1002,"软件开发部B","设计城906B");
Dept dept3=new Dept(1003,"软件开发部C","设计城906C");
Dept dept4=new Dept(1004,"软件开发部D","设计城906D");
Dept dept5=new Dept(1005,"软件开发部E","设计城906E");
list.add(dept1);
list.add(dept2);
list.add(dept3);
list.add(dept4);
list.add(dept5);
//创建redis连接
Jedis jedis = new Jedis("127.0.0.1",6379);
//选择库
jedis.select(7);
String key = "depts";
//转json
String value = JSON.toJSONString(list);
//存储
jedis.set(key,value);
System.out.println("存入成功!");
jedis.close();
}
}
json格式取出数据
package com.qf.redispro.Test;
import com.alibaba.fastjson.JSON;
import com.qf.redispro.pojo.Dept;
import redis.clients.jedis.Jedis;
import java.util.ArrayList;
import java.util.List;
public class Test6 {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1",6379);
jedis.select(7);
//获取到key中的数据
String strJsonValue = jedis.get("depts");
//输出
//json转成集合类型
List<Dept> list = JSON.parseArray(strJsonValue,Dept.class);
//遍历输出
for (Dept dept:list) {
System.out.println(dept);
}
jedis.close();
}
}
封装Redis连接池创造工具类
package com.qf.redispro.util;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import sun.net.www.content.text.Generic;
public class JedisUtil {
//声明Jedis连接池对象
private static JedisPool jedisPool=null;
//在静态代码块中编写连接池的配置信息
static{
//创建连接池的配置对象
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
//配置连接池
//连接池中最大活跃数量
poolConfig.setMaxTotal(100);
//最大空闲数量
poolConfig.setMaxIdle(10);
//最小空闲数量
poolConfig.setMinIdle(5);
//等待超时时间
poolConfig.setMaxWaitMillis(3000);
//创建连接池对象
jedisPool = new JedisPool(poolConfig,"127.0.0.1",6379);
}
//获取连接方法
public static Jedis getJedis(){
return jedisPool.getResource();
}
//关闭连接
public static void closeJedis(Jedis jedis){
jedis.close();
}
}
设置有效期
package com.qf.redispro.Test;
import com.qf.redispro.util.JedisUtil;
import redis.clients.jedis.Jedis;
public class Test7 {
public static void main(String[] args) {
//通过连接池来获取连接
Jedis jedis = JedisUtil.getJedis();
jedis.select(7);
//设置数据的有效期
jedis.expire("depts",2);
System.out.println("设置成功");
//关闭连接
JedisUtil.closeJedis(jedis);
}
}
使用管道命令
package com.qf.redispro.Test;
import com.qf.redispro.util.JedisUtil;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
public class Test8 {
public static void main(String[] args) {
//管道技术的使用-----提升操作效率
//1、获取链接
Jedis jedis = JedisUtil.getJedis();
jedis.set("javaNum", "0");
//开始计时
long beginTime = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
//每次循环单独发送一个命令
jedis.incr("javaNum");
}
long endTime = System.currentTimeMillis();
//关闭当前的连接
JedisUtil.closeJedis(jedis);
//使用管道技术,批量发送命令
jedis = JedisUtil.getJedis();
//创建管道
Pipeline pipeline = jedis.pipelined();
jedis.set("javaNum2","0");
//重新计时
long beginTime2 = System.currentTimeMillis();
for (int i=0;i<=100000;i++){
//把发送给Redis的命令,转而发送给管道即可
pipeline.incr("javaNum2");
}
//把管道里的命令提交给Redis
pipeline.syncAndReturnAll();
//获取结束时间
long endTime2 = System.currentTimeMillis();
System.out.println("在未使用管道的情况下,用时:" + (endTime-beginTime));
System.out.println("在使用管道情况下,用时:" + (endTime2-beginTime2));
}
}
Reids整合MyBatis
package com.qf.redispro.service.impl;
import com.alibaba.fastjson.JSON;
import com.qf.redispro.dao.DeptDao;
import com.qf.redispro.pojo.Dept;
import com.qf.redispro.service.DeptService;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import javax.annotation.Resource;
import java.util.List;
@Service
public class DeptServiceImpl implements DeptService {
@Resource
DeptDao deptDao;
@Override
public List<Dept> findDept() {
Jedis jedis = new Jedis("127.0.0.1",6379);
jedis.select(11);
List<Dept> list = null;
String str = jedis.get("depts");
if(str==null||str.equals("")){
list = deptDao.findDept();
jedis.set("depts",JSON.toJSONString(list));
jedis.close();
return list;
}
list = JSON.parseArray(str,Dept.class);
return list;
}
}
7、Redis中的配置
1、添加密码
进行配置文件的编写
通过redis.cli.exe执行时,先要通过auth命令输入密码
图形化桌面工具
2、事务
理解:并不是数据库事务的要么都执行、要么都不执行
Redis中的事务:把多个操作命令,放在一个队列中,通过exec命令执行事务的时候,一起执行队列中的命令
1、multi:开启事务
2、开启事务后,命令都会存放到队列中
3、exec 一起执行队列中的命令
discard 取消队列中的命令
watch:观察某个key 在开启事务之前执行,如果别的客户端也watch这个Key,当前事务自动取消
3、Redis的集群
集群:保障Redis服务高可用(即时发生单点故障,依然可以提供缓存服务)
提升了数据请求处理的能力
Redis集群所带来的问题:例如集群中有10台服务器
1、存入数据到某一台服务器,集群中如何同步数据?
2、访问的时候,访问集群中的那一台?
3.1 主从集群(生产环境不用)
搭建原则:一台主服务器---写入数据
多台从服务器---读取数据
分析:1、读写分离,从服务器会从主服务器拉取、同步数据
2、主服务器就一台,发生单点故障后,无法写入数据
结论:在生产环境中,不会采用主从结构的集群
搭建过程:
1、准备4个服务器:1主3从
2、主服务器主要配置了密码
3、配置从服务器---端口
---服从于主服务器的信息、主服务器的密码
4、启动即可
3.2 哨兵集群
原理:在主从集群的基础上,为集群中的每一台服务器都添加了一个哨兵,哨兵的职责是监控主服务器,当主服务器宕机,哨兵会投票选举,在从服务器中选举出新的主服务器,其他从服务器从新选举的主服务器上拉取、同步数据(自动故障迁移)。
图解:
1、搭建主从集群
2、在每个Redis服务的目录下,添加哨兵配置文件sentinel.conf
【概念】
主观宕机:一个哨兵认为主服务器宕机,不一定是真正宕机,有可能是网络延迟的原因
客观宕机:达到一定数量的哨兵认为主服务器宕机,认为是真正宕机,进行failover(自动故障迁移)
sentinel.conf
port 26382
sentinel monitor mymaster 127.0.0.1 6379 1
sentinel down-after-milliseconds mymaster 5000
sentinel config-epoch mymaster 12
sentinel leader-epoch mymaster 13
3、在每个服务下编写哨兵服务的启动文件
startup-sentinel.bat
title sentinel-26379 //在窗口显示标题
redis-server.exe sentinel.conf --sentinel
4、启动哨兵集群
先主 后从 先主哨兵后从哨兵
哨兵集群原理解析:
每个哨兵每隔10秒获取当前集群中的服务的拓扑结构图
每个哨兵每隔2秒通过主服务器发布、订阅哨兵自己监控的主、从服务器的情况,以及哨兵自身的信息
每个哨兵每隔1秒会向所监控的主服务器发送ping命令(心跳检测),主服务器需要在规定时间(在哨兵的配置文件中sentinel down-after-milliseconds mymaster 5000进行的配置)内返回pong
一个哨兵检测到主服务器宕机,称为主观宕机
如果达到一定数量的哨兵认为主服务器宕机,称为客观宕机
发生客观宕机后,就要进行投票选举
每个哨兵通过发送is-master-down-by-addr命令,征询其他哨兵的同意,称为新的主服务器
其他哨兵,可以同意,也可以拒绝,也可以发送is-master-down-by-addr命令
当某个哨兵同意的数量> (哨兵数量/2+1)的时候,哨兵所在的服务器就称为新的主服务器,就可以完成自动故障迁移
如果没有选举出新的主服务器,就继续投票选举
3.3:cluster集群
只能应用在Linux系统下,多主多从结构,有多个主服务器,每个主服务器对应着一个从服务器
至少:3主3从
原理:去中心化,当主服务器宕机后,从服务器自动升级为主服务器,当主服务器重启后,自动降级为从服务器
在写入数据时,由于有多个主服务器,通过哪个主服务器写入呢?
引入了hash槽的算法
8、Redis的持久化机制
Redis和Memcache对比,Redis支持持久化,而Memcache不支持
通过持久化,把内存中的数据进行存储,当Redis服务重启后,自动从持久化文件中加载数据,无需进行缓存预热
支持3种方式的持久化:
1、rdb快照方式持久化,默认支持的方式
原理:1、由Redis fork()出一个子进程,由子进程进行持久化
2、每次在符合时间条件时进行持久化,都会创建一个新的dump.rdb持久化文件
3、用新的dump.rdb持久化文件去覆盖掉旧的文件
4、快照方式,只是在符合条件的那一刻的数据进行了持久化,在本次持久化到下次持久化 之间,Redis中新增的数据可能会丢失
优点:不去抢Redis的资源,由子进程进行的持久化
效率高
缺点:丢一部分数据
2、aof只追加文件方式持久化
aop持久化,一直在一个文件中通过追加文件的方式进行持久化,文件会越来越大,效率越来越低
缺点:效率低
优点:不丢任何数据
3、rdb、aof同时进行持久化
同时开启RDB和AOF的注意事项:
如果同时开启了AOF和RDB持久化,那么在Redis宕机重启之后,需要加载一个持久化文件,优先选择AOF文件。
如果先开启了RDB,再次开启AOF,如果RDB执行了持久化,那么RDB文件中的内容会被AOF覆盖掉。
9、Redis的淘汰策略
缓存预热:把热点数据存入Redis
问题:内存中的数据已经存满,再存数据的时候,就需要首先从内存删除数据,称为数据淘汰
key的生存时间到了,Redis会立即删除吗
不会立即删除。
-
定期删除:Redis每隔一段时间就去会去查看Redis设置了过期时间的key,会再100ms的间隔中默认查看3个key。
-
惰性删除:如果当你去查询一个已经过了生存时间的key时,Redis会先查看当前key的生存时间,是否已经到了,直接删除当前key,并且给用户返回一个空值。
数据淘汰的策略:
在redis.windows.conf配置文件中
lru:最近,使用时间最少的key----强调的是使用时长
lfu:最近,使用频次最少的key----强调的是使用次数
10、Redis的常见问题及解决方案
1、缓存穿透
工具类方法:
public static String getData(String type,String key){
//判断是何种类型,用对应的方法都Redis中获取指定key所对应的值
}
获取数据的思路:
第一步:到Redis中获取
第二步:如果到Redis中获取不到,则到数据库中获取。
第三步:把从数据库中查询的数据,存入Redis
问题:恶意的传递不存在的key,发出大量的获取数据的请求,大量的请求对应key的数据,缓存未命中,导致大量的请求到达数据库,从而把数据库请求瘫痪,这个问题称为缓存穿透
归纳:请求的数据在Redis中没有,数据库也没有
缓存穿透的解决方案:
1、缓存空值,把请求的key 使用空值进行缓存,如果下次还是用同样的key,那么请求就不会穿透,从而达到数据库了
缺点:可能会缓存大量的空值
2、布隆过滤,把Redis中所有的key,存入到一个集合里,当请求Redis数据的时候,判断请求的key是否在集合中,如果在集合中,则获取数据,如果不在则直接返回
缺点:存放Key的集合需要实时维护
如果查询的key不是热点输入,也没有道数据库中查询,直接返回空
2、缓存击穿
问题描述:请求的key,在Redis中有存在值,在某一时刻,大量的key同时过期了,如果此刻,有大量的请求去请求过期的key,请求就穿透Redis到达数据库服务器。
解决方案:
1、热点数据设置为永久有效
2、把热点数据的过期时间,分散设置,避免大量的数据在同一时间点过期
3、缓存雪崩
问题描述:Redis集群都不能正常提供服务了,导致大量的请求直接到达数据库
解决:确保集群的稳定性
4、缓存倾斜
问题描述:请求的热点数据,并没有平均分布在Redis集群的各个节点上,而是分布在某个或某几个节点上,如果大量的热点数据请求到达,可能造成倾斜的节点的宕机
解决方案:集群一定要合理
哨兵集群:热点数据的分布比较合理
cluster集群:key的hash值在hash槽中