微服务设计指导-让Redis循环写入时提高10倍的技巧

简介

有微服务的架构不代表性能好,而使用微服务的架构必须要求性能好。这句话不矛盾。矛盾在外面很多人认为微服务架构代表高并发,实际上不是。我们有“书面微服务”和“实际微服务”之说。比如说网上大量教人把httpConnection或者是FeignClient的timeout改成30秒就不会超时了?那这要什么微服务呢?微服务解决的到底是什么呢?

绝大多数人忘记了微信的本质是用来解决什么问题的。

互联网应用在To C端有6秒之说,即一个小程序/APP应用打开和加载过程>6秒,肯定新用户就不会再去用了,4秒算平均水平,一般大厂都是做到加载页面1秒。

各位要知道,任何一个“查询”式页面打开一秒,全站可能存在上千、上万个API,有时一个页面是需要通过几十个API组合在一起的。因此对于API在系统内我们互联网界的要求是约束在“一根API需要在前端万级并发的情况下+系统每张单表千万数据的Data Volumn下响应时间在100毫秒之内的“。

这就是1秒钟说的由来。

  • To C端界面加载1秒;
  • 任何微服务(即时响应类)要在100毫秒内

这是因为,你的应用上会有不少外联、不少回调、不少地图、配送、支付这种调用。它们都会对我们的应用造成“级联式雪崩”。

为了避免复杂系统的雪崩就需要及时“熔断、限流、升级”。这就是微服务的本质。

如果已经用了微服务架构、实际发觉这边老是超时,于是很多人一味地去放长这个连接时间自以为就解决了。

这样的人群占比大概超过90%(业界有统计)。

因此用了微服务即代表着对于开发的技能要求水平更高,任何可以提高100毫秒的地方都值得去做。

微服务架构里特别强调一个哲理“勿以善小而不为之,勿以恶小而为之”。

Redis在for循环里写大量数据的梗

通常情况我们经常会碰到要把千、万条数据一次写入Redis,在互联网应用场景中,我们经常把百万、千万级数据也会往Redis里塞,如:Redis Bloom。

经典的写法都是以下这样的:

@Test
    public void testForLoopAddSingleString() throws Exception {
        long startTime = System.currentTimeMillis();
        String redisValue = "";
        for (int i = 0; i < 10000; i++) {
            StringBuilder redisKey = new StringBuilder();
            redisKey.append("key_").append(i);
            redisValue = String.valueOf(i);
            redisTemplate.opsForValue().set(redisKey.toString(), redisValue);
        }
        long endTime = System.currentTimeMillis();
        long costTime = endTime - startTime;
        logger.info(">>>>>>test batch add into redis by using normal for loop spent->" + costTime);

    }

运行后得到如下输出:

Come on man,这才1万条数据,插入Redis要用4.7秒。

我用的是我一直用于模拟千万级数据量的服务器,这台服务器比公司的生产服务器性能还要好几倍,在这样的服务器性能上插入1万条数据都要4.7秒,生产上我们还要算上读存储出来再写Redis的网络开销,实际只会更慢。这种效率是不能忍受的。

增强版写法-Redis Piepeline写法

我们给Redis在它的“写操作上”,不需要增加任何第三方包,自带一颗“永久有效”写加速器,如下代码:

@Test
    public void testPipelineAddSingleString() throws Exception {
        RedisSerializer<String> keySerializer = (RedisSerializer<String>)redisTemplate.getKeySerializer();
        RedisSerializer<Object> valueSerializer = (RedisSerializer<Object>)redisTemplate.getValueSerializer();
        long startTime = System.currentTimeMillis();
        redisTemplate.executePipelined((RedisCallback<Object>)pipeLine -> {
            try {
                String redisValue = "";
                for (int i = 0; i < 10000; i++) {
                    StringBuilder redisKey = new StringBuilder();
                    redisKey.append("key_").append(i);
                    redisValue = String.valueOf(i);
                    pipeLine.setEx(keySerializer.serialize(redisKey.toString()), 10,
                        valueSerializer.serialize(redisValue));
                }
            } catch (Exception e) {
                logger.error(">>>>>>test batch add into redis by using pipeline for loop error: " + e.getMessage(), e);
            }
            return null;
        });
        long endTime = System.currentTimeMillis();
        long costTime = endTime - startTime;
        logger.info(">>>>>>test batch add into redis by using pipeline for loop spent->" + costTime);
    }

大家来看这种写法,你可以认为这种写法和for循环里套着一条条的update table set field=value变成了合并后一个batchUpdate的用法一样。看一下,这种写法带来的效率上的区别吧:

Oh My God。。。写法上有一点不同。

4,745毫秒 VS 564毫秒,我算它600毫秒好了,但这得差多少?各位想想。

下面同样给出相应的简单Hash结构的批量写Redis写法

假设我们要往redis里插一个这样的结构的Hash

[图片上传失败...(image-1b12e6-1649837031485)]

@Test
    public void testPipelineAddHash() throws Exception {
        RedisSerializer<String> keySerializer = (RedisSerializer<String>)redisTemplate.getKeySerializer();
        RedisSerializer<Object> valueSerializer = (RedisSerializer<Object>)redisTemplate.getValueSerializer();
        long startTime = System.currentTimeMillis();
        String hashKey = "pipeline_hash_key";
        redisTemplate.executePipelined((RedisCallback<Object>)pipeLine -> {
            try {
                String redisValue = "";
                for (int i = 0; i < 10000; i++) {
                    StringBuilder redisKey = new StringBuilder();
                    redisKey.append("key_").append(i);
                    redisValue = String.valueOf(i);
                    pipeLine.hSet(keySerializer.serialize(hashKey), keySerializer.serialize(redisKey.toString()),
                        valueSerializer.serialize(redisValue));
                }
            } catch (Exception e) {
                logger.error(">>>>>>testPipelineAddHash by using pipeline for loop error: " + e.getMessage(), e);
            }
            return null;
        });
        long endTime = System.currentTimeMillis();
        long costTime = endTime - startTime;
        logger.info(">>>>>>testPipelineAddHash by using pipeline for loop spent->" + costTime);
    }

只用了500毫秒左右。

批量往Redis里插入复杂类型Hash的写法

如下,1万个JavaBean以Hash结构存在Redis里。

[图片上传失败...(image-be9212-1649837031485)]

@Test
    public void testPipelineAddHashBean() throws Exception {
        RedisSerializer<String> keySerializer = (RedisSerializer<String>)redisTemplate.getKeySerializer();
        Jackson2JsonRedisSerializer jacksonSerial = new Jackson2JsonRedisSerializer<>(Object.class);
        long startTime = System.currentTimeMillis();
        String hashKey = "pipeline_hash_key";
        redisTemplate.executePipelined((RedisCallback<Object>)pipeLine -> {
            try {
                String redisValue = "";
                for (int i = 0; i < 10000; i++) {
                    StringBuilder redisKey = new StringBuilder();
                    redisKey.append("key_").append(i);
                    redisValue = String.valueOf(i);
                    UserBean user = new UserBean();
                    user.setUt(redisKey.toString());
                    user.setShareCode(redisValue);
                    pipeLine.hSet(keySerializer.serialize(hashKey), keySerializer.serialize(redisKey.toString()),
                        jacksonSerial.serialize(user));
                }
            } catch (Exception e) {
                logger.error(">>>>>>testPipelineAddHash by using pipeline for loop error: " + e.getMessage(), e);
            }
            return null;
        });
        long endTime = System.currentTimeMillis();
        long costTime = endTime - startTime;
        logger.info(">>>>>>testPipelineAddHash by using pipeline for loop spent->" + costTime);
    }

400毫秒左右(有时600毫秒、有时400毫秒、大多情况在400毫秒内)

Redis Bloom过滤器内的类Pipeline写法

同理,往Redis Bloom过滤器里喂值这个量还要大,那么喂入的越快,bloom拦截时的“黑窗期”就越短,对系统越有利。因此它也有同样的批量写入的好方法,如下代码:

public <T> void addByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
        Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能为空");
        int[] offset = bloomFilterHelper.murmurHashOffset(value);
        BitFieldSubCommands commands = BitFieldSubCommands.create();// 使用合并写法,假设10万条数据一个for需要12秒
        for (int i : offset) {
            commands = commands.set(BitFieldSubCommands.BitFieldType.unsigned(1)).valueAt(i).to(1);// 合并bit
        }
        redisTemplate.opsForValue().bitField(key, commands);// 再一次写入redis
    }

10万条数据用了45秒

如果用传统写法 如以下代码为传统写法

public <T> void addByBloomFilterSingleFor(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
        Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能为空");
        int[] offset = bloomFilterHelper.murmurHashOffset(value);
        for (int i : offset) {
            redisTemplate.opsForValue().setBit(key, i, true);
        }
    }

10万条数据用了455秒

原文
https://blog.csdn.net/lifetragedy/article/details/123130025

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容