前言

这篇文章源于我昨天看到的一个有意思的问题。

快照持久化是个很耗时间的操作,而Redis采用fork一个子进程出来进行持久化。理论而言,fork出来的子进程会拷贝父进程所有的数据,这样当Redis要持久化2G的内存数据的时候,子进程也会占据几乎2G的内存。那么此时Redis相关的进程内存占用就会达到4G左右。这在数据体量比较小的时候还不严重,但是比如你的电脑内存是8G,目前备份快照数据本身体积是5G,那么按照上面的计算备份一定是无法进行的。所幸在Unix类操作系统上面做了如下的优化:在刚开始的时候父子进程共享相同的内存,直到父进程或者子进程进行内存的写入后,对被写入的内存共享才结束。这样就会减少快照持久化时对内存的消耗。这就是COW技术,减少了快照生成时候的内存使用的同时节省了不少时间。而备份期间多用的内存正比于在此期间接收到的数据更改请求数目。

更具体地讲,我们知道每个进程的虚拟空间是被划分成正文段,数据段,堆,栈这四个部分,同时对应于每一个部分,操作系统会为之分配真实物理块。当我们从父进程P1中fork出一个子进程P2时:

  • 在没有CopyOnWrite之前,我们要给子进程生成虚拟空间,并为虚拟空间地每一个部分分配对应地物理空间,接着要把父进程对应部分地物理空间地内容复制到子进程的空间中。这实际上是个既耗时又耗费空间地操作。
  • 有了COW之后, fork子进程时,我们只为其生成虚拟空间,但是并不先为每个部分分配真实的物理空间,而是让每个虚拟空间部分仍然指向父进程的物理空间。只有当父进程或子进程修改相应的共享内存空间时,才会为子进程分配物理空间并把父进程的物理空间内容进行复制。这就是所谓的写时复制,即把内存的复制延迟到了内存写入的时刻

同时需要注意地是,父子进程共享的空间粒度是页(在Linux中,页的大小为4KB),父/子进程修改某个页时,该页的共享才结束,同时子进程分配该页大小的物理空间复制父进程对应页的内容。这样,如果当子进程运行期间,父子进程都没有修改数据,那么操作系统就节省了大量的内存复制时间和占用空间。

上面讲的CopyOnWrite是操作系统在fork子进程时实现的。而题主问的是,我们能不能用多线程来实现COW进而来实现RDB生成呢?在回答这个问题之前,为了让大家更明白多线程实现COW的事情,我们先以Java中的CopyOnWriteArrayList为例进行来看多线程实现COW是个什么操作。

首先我们看这么一段代码。这段代码在多线程下肯定是不安全的,为了让它变得更安全,一个简单的方法就是读取和写入时都加锁,即同时要有读锁和写锁。但是我们都知道锁是非常影响性能的,为了减少锁的消耗,Java便推出了CopyOnWriteArrayList。

public 

CopyOnWriteArrayList 相对于 ArrayList 线程安全,底层通过复制数组的方式来实现,其核心概念就是: 数据读取时直接读取,不需要锁,数据写入时,需要锁,且对副本进行操作。那么当数据的操作以读取为主时,我们便可以省去大量的读锁带来的消耗。同时为了能让多线程操作List时,一个线程的修改能被另一个线程立马发现,CopyOnWriteList采用了Volatile关键词来进行修饰,即每次数据读取不从缓存里面读取,而是直接从数据的内存地址中读取。

我们以CopyOnWriteArrayList 的add()操作为例来看。

// 这个数组是核心的,因为用volatile修饰了

总结而言,多线程实现COW实际上就是以空间换取时间使得数据读取时不需要锁。只是减少了读锁的开销,但与常规的多线程操作共享数据的本质没有什么区别。

好,最后我们回到题主的问题,使用多线程实现COW来实现RDB生成这个问题可以规约成使用多线程实现RDB生成问题。所以我们的问题核心在于解决能不能使用多线程来实现RDB生成。如果要这么做我们需要做出哪些额外的操作?

大家肯定会想RDB的生成过程本质不就是把内存中的数据序列化到硬盘文件中么?RDB生成时,子线程只需要进行数据读取,主线程修改时加锁修改。并且为了避免常规操作时锁的过多开销,我们可以只需要在RDB生成期间再加锁,常规期间写操作不需要加锁。这样总体而言带来的开销不会多很多,因为毕竟RDB生成是个低频的操作。

但这里面其实有个很重要的概念就是”SnapShot“, 即RDB是Redis内存的某一个时刻的快照。比如,我6:15分开始生成RDB, 那么这个RDB保存的数据就是当时那一刻整个Redis内存中的数据状态。使用多进程我们是很容易保证这一点的,但是使用多线程,我们是很难保证这个性质的。因为你可能在DUMP的过程中,主线程又修改了你还没读取的数据,又或者主线程修改了你刚刚已经序列化到文件中的某个数据。也就是说使用多线程进行生成RDB的时候,你并不知道自己生成的数据是到底哪个时刻的数据。你也并不知道修改期间哪些主线程的命令已经体现在了RDB文件中。

这个会产生大的影响么?单机版的Redis也许不大会,但是Redis集群中涉及到主从复制的时候就会产生很大的影响。

单机版Redis生成RDB无非就是想留个档,那么具体RDB是哪一个时刻的,可能没那么重要。更重要的是要生成RDB。而且这个RDB显然越新越好,因为越新,Redis重启后丢失的数据就越少。那么从这个角度而言,甚至说用多线程反而可能更好,因为多线程时可以让一些生成RDB期间被修改的数据也体现在RDB中。

