标签: redis

Redis缓存穿透、缓存雪崩、redis并发问题分析

把redis作为缓存使用已经是司空见惯,但是使用redis后也可能会碰到一系列的问题,尤其是数据量很大的时候,经典的几个问题如下:

(一)缓存和数据库间数据一致性问题

分布式环境下(单机就不用说了)非常容易出现缓存和数据库间的数据一致性问题,针对这一点的话,只能说,如果你的项目对缓存的要求是强一致性的,那么请不要使用缓存。我们只能采取合适的策略来降低缓存和数据库间数据不一致的概率,而无法保证两者间的强一致性。合适的策略包括 合适的缓存更新策略,更新数据库后要及时更新缓存、缓存失败时增加重试机制,例如MQ模式的消息队列。

(二)缓存击穿问题

缓存击穿表示恶意用户模拟请求很多缓存中不存在的数据,由于缓存中都没有,导致这些请求短时间内直接落在了数据库上,导致数据库异常。这个我们在实际项目就遇到了,有些抢购活动、秒杀活动的接口API被大量的恶意用户刷,导致短时间内数据库宕机了,好在数据库是多主多从的,hold住了。

解决方案的话:

1、使用互斥锁排队

业界比价普遍的一种做法,即根据key获取value值为空时,锁上,从数据库中load数据后再释放锁。若其它线程获取锁失败,则等待一段时间后重试。这里要注意,分布式环境中要使用分布式锁,单机的话用普通的锁(synchronized、Lock)就够了。

public String getWithLock(String key, Jedis jedis, String lockKey, String uniqueId, long expireTime) {
 // 通过key获取value
 String value = redisService.get(key);
 if (StringUtil.isEmpty(value)) {
 // 分布式锁,详细可以参考https://blog.csdn.net/fanrenxiang/article/details/79803037
 //封装的tryDistributedLock包括setnx和expire两个功能,在低版本的redis中不支持
 try {
 boolean locked = redisService.tryDistributedLock(jedis, lockKey, uniqueId, expireTime);
 if (locked) {
 value = userService.getById(key);
 redisService.set(key, value);
 redisService.del(lockKey);
 return value;
 } else {
 // 其它线程进来了没获取到锁便等待50ms后重试
 Thread.sleep(50);
 getWithLock(key, jedis, lockKey, uniqueId, expireTime);
 }
 } catch (Exception e) {
 log.error("getWithLock exception=" + e);
 return value;
 } finally {
 redisService.releaseDistributedLock(jedis, lockKey, uniqueId);
 }
 }
 return value;
}

这样做思路比较清晰,也从一定程度上减轻数据库压力,但是锁机制使得逻辑的复杂度增加,吞吐量也降低了,有点治标不治本。

2、布隆过滤器(推荐)

bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小,下面先来简单的实现下看看效果,我这里用guava实现的布隆过滤器:

<dependencies> 
 <dependency> 
 <groupId>com.google.guava</groupId> 
 <artifactId>guava</artifactId> 
 <version>23.0</version> 
 </dependency> 
</dependencies> 
public class BloomFilterTest {
 
 private static final int capacity = 1000000;
 private static final int key = 999998;
 
 private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity);
 
 static {
 for (int i = 0; i < capacity; i++) {
 bloomFilter.put(i);
 }
 }
 
 public static void main(String[] args) {
 /*返回计算机*精确的时间,单位微妙*/
 long start = System.nanoTime();
 
 if (bloomFilter.mightContain(key)) {
 System.out.println("成功过滤到" + key);
 }
 long end = System.nanoTime();
 System.out.println("布隆过滤器消耗时间:" + (end - start));
 int sum = 0;
 for (int i = capacity + 20000; i < capacity + 30000; i++) {
 if (bloomFilter.mightContain(i)) {
 sum = sum + 1;
 }
 }
 System.out.println("错判率为:" + sum);
 }
}
成功过滤到999998
布隆过滤器消耗时间:215518
错判率为:318

可以看到,100w个数据中只消耗了约0.2毫秒就匹配到了key,速度足够快。然后模拟了1w个不存在于布隆过滤器中的key,匹配错误率为318/10000,也就是说,出错率大概为3%,跟踪下BloomFilter的源码发现默认的容错率就是0.03:

public static <T> BloomFilter<T> create(Funnel<T> funnel, int expectedInsertions /* n */) {
 return create(funnel, expectedInsertions, 0.03); // FYI, for 3%, we always get 5 hash functions
}

我们可调用BloomFilter的这个方法显式的指定误判率:

Redis缓存穿透、缓存雪崩、redis并发问题分析

 

private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity,0.01);

我们断点跟踪下,误判率为0.02和默认的0.03时候的区别:

Redis缓存穿透、缓存雪崩、redis并发问题分析

 

Redis缓存穿透、缓存雪崩、redis并发问题分析

 

 

对比两个出错率可以发现,误判率为0.02时数组大小为8142363,0.03时为7298440,误判率降低了0.01,BloomFilter维护的数组大小也减少了843923,可见BloomFilter默认的误判率0.03是设计者权衡系统性能后得出的值。要注意的是,布隆过滤器不支持删除操作。用在这边解决缓存穿透问题就是:

public String getByKey(String key) {
 // 通过key获取value
 String value = redisService.get(key);
 if (StringUtil.isEmpty(value)) {
 if (bloomFilter.mightContain(key)) {
 value = userService.getById(key);
 redisService.set(key, value);
 return value;
 } else {
 return null;
 }
 }
 return value;
}

(三)缓存雪崩问题

缓存在同一时间内大量键过期(失效),接着来的一大波请求瞬间都落在了数据库中导致连接异常。

解决方案:

1、也是像解决缓存穿透一样加锁排队,实现同上;

2、建立备份缓存,缓存A和缓存B,A设置超时时间,B不设值超时时间,先从A读缓存,A没有读B,并且更新A缓存和B缓存;

public String getByKey(String keyA,String keyB) {
 String value = redisService.get(keyA);
 if (StringUtil.isEmpty(value)) {
 value = redisService.get(keyB);
 String newValue = getFromDbById();
 redisService.set(keyA,newValue,31, TimeUnit.DAYS);
 redisService.set(keyB,newValue);
 }
 return value;
}

(四)缓存并发问题

这里的并发指的是多个redis的client同时set key引起的并发问题。比较有效的解决方案就是把redis.set操作放在队列中使其串行化,必须的一个一个执行,具体的代码就不上了,当然加锁也是可以的,至于为什么不用redis中的事务,留给各位看官自己思考探究。

Redis 客户端使用与监控

转载自:https://my.oschina.net/heweipo/blog/1536679

1、客户端通信协议

1)客户端与服务端之间的同学协议是在TCP协议之上构建的;

2)Redis定制了RESP(Redis Serialization Protocol ,Redis 序列化协议)实现客户端与服务端的正常交互。正因为这种协议简单而又容易理解,所以很多编程语言的客户端就容易实现了,比如 Java的客户端 Jedis.

 

2、客户端Jedis的使用

生产环境一般我们不会使用直连的方式(因为连接没法管理,导致资源不可控),而是使用连接池,使用 Jedis 连接池时主要注意一下几个方面。

1)maxActive:*大连接数,默认8个,生产环境一般配置16个差不多,(主要是Jedis处理高效)

2)maxIdle:池子里的*大空闲连接数,默认8个,意思是没有客户端来连接时池里*多闲置 8 个

3)minIdle:池子里的*小空闲连接数,默认0,意思是没有客户端来连接时池里*少闲置数

