1. 说一下Hive怎么优化?(腾讯)
a)MapJoin如果不指定MapJoin或者不符合MapJoin的条件,那么Hive解析器会将Join操作转换成Common Join,即:在Reduce阶段完成Join,容易发生数据倾斜,我们可以用MapJoin把小表全部加载到内存,在map端进行Join,避免Reducer处理。
b)行列过滤列处理:在SELECT中,只拿需要的列,如果有,尽量使用分区过滤,少用SELECT *。行处理:在分区剪裁中,当使用外关联时,如果将副表的过滤条件写在Where后面,那就会先全表关联,之后再过滤。
c)采用分桶技术
d)采用分区技术
e)合理设置Map数
- 1)通常情况下,作业会通过Input的目录产生一个或者多个Map任务。 主要的决定因素有:Input的文件总个数,Input的文件大小,集群设置的文件块大小。
- 2)是不是map数越多越好? 答案是否定的。如果一个任务有很多小文件(远远小于块大小128m),则每个小文件也会被当做一个块,用一个Map任务来完成,而一个Map任务启动和初始化的时间远大于逻辑处理的时间,就会造成很大的资源浪费。而且,同时可执行的Map数是受限的。
- 3)是不是保证每个Map处理接近128m的文件块,就高枕无忧了? 答案也是不一定。比如有一个127m的文件,正常会用一个Map去完成,但这个文件只有一 个或者两个小字段,却有几千万的记录,如果Map处理的逻辑比较复杂,用一个Map任务去做,肯定也比较耗时。 针对上面的问题2和3,我们需要采取两种方式来解决:即减少Map数和增加Map数;
f)小文件进行合并在Map执行前合并小文件,减少Map数:CombineHiveInputFormat具有对小文件进行合并的功能(系统默认的格式)。HiveInputFormat没有对小文件合并功能。
g)合理设置Reduce数 Reduce个数并不是越多越好
- 1)过多的启动和初始化Reduce也会消耗时间和资源;
- 2)另外,有多少个Reduce,就会有多少个输出文件,如果生成了很多个小文件,那么如果这些小文件作为下一个任务的输入,则也会出现小文件过多的问题;在设置Reduce个数的时候也需要考虑这两个原则:处理大数据量利用合适的Reduce数;使单个Reduce任务处理数据量大小要合适。
h)常用参数 输出合并小文件
- SET hive.merge.mapfiles = true; -- 默认true,在map-only任务结束时合并小文件
- SET hive.merge.mapredfiles = true; -- 默认false,在map-reduce任务结束时合并小文件
- SET hive.merge.size.per.task = 268435456; -- 默认256M
- SET hive.merge.smallfiles.avgsize = 16777216; -- 当输出文件的平均大小小于该值时,启动一个独立的Map-Reduce任务进行文件Merge。
2. 简述Redis的热键问题,以及Redis的数据类型 。(京东)
热键问题:
1)利用二级缓存,将热k放到JVM的缓存当中,将请求分散到多台机器上;
2)备份热k,将热点k备份到多台Redis中,当热点k请求时,可以随机的从备份的选一台,进行访问取值,返回数据。 Redis中存储数据是通过key-value存储的,对于value的数据类型有五种:
- 字符串类型:Map<String,String>
- Hash类型:Map<String,Map<String,String>>。
- List: Map<String,List> 有顺序,可重复。
- Set: Map<String,HashSet> 无顺序,不能重复。
- SortedSet(zset): Map<String,TreeSet> 有顺序,不能重复。
问题扩展 : 可以结合适用场景去对这五种数据类型给面试官进行拓展回答:
- 字符串类型适用场景:自增主键,商品编号,订单编号采用string的递增数字特性生成。
- Hash类型适用场景:存储商品信息,商品字段,定义商品信息的key。
- List类型适用场景:商品评论列表,在Redis中创建商品评论列表。
- SortedSet(zset)类型适用场景:例如根据商品销售量对商品进行排行显示。
结合项目中使用 要清楚Redis在什么场景下适用,和其他几种数据库相比较有哪些异同优劣。
3. Flume事务实现?(今日头条)
一提到事务,首先就想到的是关系型数据库中的事务,事务一个典型的特征就是将一批操作做成原子性的,要么都成功,要么都失败。
在Flume中一共有两个事务,分别是Put事务和Take事务,
- 其中Put事务处于Source到Channel之间,而Take事务处于Channel到Sink之间。
- Put事务时,数据在Flume中会被封装成Event对象,也就是一批Event,把这批Event放到一个事务,然后再把这个事务(一批Event)一次性的放入Channel中。
- 同理,Take事务时,也是把这一批Event组成的事务统一拿出来Sink到HDFS上。
- Put事务:事务开始时,调用doPut方法,doPut方法将一批数据放在putList中;数据顺利的放到putList后,接着调用docommit方法,检查channel内存队列是否有足够空间,若有,则将putList中所有的Event放到Channel中,成功放完之后就清空putList;若空间不够,则调doRollback方法回滚数据。
- Take事务: 事务开始时,调用doTake方法,将Channel数据写入takeList和HDFS缓冲区(若Sink到HDFS,则写入HDFS);若是数据全部发送成功,则清空takeList;若数据发送过程中失败,则清空临时缓冲区,将数据还给Channel。
4. 数据同样存在HDFS,为什么HBase支持在线查询?(头条)
先了解一下,在线查询,即反应根据当前时间的数据,可以认为这些数据始终是在内存的,保证了数据的实时响应。可以认为是从内存中查询,一般响应时间在1秒内。所以可以从HBase的存储机制和底层架构和读取方式这三个方面来分析。
1) HBase的存储机制:
- 首先,HBase的机制是数据先写入到内存中,当数据量达到一定的量, 再写入磁盘中,在内存中,是不进行数据的更新或合并操作的,只增加数据,这使得用户的写操作只要进入内存中就可以立即返回,保证了HBase I/O的高性能。
- 其次,在内存中的数据是有序的,如果内存空间满了,会刷写到HFile中,而在HFile中保存的内容也是有序的。HFile文件为磁盘顺序读取做了优化,按页存储。是顺序写入而不是随机写入,所以速度很稳定,这样保持稳定的同时,加快了速度。
2) HBase底层架构: HBase底层是LSM-Tree+ HTable(Region分区) + Cache——客户端可以直接定位到要查数据所在的HRegion Server服务器,然后直接在服务器的一个Region上查找要匹配的数据,并且这些数据部分是经过Cache缓存的。
3) HBase的读取 读取速度快是因为它使用了LSM树型结构。磁盘的顺序读取速度很快。HBase的存储结构导致它需要磁盘寻道时间在可预测范围内,而关系型数据库,即使有索引,也无法确定磁盘寻道次数。而且,HBase读取首先会在缓存中查找,它采用了LRU(最近最少使用算法),如果缓存中没找到,会从内存中的MemStore中查找,只有这两个地方都找不到时,才会加载HFile中的内容,而我们也提到读取HFile速度会很快,因为节省了寻道开销。
问题扩展:HBase的概念和特点可做扩展回答,再根据特点去回答优劣和核心功能,再延伸到适用场景。
1) HBase概念: HBase是建立在HDFS之上,提供高可靠性的列存储,实时读写的数据库系统。它介于NoSQL和关系型数据库之间,仅通过主键和主键的Range来检索数据,仅支持单行事务。主要用来存储非结构化和半结构化的松散数据。
2) HBase的优缺点:
- 优点:高容错性,高扩展性。 key/value存储方式面对海量数据也不会导致查询性能下降。 相对于传统行式数据库,在单张表字段很多的时候,可以将相同的列存到不同的服务实例上,分散负载压力。
- 缺点:架构设计复杂,且使用HDFS作为分布式存储,所以在存储少量数据时,它也不会很快。 HBase不支持表关联操作,数据分析是HBase的弱项。HBase只部分支持ACID,只支持单行单次操作的事务。
综合项目中使用:清楚HBase和其他数据库的区别,清楚HBase优势劣势,在哪种场景下,使用哪种技术,当所存 储数据需要快速插入查询时,使用HBase,所以Storm或Sparksteaming常常存储入HBase。当数据需要大量聚合运算,计算分析结果时,使用Hive存储,所以Hive是数仓,etl的常用工具。
5. Flume HDFS Sink小文件处理?(顺丰)
1)HDFS存入大量小文件,有什么影响?
- 元数据层面:每个小文件都有一份元数据,其中包括文件路径,文件名,所有者,所属组,权限,创建时间等,这些信息都保存在Namenode内存中。所以小文件过多,会占用Namenode服务器大量内存,影响Namenode性能和使用寿命。
- 计算层面:默认情况下MR会对每个小文件启用一个Map任务计算,非常影响计算性能。同时也影响磁盘寻址时间。
2)HDFS小文件处理
官方默认的这三个参数配置写入HDFS后会产生小文件,hdfs.rollInterval、hdfs.rollSize、 hdfs.rollCount。基于以上hdfs.rollInterval=3600,hdfs.rollSize=134217728,hdfs.rollCount =0,hdfs.roundValue=3600,hdfs.roundUnit= second几个参数综合作用,
效果如下:
- a.tmp文件在达到128M时会滚动生成正式文件,
- b.tmp文件创建超3600秒时会滚动生成正式文件。 举例:在2018-01-01 05:23的时侯sink接收到数据,那会产生如下tmp文件:/atguigu/20180101/atguigu.201801010520.tmp,即使文件内容没有达到128M,也会在06:23时滚动生成正式文件。
问题扩展 :Flume作为开发中的一个重要组件,我们要对它做一个自顶向下的了解,从他的底层架构,到细 节部分Flume的优化,还有一些Flume相关,比如Flume的拦截器,Flume内存等等,这些面试都常涉及。
综合项目使用:Flume HDFS Sink 小文件过多会有很大影响,会非常影响性能,所以在实际生产中为了合理利用资源,我们需要对相关问题有一个清楚地了解。
6. Hive自定义哪些UDF函数?(瓜子二手车)
例如:自定义转换小写函数、自定义解析公共字段函数、自定义测试函数是否创建成功的函数以及自定义将json变成List集合的函数。
/** * 自定义转换小写函数 */
public class MyLowerUDF extends UDF {
public Text evaluate(Text str){
if(null == str.toString()){
return null;
}
return new Text(str.toString().toLowerCase());
}
}
/** * 自定义转换小写函数 */
public class MyLowerUDF extends UDF {
public Text evaluate(Text str){
if(null == str.toString()){
return null;
}
return new Text(str.toString().toLowerCase());
}
}
/** * 自定义udf函数用于解析公共字段 */
public class BaseFieldUDF extends UDF {
public String evaluate(String line, String jsonKeysString) {
StringBuilder result = new StringBuilder();
if(StringUtils.isBlank(line)) {
return null;
}
String[] lineSplit = line.split("\\|");
if(lineSplit.length != 2 || StringUtils.isBlank(lineSplit[1])) {
return null;
} else {
String[] jsonKeys = jsonKeysString.split(",");
try {
JSONObject baseContent = new JSONObject(lineSplit[1]);
JSONObject jsonObject = baseContent.getJSONObject("cm");
for(String jsonKey : jsonKeys) {
if(jsonObject.has(jsonKey.trim())) {
result.append(jsonObject.getString(jsonKey.trim())).append("\t");
} else {
result.append("\t");
}
}
result.append(baseContent.getString("et")).append("\t");
result.append(lineSplit[0]).append("\t");
} catch (JSONException e) {
e.printStackTrace();
}
} return result.toString();
}
}
/*** 自定义测试函数是否创建成功的函数*/
public class MyHiveUDF extends UDF{
public Text evaluate(Text content) {
return evaluate(content, new IntWritable(0));
}
public Text evaluate(Text content, IntWritable flag) {
if (content==null)return null;if(flag.get()==1) {
return new Text(content.toString().toUpperCase());
}else {
return new Text(content.toString().toLowerCase());
}
}
}
/*** 将json变成list集合,方便在hive中使用explode函数*/
public class JsonArray extends UDF {
/**
* 由于hive中的null和java中的null不一样,因此这里将参数类型设置为Object
* hive中的null非String类型,若传入的参数为null,则直接报错,所以设置参数类型为Object
*
* @param jsonStr 可以接收的参数格式 [{},{}]或{"key1":[{},{}],"key2":[{},{}]}
* @return json数组转换后的list集合
*/
public ArrayList<String> evaluate(Object jsonStr) {
//非String类型,返回null
if (!(jsonStr instanceof String)) {
return null;
}
String jsonString = (String) jsonStr;
if (jsonString.startsWith("[")) {
return getJsonList(jsonString);
} else if (jsonString.startsWith("{")) {
return getAllJsonList(jsonString);
} else {
return null;
}
}
// 将一个json数组进行转换操作后,返回一个list集合
// 例如:由"[{},{}]" ---> "list集合"
// list集合在hive中相当于是数组,可以直接使用explode函数
public ArrayList<String> getJsonList(String jsonArrayString) {
if (jsonArrayString.length() == 0 || jsonArrayString == null)
return null;
JSONArray jsonArrayObj = null;
try {
jsonArrayObj = JSON.parseArray(jsonArrayString);
} catch (Exception e) {
return null;
}
ArrayList<String> result = new ArrayList<String>();
for (int i = 0; i < jsonArrayObj.size(); i++) {
result.add(jsonArrayObj.get(i).toString());
}
return result;
}
//将一个json数组{"key":[{},{}],"key":[{},{}]}进行转换操作,返回list集合 [{},{},{}]
public ArrayList<String> getAllJsonList(String jsonString) {
JSONObject jsonObject = null;
try {
jsonObject = JSON.parseObject(jsonString);
} catch (Exception e) {
return null;
}
//获取json对象中所有的key
Set<String> keys = jsonObject.keySet();
ArrayList<Object> jsonObjList = new ArrayList<Object>();
//获取所有的json对象(格式上是一个json数组)
for (String key : keys) {
jsonObjList.add(jsonObject.get(key));
}
ArrayList<String> result = new ArrayList<String>();
//将所有的对象合并到一个list集合中返回
for (Object jsonObj : jsonObjList) {
ArrayList<String> jsonList = getJsonList(jsonObj.toString());
result.addAll(jsonList);
}
return result;
}
}
7. Kafka的分区分配策略?(中信银行)
在Kafka内部存在两种默认的分区分配策略:Range和RoundRobin。
Range是默认策略。 Range是对每个Topic而言的(即一个Topic一个Topic分),首先对同一个Topic里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。然后用Partitions分区的个数除以消费者线程的总数来决定每个消费者线程消费几个分区。如果除不尽,那么前面几个消费者线程将会多消费一个分区。
例如:我们有10个分区,两个消费者(C1,C2),3个消费者线程,10 / 3 = 3而且除不尽。 C1-0 将消费 0, 1, 2, 3 分区 C2-0 将消费 4, 5, 6 分区 C2-1 将消费 7, 8, 9 分区
RoundRobin策略:
- 前提:同一个Consumer Group里面的所有消费者的num.streams(消费者消费线程数)必须相等;每个消费者订阅的主题必须相同。
- 首先将所有主题分区组成TopicAndPartition列表,
- 然后对TopicAndPartition列表按照hashCode进行排序,
- 最后按照轮询的方式发给每一个消费线程。
问题扩展 Kafka相关知识点比较多,这也是一个比较重要的组件,尽量从多个方面去学习,比如Kafka压测,Kafka监控,Kafka副本数设定,它丢不丢数据消息积压消费能力不足怎么办等等。
综合项目使用 : Kafka可以将主题划分为多个分区,会根据分区规则选择把消息存储到哪个分区中,如果分区规则设置的合理,那么所有的消息将会被均匀的分布到不同的分区中,这样就实现了。
8. 谈谈数据倾斜,如何发生的,并给出优化方案?(e代驾)
数据倾斜产生的原因:
- 1)key分布不均匀
- 2)业务数据本身的特性
- 3)建表时考虑不周
- 4)某些SQL语句本身就有数据倾斜
数据倾斜的解决:
1.合理设置Map数
(1)通常情况下,作业会通过Input的目录产生一个或者多个Map任务。
主要的决定因素有:Input的文件总个数,Input的文件大小,集群设置的文件块大小
(2)是不是Map数越多越好?
不是的。如果一个任务有很多小文件(远远小于块大小128M),则每个小文件也会被当做一个块,用一个Map任务来完成,而一个Map任务启动和初始化的时间远远大于逻辑处理的时间,就会造成很大的资源浪费。而且同时可执行的Map数是有限的。
(3)是不是保证每个Map处理接近128M的文件块,就高枕无忧了?
答案也是不一定。比如有一个127M的文件,正常会用一个Map去完成,但这个文件只有一个或者两个小字段,却有几千万的记录,如果Map处理的逻辑比较复杂,用一个Map任务去做,肯定也比较耗时。
针对上面的问题(2)和(3),我们需要采取两种方式来解决:即减少Map数和增加Map数。
2. 小文件进行合并
在Map执行前合并小文件,减少Map数:CombineHiveInputFormat具有对小文件进行合并的功能(系统默认的格式),HiveInputFormat没有对小文件合并功能。 set hive.input.format= org.apache.hadoop.hive.sql.io.CombineHiveInputFormat
3. 复杂文件增加Map数当Input的文件都很大,任务逻辑复杂,Map执行非常慢的时候,可以考虑增加Map数,来使得每个Map处理的数据量减少,从而提高任务的执行效率。
增加Map的方法为:根据 ComputeSliteSize(Math.max(MinSize,Math.Min(maxSize,blocksize)))=Blocksize=128M公式,调整MaxSize最大值。让MaxSize最大值低于Blocksize就可以增加Map的个数。
4. 合理设置Reduce数:(Reduce个数并不是越多越好)
- (1)过多的启动和初始化Reduce也会消耗时间和资源;
- (2)另外,有多少个Reduce,就会有多少个输出文件,如果生成了很多个小文件,那么如果这些小文件作为下一个任务的输入,则也会出现小文件过多的问题;
在设置Reduce个数的时候也需要考虑这两个原则:
- 处理大数据量利用合适的Reduce数;
- 使单个Reduce任务处理数据量大小要合适。
5. 并行执行
Hive会将一个查询转化成一个或者多个阶段,而默认情况下,Hive一次只会执行一个阶段,不过,某个特定的job可能包含众多的阶段,而这些阶段可能并非完全互相依赖的,也就是说有些阶段是可以并行执行的,这样可能使得整个job的执行时间缩短,所以如果有更多的阶段可以并行执行,那么job可能就越快完成。
6. 开启严格模式(开启后会禁止三种类型的查询)
Hive提供了一个严格模式,可以防止用户执行那些可能意想不到的不好的影响的查询,通过设置属hive.mapred.mode值默认是非严格模式Nonstrict。开启严格模式需要修改hive.mapred.mode值为strict。
- (1) 对于分区表,除非WHERE语句中含有分区字段的过滤条件来限制范围,否则不允许执行也就是避免扫描所有分区,因为每个分区都可能拥有非常大数据集;
- (2) 对于使用了ORDER BY语句的查询,要求必须使用Limit语句,因为一旦使用ORDER BY ,所有的结果数据会分发到同一个Reducer中进行处理,从而会使Reducer额外执行一段很长的时间;
- (3) 限制笛卡尔积的查询。
7. JVM重用
由于Hadoopde默认配置通常是使用派生JVM来执行Map和Reduce任务,因此JVM的启动过程可能会造成相当大的开销,尤其是执行的job包含有成百上千task任务的情况。JVM重用可以使得JVM实例在同一个job中重新使用N次,而N的值需要根据具体业务场景测试得出。 JVM重用的缺点:开启JVM重用将一直占用使用到的task插槽,以便进行重用,直到任务完成后才能释放,一旦由于某个job执行时间过长的话,那么保留的插槽就会一直空闲着却无法被其他job使用,直到所有的task都结束了才会释放。
8. 推测执行
根据一定的法则推测出“拖后腿”的任务,并为这样的任务启动一个备份任务,让该任务与原始任务同时处理同一份数据,并最终选用最先成功运行完成任务的计算结果作为最终结果。
9. Hadoop优化时经常修改的配置文件和其中配置项? (搜狐)
- 1)在hdfs-site.xml文件中配置多目录,最好提前配置好,否则更改目录需要重新启动集群;
- 2)NameNode有一个工作线程池,用来处理不同DataNode的并发心跳以及客户端并发的元数据操作。dfs.namenode.handler.count=20 * log2(Cluster Size),比如集群规模为10台时,此参数设置为60。
- 3)编辑日志存储路径dfs.namenode.edits.dir设置与镜像文件存储路径dfs.namenode.name.dir尽量分开,达到最低写入延迟。
- 4)服务器节点上YARN可使用的物理内存总量,默认是8192(MB),注意,如果你的节点内存资源不够8GB,则需要调减小这个值,而YARN不会智能的探测节点的物理内存总量。yarn.nodemanager.resource.memory-mb
- 5)单个任务可申请的最多物理内存量,默认是8192(MB)。yarn.scheduler.maximum-allocation-mb
问题扩展 Hadoop作为大数据很重要的一部分,自然也是面试中比较常见的问题,他的三大组件要做到了然于胸,还有一些小的知识点,比如Hadoop宕机怎么处理,Hadoop常用端口号等等这些都要在平时多做积累。
综合项目使用当发现作业运行效率不理想时,需要对作业执行进行性能监测,以及对作业本身、集群平台进行优化。优化后的集群可能最大化利用硬件资源,从而提高作业的执行效率。这些在Hadoop集群平台搭建以及作业运行过程中一些常用优化手段需要多做了解。
10. MySQL的索引如何理解?常用引擎是什么?有什么区别?比较Redis和MySQL的区别?说一下各自的持久化机制? (搜狗金融)
1)MySQL索引的理解:
索引(Index)是帮助MySQL高效获取数据的数据结构。我们可以简单理解为:快速查找排好序的一种数据结构。Mysql索引主要有两种结构:B+Tree索引和Hash索引。 索引虽然能非常高效的提高查询速度,同时却会降低更新表的速度。实际上索引也是一张表,该表保存了主键与索引字段,并指向实体表的记录,所以索引列也是要占用空间的。所以要看实际情况是否建立索引。
2)常用引擎是什么:
a. 常用引擎有三种:InnoBD,Myisam,Memory
b.区别:
- InnoDB:支持事务,支持外键,支持行锁,写入数据时操作快。
- Myisam:不支持事务。不支持外键,支持表锁,支持全文索引,读取数据快。
- Memory:所有的数据都保留在内存中,不需要进行磁盘的IO所以读取的速度很快,一旦关机的话表的结构会保留,但是数据会丢失,表支持Hash索引,因此查找速度很快。
3)Redis和MySQL的区别:
a. MySQL和Redis的数据库类型 MySQL是关系型数据库,主要用于存放持久化数据,将数据存储在硬盘中,读取速度较慢。Redis是NOSQL,即非关系型数据库,也是缓存数据库,即将数据存储在缓存中,缓存的读取速度快,能够大大的提高运行效率,但是保存时间有限;
b. MySQL的运行机制 MySQL作为持久化存储的关系型数据库,相对薄弱的地方在于每次请求访问数据库时,都存在着I/O操作,如果反复频繁的访问数据库。第一:会在反复链接数据库上花费大量时间,从而导致运行效率过慢;第二:反复的访问数据库也会导致数据库的负载过高,那么此时缓存的概念就衍生了出来。
c. 缓存缓存就是数据交换的缓冲区(Cache),当浏览器执行请求时,首先会对在缓存中进行查找,如果存在,就获取;否则就访问数据库。缓存的好处就是读取速度快。
d. Redis数据库Redis数据库就是一款缓存数据库,用于存储使用频繁的数据,这样减少访问数据库的次数,提高运行效率。
e. Redis和MySQL的区别总结
- (1)类型上从类型上来说,MySQL是关系型数据库,Redis是缓存数据库。
- (2)作用上MySQL用于持久化的存储数据到硬盘,功能强大,但是速度较慢。Redis用于存储使用较为频繁的数据到缓存中,读取速度快。
- (3)需求上MySQL和Redis因为需求的不同,一般都是配合使用。
- (4)Redis和MySQL各自的持久化机制:
a. Redis的持久化机制:Redis提供两种持久化方式,RDB和AOF;AOF可以完整的记录整个数据库,在AOF模式下,Redis会把执行过的每一条更新命令记录下来,保存到AOF文件中;而RDB记录的只是数据库某一时刻的快照。 两种区别是:一个是持续的用日志记录写操作,Crash后利用日志恢复;一个是平时写操作的时候不触发写,只有手动提交save命令,或关闭命令的时候,才触发备份操作。
b. MySQL的持久化机制:InnoDB引擎会引入redo日志作为中间层来保证MySQL的持久化,当有一条记录更新的时候,InnoDB引擎就会先把记录写到redolog中,并更新内存。同时InnoDB引擎会在适当的时候,将这个操作记录更新到磁盘里面。
问题扩展 MySQL索引存在的好处,以及和Redis各自适用的场景。