但是涉及到主从复制时就不可以了。主从复制时,Redis主节点会生成当时时刻的内存快照RDB文件,同时把RDB期间的所有的命令写到缓存repl_backlog中,等从节点从主节点的RDB文件恢复数据之后,便从主节点的命令缓存中读取所有的命令再进行执行一遍,以达到和主节点相同的状态。那么用多线程生成RDB时,如果当主线程执行某个写入命令时,从线程还未DUMP该数据,那么从线程生成的RDB就包含了该命令的执行结果。而子节点又恢复了数据之后,相当于子节点已经执行过了这个命令。那么当子节点从主节点的命令缓存中拉取命令来再执行一遍后,有些命令就会被重复执行。

看完觉得对你有帮助的话,那就记得关注我的专栏!

一亩三分地​zhuanlan.zhihu.com

redis 保存 array list 区别_为什么Redis的RDB备份不用多线程实现CopyOnWrite?相关推荐

  1. redis 保存 array list 区别_Redis科普篇

    这是学习Java的小姐姐第53篇原创文章 前言 hello,好久不见,又断更了一段时间.同事大部分离职了,但是活还是一样,所以只能硬着头皮顶上.现在总算歇会了,决定开启Redis源码系列,希望不要啪啪 ...

  2. redis rdb aof区别_理解Redis的持久化机制:RDB和AOF

    什么是Redis持久化? Redis作为一个键值对内存数据库(NoSQL),数据都存储在内存当中,在处理客户端请求时,所有操作都在内存当中进行,如下所示: 这样做有什么问题呢? 注 意 文末有:362 ...

  3. redis实现轮询算法_用redis实现支持优先级的消息队列

    为什么需要消息队列 系统中引入消息队列机制是对系统一个非常大的改善.例如一个web系统中,用户做了某项操作后需要发送邮件通知到用户邮箱中.你可以使用同步方式让用户等待邮件发送完成后反馈给用户,但是这样 ...

  4. redis 缓存过期默认时间_过期redis时间

    Redis命令--键(key) Redis 键(key) Redis 键命令用于管理 redis 的键. 语法 Redis 键命令的基本语法如下: redis 127.0.0.1:6379> C ...

  5. redis分布式锁java代码_基于redis实现分布式锁

    " 在上一篇文章中介绍了动态配置定时任务,其中的原理跟spring 定时任务注解@Scheduled一样的,都是通过线程池和定义执行时间来控制.来思考一个问题,如果我们的定时任务在分布式微服 ...

  6. redis缓存原理与实现_基于Redis实现范围查询的IP库缓存设计方案

    点击上方"码农沉思录"  发现更多精彩我先说下结果.我现在还不敢放线上去测,这是本地测的数据,我4g内存的电脑本地开redis,一次都没写完过全部数据,都是写一半后不是redis挂 ...

  7. 简述事件接口与事件适配器的联系与区别_通过Redis学习事件驱动设计

    01 为什么我说C程序员都要阅读Redis源码 主要原因就是『简洁』.如果你用源码编译过Redis,你会发现十分轻快,一步到位.其他语言的开发者可能不会了解这种痛,作为C/C++程序员,如果你源码编译 ...

  8. lpush和rpush的区别_关于redis之lpush、rpush、lset、lrem

    下面由栏目给大家介绍redis之lpush.rpush.lset.lrem,希望对需要的朋友有所帮助! 1.lpush 在key对应 list的头部添加字符串元素 2.rpush 在key对应 lis ...

  9. redis失效了mysql扛不住_我是Redis,MySQL大哥被我害惨了!

    我是Redis 你好,我是Redis,一个叫Antirez的男人把我带到了这个世界上. 说起我的诞生,跟关系数据库MySQL还挺有渊源的. 在我还没来到这个世界上的时候,MySQL过的很辛苦,互联网发 ...

最新文章

  1. 如何挑选靠谱的Java培训机构
  2. 建军92周年,让我们了解那些先进的军用机器人
  3. Leetcode03
  4. android 禁用dlsym_Android7.0对dlopen的改变——读取私有.so结果变化
  5. 用于Elasticsearch成绩单的Java客户端
  6. 决策者根据什么曲线做出决策_如何做出产品设计决策
  7. python代码太长_Python 太糟糕了?开发者总结了 8 大原因
  8. 学习笔记:MySQL字符串类型
  9. Load Average (系统负载)
  10. CIO如何计算信息化的投资回报率?
  11. C++在使用fgetc读取文件时出现方框乱码
  12. Intel桌面CPU系列
  13. 单字双字三字_古人取名有什么讲究?为什么有时候单字多有时候双字多?
  14. 多摩川绝对值编码器CPLD FPGA通信源码(VHDL格式+协议+说明书)
  15. 群晖DS918+硬盘选择和阵列选择
  16. python使用excel数据分析统计服_像Excel一样使用python进行数据分析
  17. 内嵌式js微信扫码登录及自定义样式
  18. 802.11a/b/g/n/ac, 802.11kvr 说明
  19. CentOS7下配置域名解析服务器
  20. plotly画图(包含x轴设置marker类型设置)

热门文章

  1. 计算机考试用远程桌面,职称计算机考试:教你体验XP远程桌面多用户登录
  2. 网格布局每个网格都能放置一个组件_Android综合试题
  3. bootstrap 数据加载中提示_解决Quartz定时器中查询懒加载数据no session的问题
  4. python封装成可执行的小程序_Python打包小程序
  5. python第三方包是什么意思_安装Python和第三方包的方法
  6. linux内核驱动开发 培训,嵌入式Linux驱动开发培训 - 华清远见教育集团官网
  7. php mvc和mvvm,mvvm模式和mvc模式的区别是什么
  8. Jar mismatch! Fix your dependencies
  9. JHipster生成微服务架构的应用栈(一)- 准备工作
  10. shell切割日志脚本