Redis-最佳实战-键值设计、批处理优化、服务端优化、集群最佳实践

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%的公司都用不到集群,主从就完全满足了;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值