4)jmxEnabled:是否开启jmx监控,默认为 true,可以通过 jconsole 等工具监控连接池

5)minEvictableIdleTimeMillis:连接*小空闲时间,默认30分钟,达到这个值后空闲连接被移除

6)numTestsPerEvictionRun:做空闲连接检测时每次采样数,默认 3

7)TestOnBorrow:向连接池借用连接时是否做连接的有效性检测(Ping),默认false,如果设置为true,那么每次借用时都会多一次 ping,不过增强了代码的健壮性,其实如果是在Redis服务端配置了 timeout ,那么这个操作我觉得是很有必要的,因为服务端会把你这边的连接给移除。

8)testOnReturn:同上,不过这里是归还时,默认为false。

9)testWhileIdle:对连接池的连接时是否做空闲检测,默认为 false

10)timeBetweenEvicationRunsMillis:空闲连接检测周期,单位为毫秒,默认 -1 ,不做检测

11)blockWhenExhausted:当连接池用尽后,新的调用者是否要等待空闲连接,这个参数和下面的maxWaitMillis 对应的,只有当 blockWhenExhausted = true 时 maxWaitMillis 才会生效。默认为 true。

12)maxWaitMillis:当连接池资源用尽后,新的调用者来等待空闲连接时的*大时间,单位毫秒,默认 -1 ,表示永不超时,这个很要命的,一定要改掉。

 

3、客户端管理

1)查看客户端连接情况

>info clients

# Clients

connected_clients:2 // 当前连接数,这个暴增,说明连接池没有配置得当

client_longest_output_list:0 // 输出缓冲区列表*大对象数,其实就是*大 oll

client_biggest_input_buf:0 // *大输入缓冲区,单位字节,其实就是*大 qbuf

blocked_clients:0 // 正在阻塞阻塞命令(比如 blpop brpop 等)的客户端个数

 

2)查看服务端连接汇总

> info stats

# Stats

total_connections_received:6 // 从启动到现在连接的总数(并不是当前连接数)

rejected_connections:0 // 从启动到现在拒*的连接数

 

3)查看客户端连接的详细信息

>client list

id=5 addr=127.0.0.1:52821 fd=8 name= age=115 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client

 

id=6 addr=127.0.0.1:52822 fd=9 name= age=91 idle=10 flags=O db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=monitor

 

id:唯一标识

name:客户端名称,通过 client setName client1 和 client getName 来设置获取名称

addr : 客户端的地址和端口,当想停止该客户端时则可以通过这个值 kill 掉

fd : 套接字所使用的文件描述符,Linux 里的 socket 句柄

age : 以秒计算的已连接时长,表示 client 从连接到现在的时长,所以叫做年龄

idle : 以秒计算的空闲时长,表示当前距离上一次执行命令的空闲时长

flags : 客户端 flag,标识客户端的信息,比如主从服务,事务执行情况等

db : 该客户端正在使用的数据库 ID

sub : 已订阅频道的数量

psub : 已订阅模式的数量

multi : 在事务中被执行的命令数量,如果没有使用事务就是 -1,记住是命令个数,包括读写

qbuf : 查询缓冲区的长度(字节为单位, 0 表示没有分配查询缓冲区)

qbuf-free : 查询缓冲区剩余空间的长度(字节为单位, 0 表示没有剩余空间)

obl : 输出缓冲区的长度(字节为单位, 0 表示没有分配输出缓冲区)

oll : 输出列表包含的对象数量(当输出缓冲区没有剩余空间时,命令回复会以字符串对象的形式被入队到这个队列里)

omem : 输出缓冲区和输出列表占用的内存总量

events : 文件描述符事件,w 可写, r 可读

cmd : *近一次执行的命令

 

注意:

A:输入缓冲区 qbuf 和 qbuf-free 以及 client_biggest_input_buf

Redis为每个客户端分配了输入缓冲区,它的作用是将客户端发送的命令临时保存,同时Redis会从输入缓冲区拉取命令并执行。其中 qbuf 代表这个缓冲区的总容量,qbuf-free 代表剩余容量。

Redis并没有提供相应的配置来规定每个缓冲区的大小,输入缓冲区会根据输入的内容大小的不同动态调整,只是要求每个客户端缓冲区的大小不能超过1G,超过后客户端将被关闭;另外如果没有达到 1 G,但是和当前Redis的空间加起来已经超过 MaxMemory 时,也可能出现 OOM,这个时候命令可能会被丢失。

其实生产环境很少有因为输入缓冲区导致的阻塞,主要还是因为命令过多积累在执行队列中,因此我们可以通过 qbuf 预测出这个连接有多少命令被发送,如果真的 qbuf 非常大,那么命令太多,可能会导致阻塞,比如管道、事务命令等。当然也有可能服务端发生阻塞导致客户端的命令一直被积压,也有可能客户端执行了大对象。

 

B:输出缓冲区 obl 和 oll 以及 client_longest_output_list

Redis为每个客户端分配了输出缓冲区,它的作用是保存命令执行的结果返回给客户端,其实就是为命令返回结果提供临时缓冲区。

与输入缓冲区不同的是,输出缓冲区的大小可以通过 client-output-buffer-limit 来进行设置。

输出缓冲区有两部分组成,固定缓冲区(obl)和动态缓冲区(oll),固定缓冲区只有 16KB,也就是返回值大于 16KB时,就会动用固定缓冲区,两种计算方式不一样哦,固定缓冲区是用字节单位,动态缓冲区使用 对象个数。另外 omem 是指动态缓冲区+固定缓冲区的总字节数。

生产环境遇到的问题在输出缓冲区还是比较多的,比如大对象的获取,就会造成 oll 占用过大,从而导致阻塞,所以*好是限制住 client-output-buffer-limit ,然后报警提示。

 

 

4)设置*大连接数 maxClients

> config get maxClients

1) “maxclients”

2) “10000”

> config set maxClients 500

OK

// *大连接数 10000,然而 Jedis的连接数默认是 8 ,对比一下差距好大啊,不知道这里如何设置的,是不是考虑到集群等因素。

 

5)设置检测客户空闲时间

> config get timeout

1) “timeout”

2) “30”

> config get timeout

1) “timeout”

2) “45”

// 如果client的idle得到了45,那么这个连接就会被kill掉,所以使用连接池时*好对连接进行可用性检测,同时也要避免客户端在一个连接内消耗大量的时间。

 

6)client setName | getName

> client setName client1

OK

> client getName

“client1”

// 给客户端指定一个自定义名字,方便查找。 我们可以在 client list 找到。

或者执行 > redis-cli client list | grep client1

 

7)client kill addr

>client kill 127.0.0.1:52875

>OK

 

8)client pause timeout(毫秒)

>client pause 1000

// 阻塞客户端 1000 毫秒,要知道Redis是单线程的,如果这个客户端阻塞,那么其他客户端全部都要等待他执行完成,所以一切客户端导致的延迟阻塞,都会是服务端的延迟阻塞。

 

 

9)monitor 监控其他客户端的命令执行

> monitor

OK

1503724607.774500 [0 127.0.0.1:52880] “get” “str”

1503724610.339557 [0 127.0.0.1:52880] “set” “str” “hll”

// 在寻找问题时这个命令很管用,但是高并发下,这个命令很致命的,因为大量输出导致该客户端的输出缓冲区被占满,从阻塞输出。所以用过后尽早关闭。

*个是执行命令的时间戳,可以使用 new Date(1503724607.774*1000)

第二个时数据库索引 0

第三个是执行的命令以及命令的参数 “get” “str”

 

 

10)客户端的其他配置

