前言
在我们的业务中,会存在一些数据迁入的问题,在迁入时,原业务的数据的核心数据都是基于Redis存储的,所以需要将批量的核心数据批处理到redis中。那如何来批量操作呢?
如果我们使用set方法一条一条的写入会有什么问题呢?
如果不使用set的话我们应该如何来处理呢?
基于以上的一些问题,我们有了今天的这篇文章 。
首先可以明确一点,对于批量写入redis的操作,肯定是不能直接用set这种单一命令来写入的,批量的连接和网络传输对于redis来说性能损耗是非常巨大的。
使用类似set这样单个命令的执行流程
单个命令的执行
前言中已经说了使用set肯定是不行的。那我们来具体分析一下为什么不行。
对于单个命令来说,执行一个set命令总的分为3步:
第一步:客户端建立连接并向服务端发送 set name key
这样一条命令
第二步:服务端解析并执行命令。
第三步:服务端返回执行结果给客户端.
一次命令的响应时间 = 1次往返的网络传输耗时 + 1次Redis执行命令耗时
N条命令的执行
上面是执行一条命令。那如果执行N条命令呢。那就是将单条命令重复N次。
总结一个结果就是:N次的发送命令和返回结果(通过网络传输) 、N次的内存执行命令。
我们应该也清楚,网络传输可以说是redis性能的瓶颈所在,所以通过N条这样重复的命令并发请求redis时,可能会导致redis出现异常阻塞。导致其他正常业务命令执行也阻塞。
N次命令的响应时间 = N次往返的网络传输耗时 + N次Redis执行命令耗时
最理想的N条命令的执行
其实我们最理想的方案应该是,我们一次批量发送多条命令,redis批量执行多条命令后,直接返回多个接口。
我们用生活中一个例子解释一下:
比如我们割麦子,如果我们一根麦子一根麦子的割,这样是不是会耗费大量的人力,大家都去割麦子了导致棉花都没人收了。
那如果我们用收割机一排一排的割,是不是就减少了人力的投入,其他人该干啥干啥,互不耽误。
N次命令的响应时间 = 1次往返的网络传输耗时 + N次Redis执行命令耗时
详细说说N次命令往返执行所消耗的资源
上面我们大概介绍了命令往返的流程分为3步。接下来我们具体说一下这三步为什么说在N次频繁处理时会出现性能瓶颈问题。
对于发送命令、返回结果这样的一个操作,它的一次数据包往返于两端的时间我们称作Round Trip Time(简称RTT)。
那对于N次命令来说,则会有N次的RTT,同时redis需要调用N次的read()和write()这两个系统方法将数据从用户态转移到内核态。然后再N次调用系统来发送服务端到客户端的响应网络请求。
以上就是N次命令大概所消耗的资源了。
实现批处理的方案
原生方法
我们可以通过原生的方法来进行批量的写入,redis自身就提供了很多这种方法,比如mset
、hmset
等。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class RedisService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void mset100kData() {
Map<String, String> dataMap = new HashMap<>(100000);
for (int i = 0; i < 100000; i++) {
dataMap.put("key" + i, "value" + i);
}
stringRedisTemplate.opsForValue().multiSet(dataMap);
}
}
mset、hmset这些原生方法其实是非常好用的,但有一个缺点就是:它只能处理对应的数据类型。如果我们有更复杂或者有多种混合结构的数据,那它就无法处理了。所以我们引入第二种处理方式:pipeline 。
pipeline
MSET虽然可以批处理,但是却只能操作部分数据类型,因此如果有对复杂数据类型的批处理需要,建议使用Pipeline功能
pipeline是批处理命令变种优化措施,它非常类似于redis的mset 。
命令行执行
我们通过命令行操作的话非常简单,将需要执行的内容写好,一次性执行。
可以看到。我们在cmd.txt中写下了3条命令:hset k300 age 20
、lpush list 1 2 3 4 5
另外一条就不在这里凑字数了。
通过命令一次性就写入到了redis中
cat cmd.txt | redis-cli -a 111111 --pipe
java代码执行
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class RedisService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void mset100kDataWithPipeline() {
final Map<String, String> dataMap = new HashMap<>(100000);
for (int i = 0; i < 100000; i++) {
dataMap.put("key" + i, "value" + i);
}
stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for (Map.Entry<String, String> entry : dataMap.entrySet()) {
connection.set(entry.getKey().getBytes(), entry.getValue().getBytes());
}
return null;
});
}
}
集群下如何解决批处理key的插槽问题?
对于MSET或Pipeline这样的批处理来说,会在一次请求中携带多条命令。
对于Redis集群,必须保证批处理命令的多个key落在一个插槽中,否则就会导致执行失败。
但是直接 通过MSET这种方式的执行,多个key通过hash计算出来的值肯定不会是一个插槽区间。 所以应该如何解决这个问题呢?
我们有四种解决方案,但是这四种方案都有缺点。没有最完美的方案,只有最适合的方案。
第一种:串行执行(最简单的方案)
串行执行其实就是我们说的循环单次执行,每次执行1条,这样计算的key肯定是没有问题的。
缺点:性能差,时间久
第二种:串行批量执行(稍复杂,但时间较短)
串行批量执行的原理非常简单:通过调用redis的hash计算函数,将原数据进行批量的分组,将同一hash结果的分为同一组进行批量执行。这样也可以确保所有的key都是在同一个插槽。
缺点:执行时间和性能取决于分组的数量,分组数量多了性能就越差。
第三种:并行批量执行(复杂,时间最短)
并行批量执行的原理与串行批量执行类似:通过调用redis的hash计算函数,将原数据分组后, 并发的执行。
缺点:代码处理稍复杂,出问题了不好寻找。
第四种:使用hash_tag(方案简单且时间较短)
简单的说一下hash_tag,就是对key取一个相同的前缀,通过{}将前缀值包裹起来,redis计算哈希时就会通过包裹的数据来进行计算。这样的话可以确保数据在同一个插槽。
{user:init}:id:1
{user:init}:id:2
{user:init}:id:3
user:init 就是hash_tag。redis会对这3个key都通过user:init来计算插槽
缺点:这一批数据都在同一个插槽,会出现数据倾斜。
串行命令 | 串行slot | 并行slot | hash_tag | |
---|---|---|---|---|
实现思路 | for循环遍历,依次执行每个命令 | 在客户端计算每个key的slot,将slot一致分为一组,每组都利用Pipeline批处理。串行执行各组命令 | 在客户端计算每个key的slot,将slot一致分为一组,每组都利用Pipeline批处理。并行执行各组命令 | 将所有key设置相同的hash_tag,则所有key的slot一定相同 |
耗时 | N次网络耗时 + N次命令耗时 | m次网络耗时 + N次命令耗时m = key的slot个数 | 1次网络耗时 + N次命令耗时 | 1次网络耗时 + N次命令耗时 |
优点 | 实现简单 | 耗时较短 | 耗时非常短 | 耗时非常短、实现简单 |
缺点 | 耗时非常久 | 实现稍复杂slot越多,耗时越久 | 实现复杂 | 容易出现数据倾斜 |
总结
我们介绍了批量写入redis的多种方案以及通过循环单次执行的问题所在。
批量写入对于业务来说并不是特别常见,但终究会是有的。希望大家能通过本文的学习了解一下redis的一些方面。