Redis键值设计
1、优雅的key结构
key==login(登录信息):user(用户信息):10(用户id) 简单明了,有层级目录
节省内存空间,如果key都是数字,那么底层会自动转为int 存储;如果是字符串并且少于44个字节,那么底层就用embstr,是连续的;如果超过了44个字节就不是连续的了,利用指针读取了
测试:
- num 的value是数字时,类型是int;
- num 的value是字符串并且小于等于44个字节时,底层类型是embstr
- num 的value是字符串并且大于44个字节时,底层类型是raw
2、拒绝BigKey
通过命令可以查看key的占用大小字节:memory usage key 但是这个命令对cps占用大,不建议使用,那么如何去衡量一个key 的大小呢?
如下的占用字节,是包括key 包括value,一个整体的字节
事实上
- 如果是字符串类型我们只要自己去衡量value 的大小就行了;--strlen key
- 如果是集合类型,只需要去衡量集合的大小就行;-- llen key
危害,运算指的是,交集等
如何避免bigkey问题?
第三方工具,实际上更推荐使用,但是因为是离线,所以时效性上有差异,不是最新的
网络监控,如果是用的阿里云,里面都提供好了,都可以找到bigKey等,云服务就是贵
第一:
- 打开一个新窗口,执行redis-cli --bigkeys,
- 如果没有权限那就是得加上密码:redis-cli -a 密码 --bigkeys
- 但是:这个统计只能看到某一类型的第一名,如果第一的是bigKey,不能保证第二个就不是bigkey,所以只供参考
第二: 自己编程,注意不能用keys * 去扫描,如果和key很多,一个keys * 把所有的key都拿出来了,可能会用时几十秒,redis是单线程的,会造成主线程阻塞,别的请求进不来;
利用scan命令,利用迭代的方式,他不会占用主线程,不会导致阻塞,只需要指定一个下标,第一次扫描的时候给0就行,可以指定类型,可以指定一次获取几个,默认是10
返回的18就是下一次要扫描的下标,这次又是29,就是再下次的下标
当光标回到0的时候,就证明全部扫描了,如果写代码的话,可以用一个while循环即可
我们拿到这些key去干什么呢?去判断字符串的长度,或者去判断集合元素的个数;
如果要想准确判断大小可以使用memory usage key,但是对cpu有太大的消耗了,不建议使用
注意,这段代码尽量不要放到主节点执行,如果有主从的话,放到从节点去执行
public class JedisTest {
private Jedis jedis;
@BeforeEach
void setUp() {
// 1.建立连接
jedis = new Jedis("127.0.0.1", 6379);
//jedis = JedisConnectionFactory.getJedis();
// 2.设置密码
// jedis.auth("123321");
// 3.选择库
jedis.select(0);
}
@Test
void testString() {
// 存入数据
String result = jedis.set("name", "虎哥");
System.out.println("result = " + result);
// 获取数据
String name = jedis.get("name");
System.out.println("name = " + name);
}
@Test
void testHash() {
// 插入hash数据
jedis.hset("user:1", "name", "Jack");
jedis.hset("user:1", "age", "21");
// 获取
Map<String, String> map = jedis.hgetAll("user:1");
System.out.println(map);
}
final static int STR_MAX_LEN = 10 * 1024;
final static int HASH_MAX_LEN = 500;
@Test
void testScan() {
int maxLen = 0;
long len = 0;
String cursor = "0";
do {
// 扫描并获取一部分key
ScanResult<String> result = jedis.scan(cursor);
// 记录cursor
cursor = result.getCursor();
List<String> list = result.getResult();
if (list == null || list.isEmpty()) {
break;
}
// 遍历
for (String key : list) {
// 判断key的类型
String type = jedis.type(key);
switch (type) {
case "string":
len = jedis.strlen(key);
maxLen = STR_MAX_LEN;
break;
case "hash":
len = jedis.hlen(key);
maxLen = HASH_MAX_LEN;
break;
case "list":
len = jedis.llen(key);
maxLen = HASH_MAX_LEN;
break;
case "set":
len = jedis.scard(key);
maxLen = HASH_MAX_LEN;
break;
case "zset":
len = jedis.zcard(key);
maxLen = HASH_MAX_LEN;
break;
default:
break;
}
if (len >= maxLen) {
System.out.printf("Found big key : %s, type: %s, length or size: %d %n", key, type, len);
}else {
System.out.println("没有bigKey");
}
}
} while (!cursor.equals("0"));
}
@AfterEach
void tearDown() {
if (jedis != null) {
jedis.close();
}
}
}
当我们找到bigKey怎么办?想办法把这些数据拆分以后重新存储,然后删除bigKey
4.0后版本unlink,异步的删除,利用其他线程,而不是主线程,这样就不会阻塞了
3.0前版本用hdel 一个一个字段删,删完了再删key
如何遍历呢,还是用scan 只不过有类型的,如hash就用hscan,字符串就用sscan,zscore就用zscan
3、恰当的数据类型
bigKey打散拆分后选用合适的小key
entry上限尽量不要调,如要非要的话,也不要超过1000,
内存占比62.23M
通过config 命令,可以获取和修改配置,动态修改,重启后失效默认500
string类型,“123”--就会占48个字节,说明字符串内存占比挺大的,3个字节就能保存的事,,因为String里边有很多元信息,发现内存占用77.54M,比hash 100万键值对,内存占比还要大
- 算法:
- 0-99/100 都等于0,0-99%100, 等于0-99
- 100-199/100 都等于1,100-199%100 等于0-99
- 200-299/100 都等于2,200-299%100 等于0-99
- ...
这种方案就是将hash打散,100、500、800都可以,只要1000以下就行
经过测试,内存占比大概是24M左右,也没有bigKey就解决了内存占比大,bigKey问题
代码
@Test
void testBigHash() {
Map<String, String> map = new HashMap<>();
for (int i = 1; i <= 100000; i++) {
map.put("key_" + i, "value_" + i);
}
jedis.hmset("test:big:hash", map);
}
@Test
void testBigString() {
for (int i = 1; i <= 100000; i++) {
jedis.set("test:str:key_" + i, "value_" + i);
}
}
@Test
void testSmallHash() {
int hashSize = 100;
Map<String, String> map = new HashMap<>(hashSize);
for (int i = 1; i <= 100000; i++) {
int k = (i - 1) / hashSize;
int v = i % hashSize;
map.put("key_" + v, "value_" + v);
if (v == 0) {
jedis.hmset("test:small:hash_" + k, map);
}
}
}
批处理优化
为什么要批处理?相对于网络传输的时间,Redis服务端执行命令的时间可以忽略不计,主要都是网络传输耗时,比如我们ping baidu.com 可能需要5毫秒,而Redis执行一条命令可能只需要2微秒,Redis1秒 就说能执行5万条命令,你说快不快,所有的时间都浪费在了网络传输上了
1条命令执行一次
插入10万条数据,耗时40多秒,这要时间都浪费在了网络传输上了
@Test
void testFor() {
for (int i = 1; i <= 100000; i++) {
jedis.set("test:key_" + i, "value_" + i);
}
}
一条一条的执行,这种方式是非常耗时的,非常不建议大批数量倒入的时候使用
N条命令批量处理执行,一次执行多条命令,网络传输次数少了,自然就节省时间了,因为Redis服务端他的速度是非常快的
Pipeline:实现单机模式下的批处理
new一个数组,长度2000,那么它就可以放2000/2=1000个 键值对
测试1000个键值对 插入一次,减少网路传输,经过测试,只需要182毫秒,连1秒都不到,对比上次需要44秒,我的天哪,这就是100次网络传输与10万次网络传输的区别
@Test
void testMxx() {
// mset key value key value... 2000个数组,1000个键值对
String[] arr = new String[2000];
int j;
long b = System.currentTimeMillis();
for (int i = 1; i <= 100000; i++) {
/*
正好2000个参数,1000个键值对
<< 左移一位 就是 * 2 左乘右除
j = 0、2、4、6、8...1998
j+1 = 1、3、5、7...1999
*/
j = (i % 1000) << 1;
arr[j] = "test:key_" + i;
arr[j + 1] = "value_" + i;
// i从1开始,当i等于1000的的倍数时候,正好%是0,正好是1000个键值对
// 2000个数,正好将数组填满
if (j == 0) {
// 1000个键值对 放一次,减少了网络传输
jedis.mset(arr);
}
}
long e = System.currentTimeMillis();
System.out.println("time: " + (e - b));
}
但是,不是所有类型都有批处理,mset只能处理字符串,list没有,这是jedis
pipeline模式
@Test
void testPipeline() {
// 创建管道
Pipeline pipeline = jedis.pipelined();
long b = System.currentTimeMillis();
for (int i = 1; i <= 100000; i++) {
// 放入命令到管道,pipeline 可以支持存放多种数据类型,set就是字符串
// 比如说hset、lpush 等任意add zadd等,也就是说redis的所有数据类型都可以去批处理
pipeline.set("test:key_" + i, "value_" + i);
if (i % 1000 == 0) {
// 每放入1000条命令,批量执行
pipeline.sync();
}
}
long e = System.currentTimeMillis();
System.out.println("time: " + (e - b));
}
StringRedisTemplate 里也有,应该是executePipeline,一个道理,速度也非常快,
那么pipeline 和mset等 选谁呢?虽然m操作会比pipeline要快点,因为m操作是redis的内置操作,内置原子性,m操作会把一次性的命令都执行完;而pipeline不是,这一组命令发到redis,但是这一组命令不是一次都执行了的,可能我这一组命令发到一半,别的客户端也发送了一条命令,相当于放到一个队列里,有插队的情况,redis会从队列里一次执行相关命令,而m操作没有插队的情况,虽然多,但是也是可以接受的
因此如果我们的命令需要原子性的时候可以用m操作
不能携带太多命令,不能把带宽一次都沾满了,那大家就都没得玩了
以上pipeline 只是单机模式下的批处理
集群下的批处理
并行执行:如果1000条命令分为10组嘞,那么就开启10个线程去执行
hash_tag key的有效部分 {a}name {a}age {a}就是有效部分,slot插槽
第四种方式更好,一次网络耗时,+N次命令,还不会开启多个线程
所有的key都设置一个插槽,如果有1万个key那就都扔到同一个节点上了,这个节点将来的数据量可能就比别的节点多很多--这就是数据倾斜,所以并不推荐
我们推荐并行slot
批处理:一次链接中的key 需要再一个节点一个插槽,因为是批处理,要求一次执行完,,并行和串行,就是将插槽一致的分为一组,一组一组的去执行,每一组都是一次执行完,这才是批处理,如果每个key的插槽都不一样,每条命令操作一个key,那就不是批处理了
在我们的StringRedistTempleate 里,spring下,已经解决了批处理的问题了,原理就是将插槽一样的分为一组,然后异步的去执行--并行slot,不要头铁去中jedis,还得自己去写
服务端优化
持久化配置
。。。
慢查询
会导致redis主线程阻塞,影响性能,redis服务端来了命令,其实有个入队操作,会一次执行的;需要找到慢查询并解决它
推荐慢查询设置为1000微妙,也就是1毫秒,因为redis查询时微妙来计算的
可以放到日志里,默认128条,通过set进行修改
这样慢查询就被记录下来了,记录下来不是目的,需要找到他们并解决
一般keys * 就是慢查询,查询时间较久
导致慢查询,比如数据结构不合理,hash 存了1万个键值对等,可以通过客户端查看慢查询日志,和服务器的一些信息,一目了然
命令及安全配置
这是默认,没有任何限制,谁都可以访问,而且rendis默认没有密码设置
生成公私秘钥
ssh 远程登录命令,登录账号root@129.168.150.101,回车发现需要服务器密码
服务器密码不会告诉别人,大多数情况下是安全的,别人是无法访问的
但是ssh免密登录是有问题的,每次登录都需要密码,很麻烦,就出现了免秘钥授权登录
利用ssh 生成一对公钥和私钥,公钥放到服务器上,私钥留在本地,这样的话,当我们去登录服务器,服务器和我们本地利用公私钥进行加密的认证,认证后发现没问题,就会认为你是一个可信任的,从而允许访问,前提是公钥信息必须保存在服务器上才可以
实现
- 登录服务器,将公钥放到服务器上,创建一个文件 叫 .ssh,免密登录的公钥都是放在这里的
- 然后将公钥文件夹上传,上传后重命名,mv重命名
- 持有私钥的本地我们,再去登录,就不用密码了
注意:我们先以管理的身份登录了服务器,才把公钥放进来的,那我要不知道服务器的账号密码,是不是就没有办法放呢?这种情况下服务器是否是安全的呢?
但是Redis就有这么一个漏洞,可以不登录服务器的前提下,将公钥秘钥送到 服务器里,这就有问题了
- 准备一个foo文件,将公钥秘钥写进去
- 192.168.1.11是要攻击的redis,| 管道,set crackit 这个名字随便起,set命令 key 还有value,这里的value就是通过管道传进来的,foo.txt文件的内容,现在在redis就有了一组键值对,key叫crackit value就是公钥信息,这样仅仅是将公钥保存到redis了,需要保存到root目录下
- 利用redis客户端去连接,并且c'o'n'fig set命令,修改redis配置 dir 是持久化保存的目录,修改成了放秘钥的位置,并该文件的内容就是redis里边所有的数据,包括我们上边的公钥key 和value
- 然后指定rdb文件的名称,是秘钥名称
一旦我们执行了RDB文件,这个文件就会自动存到.ssh目录下,并且文件的内如就是redis里边所有的数据,而里面有个key 叫crackit,那么这个文件就有有秘钥了
这个大前提,
第一就是我们可以连到该redis服务器
第二就是可以修改redis.conf 文件
第三就是redis具备写入本地目录的权限,有root权限
即:
针对性处理:
修改配置文件 vim redis.conf
修改配置文件,并重启redis,登录,发现keys * 不可以用了,因为改成了hehe
修改网卡,默认是0.0.0.0,这谁都可以访问;
可以修改成公司的局域网,外网就无法访问,或者从跟上redis服务器就不要有外网
尽量不要使用默认的端口,6379,换一个,比如6388,别人不知道的
内存配置
重点关注数据内存,有没有bigkey 还有就是缓冲区内存
集群最佳实践
部分插槽坏了,整个集群不会停止对外服务,可以改成false,集群依然可用
比如说将某一个主从节点干掉
在set的时候,说集群不可用,
修改集群下的配置vi redis.conf ,将yes 改成 no,添加一条命令这里是
set 发现依然可用,前提是插槽值落在健康的节点上,如果落在不可用的节点插槽,那么依然不可用
集群节点之间ping 节点不能太多,节点太多的话,数据就多,
可以拆分,每个业务可以有一个小集群,不要到放到一个集群整个成千上万个节点
超时时间-ping的时间
能使用主从,最好就使用主从,80%的公司都用不到集群,主从就完全满足了;