1)timeout :指Redis的连接如果空闲时间超过timeout则断开,可以通过 client list 查看连接

2)maxClients:这个和下面的 tcp 连接数不是一码事,这个指的是客户端数。

3)tcp-keepalive:检测TCP连接的活性周期,默认为 0 ,不检测。这个*好是配置一个 60 秒,因为大量的死连接会占用服务端的TCP资源。

4)tcp-backlog:TCP 三次握手成功后,会把这个连接放入队列中,这个队列的*大长度就是 tcp-backlog 的配置,默认是 511 , 通常这个参数不用调整。但是有些Linux系统的这个值配置时 128,则需要对应调整为 511.

如下查看和设置

> cat /proc/sys/net/core/somaxconn

128

> echo 511 > /proc/sys/net/core/somaxconn

> cat /proc/sys/net/core/somaxconn

511

Grafana+Prometheus系统监控之Redis

介绍

REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统。

Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。

应用

目前平台Redis具体使用到以下三个方面:

  • 数据缓存,大并发下用于快读读取数据
  • 消息队列,主要用于日志队列传输、异步任务
  • session存储,主要用于平台各个项目的用户信息存储,适用于负载均衡集群环境

监控

目前平台使用Redis还是比较广了,下面与大家分享一款Redis监控实现,将会使用到以下组间grafana、prometheus以及redis_exporter。

由于grafana和prometheus之前安装配置过:Linux下打造全方位立体监控系统

下面主要说一下redis_exporter的安装配置。

下载:

wget https://github.com/oliver006/redis_exporter/releases/download/v0.13/redis_exporter-v0.13.linux-amd64.tar.gz

解压:

tar -xvf  redis_exporter-v0.13.linux-amd64.tar.gz

下载grafana的redis的prometheus-redis_rev1.json模板:

wget  https://grafana.com/api/dashboards/763/revisions/1/download

在grafana中导入json模板:

1

启动redis_exporter:

  1. ## 无密码
  2. ./redis_exporter redis//192.168.1.120:6379 &
  3. ## 有密码
  4. redis_exporter -redis.addr 192.168.1.120:6379 -redis.password 123456

prometheus.yml加入redis节点,然后重启prometheus:

  1. job_name: redis
  2. static_configs:
  3. targets: [‘192.168.1.120:9121’]
  4. labels:
  5. instance: redis120

配置成功UP状态

2

访问grafana,*终呈现:

3

参考文档:
https://github.com/oliver006/redis_exporter

作者: 小柒

出处: https://blog.52itstyle.com

分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。

Redis常见延迟问题排查手册!附33条优化建议

Redis作为内存数据库,拥有非常高的性能,单个实例的QPS能够达到10W左右。但我们在使用Redis时,经常时不时会出现访问延迟很大的情况,如果你不知道Redis的内部实现原理,在排查问题时就会一头雾水。

 

很多时候,Redis出现访问延迟变大,都与我们的使用不当或运维不合理导致的。

 

Redis变慢了?常见延迟问题定位与分析

 

下面我们就来分析一下Redis在使用过程中,经常会遇到的延迟问题以及如何定位和分析。

 

使用复杂度高的命令

 

 

如果在使用Redis时,发现访问延迟突然增大,如何进行排查?

 

首先,*步,建议你去查看一下Redis的慢日志。Redis提供了慢日志命令的统计功能,我们通过以下设置,就可以查看有哪些命令在执行时延迟比较大。

 

首先设置Redis的慢日志阈值,只有超过阈值的命令才会被记录,这里的单位是微妙,例如设置慢日志的阈值为5毫秒,同时设置只保留*近1000条慢日志记录:

 

 

# 命令执行超过5毫秒记录慢日志

CONFIG SET slowlog-log-slower-than 5000

# 只保留*近1000条慢日志

CONFIG SET slowlog-max-len 1000

 

设置完成之后,所有执行的命令如果延迟大于5毫秒,都会被Redis记录下来,我们执行SLOWLOG get 5查询*近5条慢日志:

 

 

127.0.0.1:6379> SLOWLOG get 5

1) 1) (integer) 32693       # 慢日志ID

2) (integer) 1593763337  # 执行时间

3) (integer) 5299        # 执行耗时(微妙)

4) 1) “LRANGE”           # 具体执行的命令和参数

2) “user_list_2000”

3) “0”

4) “-1”

2) 1) (integer) 32692

2) (integer) 1593763337

3) (integer) 5044

4) 1) “GET”

2) “book_price_1000”

 

通过查看慢日志记录,我们就可以知道在什么时间执行哪些命令比较耗时,如果你的业务经常使用O(n)以上复杂度的命令,例如sort、sunion、zunionstore,或者在执行O(n)命令时操作的数据量比较大,这些情况下Redis处理数据时就会很耗时。

 

如果你的服务请求量并不大,但Redis实例的CPU使用率很高,很有可能是使用了复杂度高的命令导致的。

 

解决方案就是,不使用这些复杂度较高的命令,并且一次不要获取太多的数据,每次尽量操作少量的数据,让Redis可以及时处理返回。

 

存储大key

 

 

如果查询慢日志发现,并不是复杂度较高的命令导致的,例如都是SET、DELETE操作出现在慢日志记录中,那么你就要怀疑是否存在Redis写入了大key的情况。

 

Redis在写入数据时,需要为新的数据分配内存,当从Redis中删除数据时,它会释放对应的内存空间。

 

如果一个key写入的数据非常大,Redis在分配内存时也会比较耗时。同样的,当删除这个key的数据时,释放内存也会耗时比较久。

 

你需要检查你的业务代码,是否存在写入大key的情况,需要评估写入数据量的大小,业务层应该避免一个key存入过大的数据量。

 

那么有没有什么办法可以扫描现在Redis中是否存在大key的数据吗?

 

Redis也提供了扫描大key的方法:

 

 

redis-cli -h $host -p $port –bigkeys -i 0.01

 

使用上面的命令就可以扫描出整个实例key大小的分布情况,它是以类型维度来展示的。

 

需要注意的是当我们在线上实例进行大key扫描时,Redis的QPS会突增,为了降低扫描过程中对Redis的影响,我们需要控制扫描的频率,使用-i参数控制即可,它表示扫描过程中每次扫描的时间间隔,单位是秒。

 

使用这个命令的原理,其实就是Redis在内部执行scan命令,遍历所有key,然后针对不同类型的key执行strlen、llen、hlen、scard、zcard来获取字符串的长度以及容器类型(list/dict/set/zset)的元素个数。

 

而对于容器类型的key,只能扫描出元素*多的key,但元素*多的key不一定占用内存*多,这一点需要我们注意下。不过使用这个命令一般我们是可以对整个实例中key的分布情况有比较清晰的了解。

 

针对大key的问题,Redis官方在4.0版本推出了lazy-free的机制,用于异步释放大key的内存,降低对Redis性能的影响。即使这样,我们也不建议使用大key,大key在集群的迁移过程中,也会影响到迁移的性能,这个后面在介绍集群相关的文章时,会再详细介绍到。

 

集中过期

 

 

有时你会发现,平时在使用Redis时没有延时比较大的情况,但在某个时间点突然出现一波延时,而且报慢的时间点很有规律,例如某个整点,或者间隔多久就会发生一次。

 

如果出现这种情况,就需要考虑是否存在大量key集中过期的情况。

 

如果有大量的key在某个固定时间点集中过期,在这个时间点访问Redis时,就有可能导致延迟增加。

 

