微服务设计指导-让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
@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里。
@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秒