在我们的业务中,会存在一些数据迁入的问题,在迁入时,原业务的数据的核心数据都是基于redis存储的,所以需要将批量的核心数据批处理到redis中。那如何来批量操作呢?
如果我们使用set方法一条一条的写入会有什么问题呢?
如果不使用set的话我们应该如何来处理呢?
基于以上的一些问题,我们有了今天的这篇文章 。
首先可以明确一点,对于批量写入redis的操作,肯定是不能直接用set这种单一命令来写入的,批量的连接和网络传输对于redis来说性能损耗是非常巨大的。
前言中已经说了使用set肯定是不行的。那我们来具体分析一下为什么不行。
对于单个命令来说,执行一个set命令总的分为3步:
第一步:客户端建立连接并向服务端发送 set name zhangsan 这样一条命令
第二步:服务端解析并执行命令。
第三步:服务端返回执行结果给客户端.
上面是执行一条命令。那如果执行N条命令呢。那就是将单条命令重复N次。
总结一个结果就是:N次的发送命令和返回结果(通过网络传输) 、N次的内存执行命令。
我们应该也清楚,网络传输可以说是redis性能的瓶颈所在,所以通过N条这样重复的命令并发请求redis时,可能会导致redis出现异常阻塞。导致其他正常业务命令执行也阻塞。
其实我们最理想的方案应该是,我们一次批量发送多条命令,redis批量执行多条命令后,直接返回多个接口。
我们用生活中一个例子解释一下:
比如我们割麦子,如果我们一根麦子一根麦子的割,这样是不是会耗费大量的人力,大家都去割麦子了导致棉花都没人收了。
那如果我们用收割机一排一排的割,是不是就减少了人力的投入,其他人该干啥干啥,互不耽误。
上面我们大概介绍了命令往返的流程分为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是批处理命令变种优化措施,它非常类似于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
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;
});
}
}
对于MSET或Pipeline这样的批处理来说,会在一次请求中携带多条命令。
对于Redis集群,必须保证批处理命令的多个key落在一个插槽中,否则就会导致执行失败。
但是直接通过MSET这种方式的执行,多个key通过hash计算出来的值肯定不会是一个插槽区间。所以应该如何解决这个问题呢?
我们有四种解决方案,但是这四种方案都有缺点。没有最完美的方案,只有最适合的方案。
串行执行其实就是我们说的循环单次执行,每次执行1条,这样计算的key肯定是没有问题的。
缺点:性能差,时间久
串行批量执行的原理非常简单:通过调用redis的hash计算函数,将原数据进行批量的分组,将同一hash结果的分为同一组进行批量执行。这样也可以确保所有的key都是在同一个插槽。
缺点:执行时间和性能取决于分组的数量,分组数量多了性能就越差。
并行批量执行的原理与串行批量执行类似:通过调用redis的hash计算函数,将原数据分组后,并发的执行。
缺点:代码处理稍复杂,出问题了不好寻找。
简单的说一下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来计算插槽
缺点:这一批数据都在同一个插槽,会出现数据倾斜。
我们介绍了批量写入redis的多种方案以及通过循环单次执行的问题所在。
批量写入对于业务来说并不是特别常见,但终究会是有的。希望大家能通过本文的学习了解一下redis的一些方面。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。