Redis的过期策略采用主动过期+懒惰过期两种策略:

 

  • 主动过期:Redis内部维护一个定时任务,默认每隔100毫秒会从过期字典中随机取出20个key,删除过期的key,如果过期key的比例超过了25%,则继续获取20个key,删除过期的key,循环往复,直到过期key的比例下降到25%或者这次任务的执行耗时超过了25毫秒,才会退出循环;
  • 懒惰过期:只有当访问某个key时,才判断这个key是否已过期,如果已经过期,则从实例中删除。

 

注意,Redis的主动过期的定时任务,也是在Redis主线程中执行的,也就是说如果在执行主动过期的过程中,出现了需要大量删除过期key的情况,那么在业务访问时,必须等这个过期任务执行结束,才可以处理业务请求。此时就会出现,业务访问延时增大的问题,*大延迟为25毫秒。

 

而且这个访问延迟的情况,不会记录在慢日志里。慢日志中只记录真正执行某个命令的耗时,Redis主动过期策略执行在操作命令之前,如果操作命令耗时达不到慢日志阈值,它是不会计算在慢日志统计中的,但我们的业务却感到了延迟增大。

 

此时你需要检查你的业务,是否真的存在集中过期的代码,一般集中过期使用的命令是expireat或pexpireat命令,在代码中搜索这个关键字就可以了。

 

如果你的业务确实需要集中过期掉某些key,又不想导致Redis发生抖动,有什么优化方案?

 

解决方案是,在集中过期时增加一个随机时间,把这些需要过期的key的时间打散即可。

 

伪代码可以这么写:

 

 

# 在过期时间点之后的5分钟内随机过期掉

redis.expireat(key, expire_time + random(300))

 

这样Redis在处理过期时,不会因为集中删除key导致压力过大,阻塞主线程。

 

另外,除了业务使用需要注意此问题之外,还可以通过运维手段来及时发现这种情况。

 

做法是我们需要把Redis的各项运行数据监控起来,执行info可以拿到所有的运行数据,在这里我们需要重点关注expired_keys这一项,它代表整个实例到目前为止,累计删除过期key的数量。

 

我们需要对这个指标监控,当在很短时间内这个指标出现突增时,需要及时报警出来,然后与业务报慢的时间点对比分析,确认时间是否一致,如果一致,则可以认为确实是因为这个原因导致的延迟增大。

 

实例内存达到上限

 

 

有时我们把Redis当做纯缓存使用,就会给实例设置一个内存上限maxmemory,然后开启LRU淘汰策略。

 

当实例的内存达到了maxmemory后,你会发现之后的每次写入新的数据,有可能变慢了。

 

导致变慢的原因是,当Redis内存达到maxmemory后,每次写入新的数据之前,必须先踢出一部分数据,让内存维持在maxmemory之下。

 

这个踢出旧数据的逻辑也是需要消耗时间的,而具体耗时的长短,要取决于配置的淘汰策略:

 

  • allkeys-lru:不管key是否设置了过期,淘汰*近*少访问的key;
  • volatile-lru:只淘汰*近*少访问并设置过期的key;
  • allkeys-random:不管key是否设置了过期,随机淘汰;
  • volatile-random:只随机淘汰有设置过期的key;
  • allkeys-ttl:不管key是否设置了过期,淘汰即将过期的key;
  • noeviction:不淘汰任何key,满容后再写入直接报错;
  • allkeys-lfu:不管key是否设置了过期,淘汰访问频率*低的key(4.0+支持);
  • volatile-lfu:只淘汰访问频率*低的过期key(4.0+支持)。

 

具体使用哪种策略,需要根据业务场景来决定。

 

我们*常使用的一般是allkeys-lru或volatile-lru策略,它们的处理逻辑是,每次从实例中随机取出一批key(可配置),然后淘汰一个*少访问的key,之后把剩下的key暂存到一个池子中,继续随机取出一批key,并与之前池子中的key比较,再淘汰一个*少访问的key。以此循环,直到内存降到maxmemory之下。

 

如果使用的是allkeys-random或volatile-random策略,那么就会快很多,因为是随机淘汰,那么就少了比较key访问频率时间的消耗了,随机拿出一批key后直接淘汰即可,因此这个策略要比上面的LRU策略执行快一些。

 

但以上这些逻辑都是在访问Redis时,真正命令执行之前执行的,也就是它会影响我们访问Redis时执行的命令。

 

另外,如果此时Redis实例中有存储大key,那么在淘汰大key释放内存时,这个耗时会更加久,延迟更大,这需要我们格外注意。

 

如果你的业务访问量非常大,并且必须设置maxmemory限制实例的内存上限,同时面临淘汰key导致延迟增大的的情况,要想缓解这种情况,除了上面说的避免存储大key、使用随机淘汰策略之外,也可以考虑拆分实例的方法来缓解,拆分实例可以把一个实例淘汰key的压力分摊到多个实例上,可以在一定程度降低延迟。

 

fork耗时严重

 

 

如果你的Redis开启了自动生成RDB和AOF重写功能,那么有可能在后台生成RDB和AOF重写时导致Redis的访问延迟增大,而等这些任务执行完毕后,延迟情况消失。

 

遇到这种情况,一般就是执行生成RDB和AOF重写任务导致的。

 

生成RDB和AOF都需要父进程fork出一个子进程进行数据的持久化,在fork执行过程中,父进程需要拷贝内存页表给子进程,如果整个实例内存占用很大,那么需要拷贝的内存页表会比较耗时,此过程会消耗大量的CPU资源,在完成fork之前,整个实例会被阻塞住,无法处理任何请求,如果此时CPU资源紧张,那么fork的时间会更长,甚至达到秒级。这会严重影响Redis的性能。

 

具体原理也可以参考我之前写的文章:Redis持久化是如何做的?RDB和AOF对比分析。

 

我们可以执行info命令,查看*后一次fork执行的耗时latest_fork_usec,单位微妙。这个时间就是整个实例阻塞无法处理请求的时间。

 

除了因为备份的原因生成RDB之外,在主从节点*次建立数据同步时,主节点也会生成RDB文件给从节点进行一次全量同步,这时也会对Redis产生性能影响。

 

要想避免这种情况,我们需要规划好数据备份的周期,建议在从节点上执行备份,而且*好放在低峰期执行。如果对于丢失数据不敏感的业务,那么不建议开启AOF和AOF重写功能。

 

另外,fork的耗时也与系统有关,如果把Redis部署在虚拟机上,那么这个时间也会增大。所以使用Redis时建议部署在物理机上,降低fork的影响。

 

绑定CPU

 

 

很多时候,我们在部署服务时,为了提高性能,降低程序在使用多个CPU时上下文切换的性能损耗,一般会采用进程绑定CPU的操作。

 

但在使用Redis时,我们不建议这么干,原因如下。

 

绑定CPU的Redis,在进行数据持久化时,fork出的子进程,子进程会继承父进程的CPU使用偏好,而此时子进程会消耗大量的CPU资源进行数据持久化,子进程会与主进程发生CPU争抢,这也会导致主进程的CPU资源不足访问延迟增大。

 

所以在部署Redis进程时,如果需要开启RDB和AOF重写机制,一定不能进行CPU绑定操作!

 

开启AOF

 

 

上面提到了,当执行AOF文件重写时会因为fork执行耗时导致Redis延迟增大,除了这个之外,如果开启AOF机制,设置的策略不合理,也会导致性能问题。

 

开启AOF后,Redis会把写入的命令实时写入到文件中,但写入文件的过程是先写入内存,等内存中的数据超过一定阈值或达到一定时间后,内存中的内容才会被真正写入到磁盘中。

 

AOF为了保证文件写入磁盘的安全性,提供了3种刷盘机制:

 

  • appendfsync always:每次写入都刷盘,对性能影响*大,占用磁盘IO比较高,数据安全性*高;
  • appendfsync everysec:1秒刷一次盘,对性能影响相对较小,节点宕机时*多丢失1秒的数据;
  • appendfsync no:按照操作系统的机制刷盘,对性能影响*小,数据安全性低,节点宕机丢失数据取决于操作系统刷盘机制。

 

当使用*种机制appendfsync always时,Redis每处理一次写命令,都会把这个命令写入磁盘,而且这个操作是在主线程中执行的。

 

内存中的的数据写入磁盘,这个会加重磁盘的IO负担,操作磁盘成本要比操作内存的代价大得多。如果写入量很大,那么每次更新都会写入磁盘,此时机器的磁盘IO就会非常高,拖慢Redis的性能,因此我们不建议使用这种机制。

 

与*种机制对比,appendfsync everysec会每隔1秒刷盘,而appendfsync no取决于操作系统的刷盘时间,安全性不高。因此我们推荐使用appendfsync everysec这种方式,在*坏的情况下,只会丢失1秒的数据,但它能保持较好的访问性能。

 

当然,对于有些业务场景,对丢失数据并不敏感,也可以不开启AOF。

 

使用Swap

 

 

如果你发现Redis突然变得非常慢,每次访问的耗时都达到了几百毫秒甚至秒级,那此时就检查Redis是否使用到了Swap,这种情况下Redis基本上已经无法提供高性能的服务。

 

我们知道,操作系统提供了Swap机制,目的是为了当内存不足时,可以把一部分内存中的数据换到磁盘上,以达到对内存使用的缓冲。

 

但当内存中的数据被换到磁盘上后,访问这些数据就需要从磁盘中读取,这个速度要比内存慢太多!

 

尤其是针对Redis这种高性能的内存数据库来说,如果Redis中的内存被换到磁盘上,对于Redis这种性能*其敏感的数据库,这个操作时间是无法接受的。

 

我们需要检查机器的内存使用情况,确认是否确实是因为内存不足导致使用到了Swap。

 

如果确实使用到了Swap,要及时整理内存空间,释放出足够的内存供Redis使用,然后释放Redis的Swap,让Redis重新使用内存。

 

释放Redis的Swap过程通常要重启实例,为了避免重启实例对业务的影响,一般先进行主从切换,然后释放旧主节点的Swap,重新启动服务,待数据同步完成后,再切换回主节点即可。

 

可见,当Redis使用到Swap后,此时的Redis的高性能基本被废掉,所以我们需要提前预防这种情况。

 

我们需要对Redis机器的内存和Swap使用情况进行监控,在内存不足和使用到Swap时及时报警出来,及时进行相应的处理。

 

网卡负载过高

 

 

如果以上产生性能问题的场景,你都规避掉了,而且Redis也稳定运行了很长时间,但在某个时间点之后开始,访问Redis开始变慢了,而且一直持续到现在,这种情况是什么原因导致的?

 

之前我们就遇到这种问题,特点就是从某个时间点之后就开始变慢,并且一直持续。这时你需要检查一下机器的网卡流量,是否存在网卡流量被跑满的情况。

 

网卡负载过高,在网络层和TCP层就会出现数据发送延迟、数据丢包等情况。Redis的高性能除了内存之外,就在于网络IO,请求量突增会导致网卡负载变高。

 

如果出现这种情况,你需要排查这个机器上的哪个Redis实例的流量过大占满了网络带宽,然后确认流量突增是否属于业务正常情况,如果属于那就需要及时扩容或迁移实例,避免这个机器的其他实例受到影响。

 

运维层面,我们需要对机器的各项指标增加监控,包括网络流量,在达到阈值时提前报警,及时与业务确认并扩容。

 

以上我们总结了Redis中常见的可能导致延迟增大甚至阻塞的场景,这其中既涉及到了业务的使用问题,也涉及到Redis的运维问题。

 

可见,要想保证Redis高性能的运行,其中涉及到CPU、内存、网络,甚至磁盘的方方面面,其中还包括操作系统的相关特性的使用。

 

作为开发人员,我们需要了解Redis的运行机制,例如各个命令的执行时间复杂度、数据过期策略、数据淘汰策略等,使用合理的命令,并结合业务场景进行优化。

 

作为DBA运维人员,需要了解数据持久化、操作系统fork原理、Swap机制等,并对Redis的容量进行合理规划,预留足够的机器资源,对机器做好完善的监控,才能保证Redis的稳定运行。

 

Redis的*佳实践方式:业务层面和运维层面

 

在上文中,主要讲解了 Redis 常见的导致变慢的场景以及问题定位和分析,主要是由业务使用不合理和运维不当导致的。

 

我们在了解了导致Redis变慢的原因之后,针对性地优化,就可以让Redis稳定发挥出更高性能。

 

接着就来总结一下,在使用Redis时的*佳实践方式,主要包含两个层面:业务层面、运维层面。

 

由于我之前写过很多UGC后端服务,在大量场景下用到了Redis,这个过程中也踩过很多坑,所以在使用过程中也总结了一套合理的使用方法。

 

后来做基础架构,开发Codis、Redis相关的中间件,在这个阶段关注领域从使用层面下沉到Redis的开发和运维,更多聚焦在Redis的内部实现和运维过程中产生的各种问题,在这块也积累了一些经验。

 

下面就针对这两块,分享一下我认为比较合理的Redis使用和运维方法,不一定*全面,也可能与你使用Redis的方法不同,但以下这些方法都是我在踩坑之后总结的实际经验,供你参考。

 

业务层面

 

 

业务层面主要是开发人员需要关注,也就是开发人员在写业务代码时,如何合理地使用Redis。开发人员需要对Redis有基本的了解,才能在合适的业务场景使用Redis,从而避免业务层面导致的延迟问题。

 

在开发过程中,业务层面的优化建议如下:

 

  • key的长度尽量要短,在数据量非常大时,过长的key名会占用更多的内存;
  • 一定避免存储过大的数据(大value),过大的数据在分配内存和释放内存时耗时严重,会阻塞主线程;
  • Redis 4.0以上建议开启lazy-free机制,释放大value时异步操作,不阻塞主线程;
  • 建议设置过期时间,把Redis当做缓存使用,尤其在数量很大的时,不设置过期时间会导致内存的无限增长;
  • 不使用复杂度过高的命令,例如SORT、SINTER、SINTERSTORE、ZUNIONSTORE、ZINTERSTORE,使用这些命令耗时较久,会阻塞主线程;
  • 查询数据时,一次尽量获取较少的数据,在不确定容器元素个数的情况下,避免使用LRANGE key 0 -1,ZRANGE key 0 -1这类操作,应该设置具体查询的元素个数,推荐一次查询100个以下元素;
  • 写入数据时,一次尽量写入较少的数据,例如HSET key value1 value2 value3…,控制一次写入元素的数量,推荐在100以下,大数据量分多个批次写入;
  • 批量操作数据时,用MGET/MSET替换GET/SET、HMGET/MHSET替换HGET/HSET,减少请求来回的网络IO次数,降低延迟,对于没有批量操作的命令,推荐使用pipeline,一次性发送多个命令到服务端;
  • 禁止使用KEYS命令,需要扫描实例时,建议使用SCAN,线上操作一定要控制扫描的频率,避免对Redis产生性能抖动
  • 避免某个时间点集中过期大量的key,集中过期时推荐增加一个随机时间,把过期时间打散,降低集中过期key时Redis的压力,避免阻塞主线程;
  • 根据业务场景,选择合适的淘汰策略,通常随机过期要比LRU过期淘汰数据更快;
  • 使用连接池访问Redis,并配置合理的连接池参数,避免短连接,TCP三次握手和四次挥手的耗时也很高;
  • 只使用db0,不推荐使用多个db,使用多个db会增加Redis的负担,每次访问不同的db都需要执行SELECT命令,如果业务线不同,建议拆分多个实例,还能提高单个实例的性能;
  • 读的请求量很大时,推荐使用读写分离,前提是可以容忍从节数据更新不及时的问题;
  • 写请求量很大时,推荐使用集群,部署多个实例分摊写压力。

 

运维层面

 

 

运维层面主要是DBA需要关注的,目的是合理规划Redis的部署和保障Redis的稳定运行,主要优化如下:

 

  • 不同业务线部署不同的实例,各自独立,避免混用,推荐不同业务线使用不同的机器,根据业务重要程度划分不同的分组来部署,避免某一个业务线出现问题影响其他业务线;
  • 保证机器有足够的CPU、内存、带宽、磁盘资源,防止负载过高影响Redis性能;
  • 以master-slave集群方式部署实例,并分布在不同机器上,避免单点,slave必须设置为readonly;
  • master和slave节点所在机器,各自独立,不要交叉部署实例,通常备份工作会在slave上做,做备份时会消耗机器资源,交叉部署会影响到master的性能;
  • 推荐部署哨兵节点增加可用性,节点数量至少3个,并分布在不同机器上,实现故障自动故障转移;
  • 提前做好容量规划,一台机器部署实例的内存上限,*好是机器内存的一半,主从全量同步时会占用*多额外一倍的内存空间,防止网络大面积故障引发所有master-slave的全量同步导致机器内存被吃光;
  • 做好机器的CPU、内存、带宽、磁盘监控,在资源不足时及时报警处理,Redis使用Swap后性能急剧下降,网络带宽负载过高访问延迟明显增大,磁盘IO过高时开启AOF会拖慢Redis的性能;
  • 设置*大连接数上限,防止过多的客户端连接导致服务负载过高;
  • 单个实例的使用内存建议控制在10G以下,过大的实例会导致备份时间久、资源消耗多,主从全量同步数据时间阻塞时间更长;
  • 设置合理的slowlog阈值,推荐10毫秒,并对其进行监控,产生过多的慢日志需要及时报警;
  • 设置合理的复制缓冲区repl-backlog大小,适当调大repl-backlog可以降低主从全量复制的概率;
  • 设置合理的slave节点client-output-buffer-limit大小,对于写入量很大的实例,适当调大可以避免主从复制中断问题;
  • 备份时推荐在slave节点上做,不影响master性能;
  • 不开启AOF或开启AOF配置为每秒刷盘,避免磁盘IO消耗降低Redis性能;
  • 当实例设置了内存上限,需要调大内存上限时,先调整slave再调整master,否则会导致主从节点数据不一致;
  • 对Redis增加监控,监控采集info信息时,使用长连接,频繁的短连接也会影响Redis性能;
  • 线上扫描整个实例数时,记得设置休眠时间,避免扫描时QPS突增对Redis产生性能抖动;
  • 做好Redis的运行时监控,尤其是expired_keys、evicted_keys、latest_fork_usec指标,短时间内这些指标值突增可能会阻塞整个实例,引发性能问题。

 

以上就是我在使用Redis和开发Redis相关中间件时,总结出来Redis推荐的实践方法,以上提出的这些方面,都或多或少在实际使用中遇到过。

 

可见,要想稳定发挥Redis的高性能,需要在各个方面做好工作,但凡某一个方面出现问题,必然会影响到Redis的性能,这对我们使用和运维提出了更高的要求。

阿里云上redis外网无法访问的解决方案

折磨了一天的我,现在是夜里21:45.终于把redis给弄通了。从安装到跑通可谓是一路艰辛。下边说一下情况吧。

下边这个工具是windows连接redis的*佳工具(可以说没有之一我个人认为的)。

%title插图%num

这就是问题,redis已经安装好了,而且跑起来了。阿里云服务器上可以直接操作,但是本地连接的时候就会报错mmp.

各种骚操作更改配置文件,然后各种不行。下边需要一步步去排查。

1.首先确定你的redis.conf文件配置的几点注意点:

%title插图%num

1).规则要在阿里云上 配置好

2)redis.conf配置文件需要更改几个地方,下图bind  改成0.0.0.0必须要改。改成本机的内部ip我不知道可以不。我没有尝试。

%title插图%num

3)daemonize  yes

4)  还有一个可以配置 密码 ,我给忘记在哪了,反正在该文件 ,应该是可配可不配。配的话需要在代码上或者工具连接时候加上密码

可以说如果完成了上边几步基本上百分之九十的连接不上的问题都会迎刃而解。

Redis如何实现布隆过滤器

本文将介绍布隆过滤器的原理以及Redis如何实现布隆过滤器。

应用场景
1、50亿个电话号码,现有10万个电话号码,如何判断这10万个是否已经存在在50亿个之中?(可能方案:数据库,set, hyperloglog)
2、新闻客户端看新闻时,它会不断推荐新的内容,每次推荐时都要去重,那么如何实现推送去重?
3、爬虫URL去重?
4、NoSQL数据库领域降低数据库的IO请求数量?
5、邮箱系统的垃圾邮件过滤?
布隆过滤器(Bloom Filter)就是专门来解决这种问题的,它起到去重的同时,在空间上还能节省90%以上,只是存在一定的误判概率。

认识布隆过滤器
布隆过滤器是一种类似set的数据结构,只是不太准确,当用bf.exists判断元素是否存在时返回结果存在但真实不一定存在;当返回不存在时肯定是不存在,所以判断去重时有一定的误判概率。
当然,误判只会发生在过滤器没有添加过的元素,对于添加过的元素不会发生误判。
特点:高效地插入和查询,占用空间少,返回的结果是不确定性的。

布隆过滤器原理
每个布隆过滤器对应到Redis的数据结构中就是一个大型的位数组和几个不同的无偏hash函数,无偏表示分布均匀。

添加key时,使用多个hash函数对key进行hash运算得到一个整数索引值,对位数组长度进行取模运算得到一个位置,每个hash函数都会得到一个不同的位置,将这几个位置都置1就完成了add操作。

查询同理,只要有一位是0就表示这个key不存在,但如果都是1,则不一定存在对应的key。

%title插图%num

空间占用估计
布隆过滤器的空间占用有一个简单的计算公式,但推导比较繁琐。布隆过滤器有两个参数,预计元素数量n,错误率f,公式得到两个输出,位数组长度L(即存储空间大小bit),hash函数的*佳数量k。

k = 0.7*(1/n)
f = 0.6185^(L/n)

1、位数组相对长度越长,错误率越低;
2、位数组相对长度越长,需要的hash函数越多;
3、当一个元素平均需要一个字节(8bit)的指纹空间时(L/n=8),错误率大约为2%。

实际元素超出时,误判率会怎样变化?

f = (1-0.5^t)^k # t为实际元素与预计元素的倍数
1、当错误率为10%时,倍数比为2时,错误率接近40%;
2、当错误率为1%,倍数比为2时,错误率15%;
3、当错误率为0.1%,倍数为2时,错误率5%

Redis实现简单Bloom Filter
要想使用redis提供的布隆过滤器,必须添加redis 4.0版本以上的插件才行,具体参照网上安装步骤。

布隆过滤器有两个基本指令,bf.add添加元素,bf.exists查询元素是否存在,bf.madd一次添加多个元素,bf.mexists一次查询多个元素。

> bf.add spiderurl www.baidu.com
> bf.exists spiderurl www.baidu.com
> bf.madd spiderurl www.sougou.com www.jd.com
> bf.mexists spiderurl www.jd.com www.taobao.com

布隆过滤器在*次add的时候自动创建基于默认参数的过滤器,Redis还提供了自定义参数的布隆过滤器。

在add之前使用bf.reserve指令显式创建,其有3个参数,key,error_rate, initial_size,错误率越低,需要的空间越大,error_rate表示预计错误率,initial_size参数表示预计放入的元素数量,当实际数量超过这个值时,误判率会上升,所以需要提前设置一个较大的数值来避免超出。

默认的error_rate是0.01,initial_size是100。

利用布隆过滤器减少磁盘 IO 或者网络请求,因为一旦一个值必定不存在的话,我们可以不用进行后续昂贵的查询请求。

redis两种备份方式(持久化方式)

一、redis持久化的两种方式:
RDB: 对内存中数据库状态进行快照
AOF: 把每条写命令都写入文件
RDB方式:将redis在内存中的数据库状态保存到磁盘里面,RDB文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件的数据状态。

RDB的生成方式:

指向命令手动生成
通过配置自动生成
1.指向命令手动生成

有两个redis命令可以生成RDB文件,一个是SAVE,另一个是BGSAVE,SAVE命令会阻塞redis服务器进程,直到RDB文件

创建完毕为止,在服务器阻塞期间,服务器不能处理任何的进程,BGSAVE会派出一个子进程,然后由子进程负责创建RDB

文件,服务器进程(父进程)继续处理命令请求,创建RDB文件结束之前,客户端发送的 BGSAVE 和 SAVE 命令会被服务器拒*

2.通过配置自动生成

可以设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令,可以通过save选项设置多个保

存条件,但只要其中任意一个条件被满足就会执行BGSAGE命令
%title插图%num

AOF方式:是通过保存redis服务器所执行的写命令来记录数据库状态的AOF文件刷新方式,有三种:

1.appendfsync always — 每提交一个修改命令都调用fsync到AOF文件,非常慢,但是很安全;

2.appendfsync everysec — 每秒都调用fsyns刷新到AOF文件,很快但可能丢失一秒内的数据;

3.appendfsync no — 依靠OS进行刷新,redis不主动刷新AOF,这样*快但是安全性差;

默认并且推荐每秒刷新,这样在速度和安全上都做到了兼顾

二、数据恢复
1.ROB方式

ROB文件的载入工作是在服务器启动时自动执行的,没有专门用于载入ROB文件命令,只要redis服务器再启动时检测到

ROB文件存在,它就会自动载入ROB的文件,在服务器载入的期间,会一直处于阻塞状态,直到载入工作完成为止

2.AOF方式

服务器在启动时,通过载入和执行AOF文件中保存的命令来还原服务器关闭之前的数据,具体过程:

载入AOF文件

创建模拟客户端

从AOF文件中读取一条命令

使用模拟客户端执行命令

循环读取并执行命令,直到全部完成

如果同时启动了AOF和ROB方式,AOF优先,启动时只加载AOF文件恢复数据

三、RDB和AOF对比总结
两种备份方案的选择:

对于RDB持久化,

一方面是bgsave在进行fork操作时redis主进程会阻塞,

另一方面,子进程向硬盘写数据也会带来IO压力,但数据的完整性和一致性受备份条件影响可能较差;

而AOF持久化由于持续的写入IO压力更大,但数据的一致性和完整性较好。
————————————————

原文链接:https://blog.csdn.net/qq_35275233/article/details/87892822

redis持久化,主从及数据备份

现在在项目里已经大量使用redis了,为了提高redis的性能和可靠性我们需要知道和做到以下几件事:

常用内存优化手段与参数
redis的性能如何是完全依赖于内存的,所以我们需要知道如何来控制和节省内存。

首先*重要的一点是不要开启Redis的VM选项,即虚拟内存功能,这个本来是作为Redis存储超出物理内存数据的一种数据在内存与磁盘换入换出的一个持久化策略,但是其内存管理成本非常的高,所以要关闭VM功能,请检查你的redis.conf文件中 vm-enabled 为 no。

其次*好设置下redis.conf中的maxmemory选项,该选项是告诉Redis当使用了多少物理内存后就开始拒*后续的写入请求,该参数能很好的保护好你的Redis不会因为使用了过多的物理内存而导致swap,*终严重影响性能甚至崩溃。

另外Redis为不同数据类型分别提供了一组参数来控制内存使用,我们知道Redis Hash是value内部为一个HashMap,如果该Map的成员数比较少,则会采用类似一维线性的紧凑格式来存储该Map, 即省去了大量指针的内存开销,这个参数控制对应在redis.conf配置文件中下面2项:

hash-max-zipmap-entries 64
hash-max-zipmap-value 512
含义是当value这个Map内部不超过多少个成员时会采用线性紧凑格式存储,默认是64,即value内部有64个以下的成员就是使用线性紧凑存储,超过该值自动转成真正的HashMap。

hash-max-zipmap-value 含义是当 value这个Map内部的每个成员值长度不超过多少字节就会采用线性紧凑存储来节省空间。

以上2个条件任意一个条件超过设置值都会转换成真正的HashMap,也就不会再节省内存了,那么这个值是不是设置的越大越好呢,答案当然是否定的,HashMap的优势就是查找和操作的时间复杂度都是O(1)的,而放弃Hash采用一维存储则是O(n)的时间复杂度,如果成员数量很少,则影响不大,否则会严重影响性能,所以要权衡好这个值的设置,总体上还是*根本的时间成本和空间成本上的权衡。

同样类似的参数还有:

list-max-ziplist-entries 512
说明:list数据类型多少节点以下会采用去指针的紧凑存储格式。

list-max-ziplist-value 64
说明:list数据类型节点值大小小于多少字节会采用紧凑存储格式。

set-max-intset-entries 512
说明:set数据类型内部数据如果全部是数值型,且包含多少节点以下会采用紧凑格式存储。

Redis内部实现没有对内存分配方面做过多的优化,在一定程度上会存在内存碎片,不过大多数情况下这个不会成为Redis的性能瓶颈,不过如果在Redis内部存储的大部分数据是数值型的话,Redis内部采用了一个shared integer的方式来省去分配内存的开销,即在系统启动时先分配一个从1~n 那么多个数值对象放在一个池子中,如果存储的数据恰好是这个数值范围内的数据,则直接从池子里取出该对象,并且通过引用计数的方式来共享,这样在系统存储了大量数值下,也能一定程度上节省内存并且提高性能,这个参数值n的设置需要修改源代码中的一行宏定义REDIS_SHARED_INTEGERS,该值默认是10000,可以根据自己的需要进行修改,修改后重新编译就可以了。

持久化
redis是一个支持持久化的内存数据库,也就是说redis需要经常将内存中的数据同步到磁盘来保证持久化。redis支持两种持久化方式,一种是 Snapshotting(快照)也是默认方式,另一种是Append-only file(缩写aof)的方式。

snapshotting
快照是默认的持久化方式。这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。可以通过配置设置自动做快照持久化的方式。我们可以配置redis在n秒内如果超过m个key被修改就自动做快照,下面是默认的快照保存配置:

save 900 1 #900秒内如果超过1个key被修改,则发起快照保存
save 300 10 #300秒内容如超过10个key被修改,则发起快照保存
save 60 10000 #60秒内容如超过10000个key被修改,则发起快照保存
也可以命令行的方式让redis进行snapshotting:
redis-cli -h ip -p port bgsave
保存快照有save和bgsave两个命令,save操作是在主线程中保存快照的,由于redis是用一个主线程来处理所有 client的请求,这种方式会阻塞所有client请求,所以不推荐使用。
快照生成过程大致如下:

redis调用fork,现在有了子进程和父进程;
父进程继续处理client请求,子进程负责将内存内容写入到临时文件。由于os的写时复制机制(copy on write)父子进程会共享相同的物理页面,当父进程处理写请求时os会为父进程要修改的页面创建副本,而不是写共享的页面。所以子进程的地址空间内的数据是fork时刻整个数据库的一个快照;
当子进程将快照写入临时文件完毕后,用临时文件替换原来的快照文件,然后子进程退出。
同时snapshotting也有不足的,因为两次快照操作之间是有时间间隔的,一旦数据库出现问题,那么快照文件中保存的数据并不是全新的,从上次快照文件生成到Redis停机这段时间的数据全部丢掉了。如果业务对数据准确性要求*高的话,就得采用aof持久化机制了。

aof
aof 比快照方式有更好的持久化性,是由于在使用aof持久化方式时,redis会将每一个收到的写命令都通过write函数追加到文件中(默认是 appendonly.aof)。当redis重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。当然由于os会在内核中缓存 write做的修改,所以可能不是立即写到磁盘上。这样aof方式的持久化也还是有可能会丢失部分修改。不过我们可以通过配置文件告诉redis我们想要通过fsync函数强制os写入到磁盘的时机。有三种方式如下(默认是:每秒fsync一次):

appendonly yes //启用aof持久化方式
# appendfsync always //每次收到写命令就立即强制写入磁盘,*慢的,但是保证完全的持久化,不推荐使用
appendfsync everysec //每秒钟强制写入磁盘一次,在性能和持久化方面做了很好的折中,推荐
# appendfsync no //完全依赖os,性能*好,持久化没保证
aof 的方式也同时带来了另一个问题。持久化文件会变的越来越大。例如我们调用incr test命令100次,文件中必须保存全部的100条命令,其实有99条都是多余的。因为要恢复数据库的状态其实文件中保存一条set test 100就够了。为了压缩aof的持久化文件。redis提供了bgrewriteaof命令。收到此命令redis将使用与快照类似的方式将内存中的数据 以命令的方式保存到临时文件中,*后替换原来的文件。bgrewriteaof命令如下:

redis-cli -h ip -p port bgrewriteaof
bgrewriteaof命令执行过程如下:
redis调用fork ,现在有父子两个进程;
子进程根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令;
父进程继续处理client请求,除了把写命令写入到原来的aof文件中。同时把收到的写命令缓存起来。这样就能保证如果子进程重写失败的话并不会出问题;
当子进程把快照内容写入以命令方式写到临时文件中后,子进程发信号通知父进程。然后父进程把缓存的写命令也写入到临时文件;
现在父进程可以使用临时文件替换老的aof文件,并重命名,后面收到的写命令也开始往新的aof文件中追加。
这两种持久化方式有各自的特点,快照相对性能影响不大,但一旦崩溃,数据量丢失较大,而aof数据安全性较高,但性能影响较大,这就得根据业务特点自行选择了。

主从复制
redis的主从复制策略是通过其持久化的rdb文件来实现的,其过程是先dump出rdb文件,将rdb文件全量传输给slave,然后再将dump后的操作实时同步到slave中。

要使用主从功能需要在slave端进行简单的配置:
slaveof master_ip master_port #如果这台机器是台redis slave,可以打开这个设置。
slave-serve-stale-data no #如果slave 无法与master 同步,设置成slave不可读,方便监控脚本发现问题。
配置好之后启动slave端就可以进行主从复制了,主从复制的过程大致如下:

Slave端在配置文件中添加了slaveof指令,于是Slave启动时读取配置文件,初始状态为REDIS_REPL_CONNECT;
Slave端在定时任务serverCron(Redis内部的定时器触发事件)中连接Master,发送sync命令,然后阻塞等待master发送回其内存快照文件(*新版的Redis已经不需要让Slave阻塞);
Master端收到sync命令简单判断是否有正在进行的内存快照子进程,没有则立即开始内存快照,有则等待其结束,当快照完成后会将该文件发送给Slave端;
Slave端接收Master发来的内存快照文件,保存到本地,待接收完成后,清空内存表,重新读取Master发来的内存快照文件,重建整个内存表数据结构,并*终状态置位为 REDIS_REPL_CONNECTED状态,Slave状态机流转完成;
Master端在发送快照文件过程中,接收的任何会改变数据集的命令都会暂时先保存在Slave网络连接的发送缓存队列里(list数据结构),待快照完成后,依次发给Slave,之后收到的命令相同处理,并将状态置位为 REDIS_REPL_ONLINE。
整个复制过程完成,流程如下图所示:

%title插图%num

从以上的复制过程中可以发现,Slave从库在连接Master主库时,Master会进行内存快照,然后把整个快照文件发给Slave,也就是没有象MySQL那样有复制位置的概念,即无增量复制,如果一个master连接多个slave,就会比较影响master性能了。

数据备份策略
具体的备份策略是可以很灵活的,比如可以大致如下:

为了提高master的性能关闭master的持久化机制,即不进行快照也不进行aof,而是在凌晨访问量低的时候定时的用bgsave命令进行快照,并将快照文件保存到备份服务器上;
slave端开启aof机制,并定时的用bgrewriteaof 进行数据压缩,将压缩后的数据文件保存到备份服务器上;
定时的检查master与slave上的数据是否一致;
当master出问题并需要恢复时,如果采用master的备份快照恢复直接将备份的dump.rdb拷贝到相应路径下重启即可;如果要从slave端恢复,需要在slave端执行一次快照,然后将快照文件拷贝到master路径下然后重启即可。不过有一点需要注意的是,master重启时slave端数据会被冲掉,所以slave端要在master重启前做好备份。
持久化磁盘IO方式及其带来的问题
有Redis线上运维经验的人会发现Redis在物理内存使用比较多,但还没有超过实际物理内存总容量时就会发生不稳定甚至崩溃的问题,有人认为是基于快照方式持久化的fork系统调用造成内存占用加倍而导致的,这种观点是不准确的,因为fork 调用的copy-on-write机制是基于操作系统页这个单位的,也就是只有有写入的脏页会被复制,但是一般的系统不会在短时间内所有的页都发生了写入而导致复制,那么是什么原因导致Redis崩溃的呢?

答案是Redis的持久化使用了Buffer IO造成的,所谓Buffer IO是指Redis对持久化文件的写入和读取操作都会使用物理内存的Page Cache,而大多数数据库系统会使用Direct IO来绕过这层Page Cache并自行维护一个数据的Cache,而当Redis的持久化文件过大(尤其是快照文件),并对其进行读写时,磁盘文件中的数据都会被加载到物理内存中作为操作系统对该文件的一层Cache,而这层Cache的数据与Redis内存中管理的数据实际是重复存储的,虽然内核在物理内存紧张时会做Page Cache的剔除工作,但内核可能认为某块Page Cache更重要,而让你的进程开始Swap,这时你的系统就会开始出现不稳定或者崩溃了。经验是当你的Redis物理内存使用超过内存总容量的3/5时就会开始比较危险了。

————————————————

原文链接:https://blog.csdn.net/tonyXf121/article/details/8475603

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速