关于Redis你可能不了解的一些事

引子

Redis 是一个高性能分布式的key-value数据库。它支持多种数据结构,并可应用于缓存、队列等多种场景下。使用过Redis的小伙伴们可能对这些已经非常熟知了,下面我想来谈谈Redis也许并不被每个人了解的那点事。

Redis持久化机制

刚看到标题你可能会说,我知道,不就是RDB和AOF嘛。这些已经是老生常谈了。那么我们今天就深入谈谈这两种持久化方式的逻辑和原理。

RDB的原理

在Redis中RDB持久化的触发分为两种:自己手动触发与Redis定时触发。

针对RDB方式的持久化,手动触发可以使用:

(1)save:会阻塞当前Redis服务器,直到持久化完成,线上应该禁止使用。
(2)bgsave:该触发方式会fork一个子进程,由子进程负责持久化过程,因此阻塞只会发生在fork子进程的时候。

而自动触发的场景如下:

根据我们的 save m n 配置规则自动触发;
从节点全量复制时,主节点发送rdb文件给从节点完成复制操作,主节点会触发 bgsave;
执行 debug reload 时处罚;
执行 shutdown时,如果没有开启aof,也会触发。

由于 save 基本不会被使用到,我们来看看 bgsave 这个命令是如何完成RDB的持久化的。

RDB文件保存过程

(1)redis调用fork,现在有了子进程和父进程。
(2)父进程继续处理client请求,子进程负责将内存内容写入到临时文件。由于os的写时复制机制(copy on write)父子进程会共享相同的物理页面,当父进程处理写请求时os会为父进程要修改的页面创建副本,而不是写共享的页面。所以子进程的地址空间内的数据是fork时刻整个数据库的一个快照。
(3)当子进程将快照写入临时文件完毕后,用临时文件替换原来的快照文件,然后子进程退出。

PS:fork 操作会阻塞,导致Redis读写性能下降。我们可以控制单个Redis实例的最大内存,来尽可能降低Redis在fork时的时间消耗;或者控制自动触发的频率减少fork次数。

AOF的原理

AOF的整个流程大体来看可以分为两步,一步是命令的实时写入(如果是 appendfsync everysec 配置,会有1s损耗),第二步是对aof文件的重写。

对于增量追加到文件这一步主要的流程是:

(1)命令写入
(2)追加到aof_buf
(3)同步到aof磁盘

那么这里为什么要先写入buf再同步到磁盘呢?如果实时写入磁盘会带来非常高的磁盘IO,影响整体性能。

AOF重写

你可以会想,每一条写命令都生成一条日志,那么AOF文件是不是会很大?答案是肯定的,AOF文件会越来越大,所以Redis又提供了一个功能,叫做AOF rewrite。其功能就是重新生成一份AOF文件,新的AOF文件中一条记录的操作只会有一次,而不像一份老文件那样,可能记录了对同一个值的多次操作。

手动触发: bgrewriteaof

自动触发就是根据配置规则来触发,当然自动触发的整体时间还跟Redis的定时任务频率有关系。

下面来看看重写的流程图:

(1)redis调用fork ,现在有父子两个进程
(2)子进程根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令
(3)父进程继续处理client请求,除了把写命令写入到原来的aof文件中。同时把收到的写命令缓存起来。这样就能保证如果子进程重写失败的话并不会出问题。
(4)当子进程把快照内容写到临时文件中后,子进程发信号通知父进程。然后父进程把缓存的写命令也写入到临时文件。
(5)现在父进程可以使用临时文件替换老的aof文件,并重命名,后面收到的写命令也开始往新的aof文件中追加。

PS:需要注意到是重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。

Redis为什么这么快?

Redis采用的是基于内存的单进程单线程模型的 KV 数据库,由C语言编写,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。这个数据不比采用单进程多线程的同样基于内存的 KV 数据库 Memcached 差!原因如下:

1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速;
2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用去考虑各种锁的问题,不存在加锁释放锁的操作,也不可能出现死锁而导致的性能消耗;
4、使用多路I/O复用模型,非阻塞IO;
5、使用的底层模型不同,底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接构建了自己的VM机制。

多路I/O复用模型

多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗)。

Redis事务

Redis中的事务(transaction)是一组命令的集合。事务同命令一样都是Redis最小的执行单位,一个事务中的命令要么都执行,要么都不执行。Redis事务的实现需要 MULTI 和 EXEC 两个命令,事务开始的时候先向Redis服务器发送 MULTI 命令,然后依次发送需要在本次事务中处理的命令,最后再发送 EXEC 命令表示事务命令结束。

举个例子,使用redis-cli连接redis,然后在命令行工具中输入如下命令:

从输出中可以看到,当输入MULTI命令后,服务器返回OK表示事务开始成功,然后依次输入需要在本次事务中执行的所有命令,每次输入一个命令服务器并不会马上执行,而是返回”QUEUED”,这表示命令已经被服务器接受并且暂时保存起来,最后输入EXEC命令后,本次事务中的所有命令才会被依次执行,可以看到最后服务器一次性返回了三个OK,这里返回的结果与发送的命令是按顺序,这说明这次事务中的命令全都执行成功了。

再举个例子,在命令行工具中输入如下命令:

和前面的例子一样,先输入MULTI最后输入EXEC表示中间的命令属于一个事务,不同的是中间输入的命令有一个错误(set写成了sett),这样因为有一个错误的命令导致事务中的其他命令都不执行了,可见事务中的所有命令是保持一致的。

如果客户端在发送EXEC命令之前断线了,则服务器会清空事务队列,事务中的所有命令都不会被执行。而一旦客户端发送了EXEC命令之后,事务中的所有命令都会被执行,即使此后客户端断线也没关系,因为服务器已经保存了事务中的所有命令。

除了保证事务中的所有命令要么全执行要么全不执行外,Redis的事务还能保证一个事务中的命令依次执行而不会被其他命令插入。试想一个客户端A需要执行几条命令,同时客户端B发送了几条命令,如果不使用事务,则客户端B的命令有可能会插入到客户端A的几条命令中,如果想避免这种情况发生,也可以使用事务。

Redis事务错误处理

如果一个事务中的某个命令执行出错,Redis会怎样处理呢?要回答这个问题,首先要搞清楚是什么原因导致命令执行出错:

1.语法错误:就像上面的例子一样,语法错误表示命令不存在或者参数错误,这种情况需要区分Redis的版本,Redis 2.6.5之前的版本会忽略错误的命令,执行其他正确的命令,2.6.5之后的版本会忽略这个事务中的所有命令,都不执行

2.运行错误 运行错误表示命令在执行过程中出现错误,比如用GET命令获取一个散列表类型的键值。这种错误在命令执行之前Redis是无法发现的,所以在事务里这样的命令会被Redis接受并执行。如果食物里有一条命令执行错误,其他命令依旧会执行(包括出错之后的命令)。

Redis中的事务并没有关系型数据库中的事务回滚(rollback)功能,因此使用者必须自己收拾剩下的烂摊子。不过由于Redis不支持事务回滚功能,这也使得Redis的事务简洁快速。

WATCH、UNWATCH、DISCARD命令

从上面的例子我们可以看到,事务中的命令要全部执行完之后才能获取每个命令的结果,但是如果一个事务中的命令B依赖于他上一个命令A的结果的话该怎么办呢?就比如说实现类似Java中的i++的功能,先要获取当前值,才能在当前值的基础上做加一操作。这种场合仅仅使用上面介绍的MULTI和EXEC是不能实现的,因为MULTI和EXEC中的命令是一起执行的,并不能将其中一条命令的执行结果作为另一条命令的执行参数,所以这个时候就需要引进Redis事务家族中的另一成员:WATCH命令

换个角度思考上面说到的实现i++的方法,可以这样实现:

  • 监控i的值,保证i的值不被修改
  • 获取i的原值
  • 如果过程中i的值没有被修改,则将当前的i值+1,否则不执行

WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,EXEC命令执行完之后被监控的键会自动被UNWATCH)。举个例子:

上面的例子中,首先设置mykey的键值为1,然后使用WATCH命令监控mykey,随后更改mykey的值为2,然后进入事务,事务中设置mykey的值为3,然后执行EXEC运行事务中的命令,最后使用get命令查看mykey的值,发现mykey的值还是2,也就是说事务中的命令根本没有执行(因为WATCH监控mykey的过程中,mykey被修改了,所以随后的事务便会被取消)。

UNWATCH命令可以在WATCH命令执行之后、MULTI命令执行之前取消对某个键的监控。举个例子:

上面的例子中,首先设置mykey的键值为1,然后使用WATCH命令监控mykey,随后更改mykey的值为2,然后取消对mykey的监控,再进入事务,事务中设置mykey的值为3,然后执行EXEC运行事务中的命令,最后使用get命令查看mykey的值,发现mykey的值还是3,也就是说事务中的命令运行成功。

DISCARD命令则可以在MULTI命令执行之后,EXEC命令执行之前取消WATCH命令并清空事务队列,然后从事务状态中退出。举个例子:

上面的例子中,首先设置mykey的键值为1,然后使用WATCH命令监控mykey,随后更改mykey的值为2,然后进入事务,事务中设置mykey的值为3,然后执行DISCARD命令,再执行EXEC运行事务中的命令,发现报错“ERR EXEC without MULTI”,说明DISCARD命令成功执行——取消WATCH命令并清空事务队列,然后从事务状态中退出。

Redis分布式锁

上面介绍的Redis的WATCH、MULTI和EXEC命令,只会在数据被其他客户端抢先修改的情况下,通知执行这些命令的客户端,让它撤销对数据的修改操作,并不能阻止其他客户端对数据进行修改,所以只能称之为乐观锁(optimistic locking)。

而这种乐观锁并不具备可扩展性——当客户端尝试完成一个事务的时候,可能会因为事务执行失败而进行反复的重试。保证数据准确性非常重要,但是当负载变大的时候,使用乐观锁的做法并不完美。这时就需要使用Redis实现分布式锁。

分布式锁:是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

Redis命令介绍:

Redis实现分布式锁主要用到命令是SETNX命令(SET if Not eXists)。

语法:SETNX key value

功能:当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。

使用Redis构建锁:

思路:将“lock:”+参数名设置为锁的键,使用SETNX命令尝试将一个随机的uuid设置为锁的值,并为锁设置过期时间,使用SETNX设置锁的值可以防止锁被其他进程获取。如果尝试获取锁的时候失败,那么程序将不断重试,直到成功获取锁或者超过给定是时限为止。

代码:

public String acquireLockWithTimeout(
 Jedis conn, String lockName, long acquireTimeout, long lockTimeout)
 {
 String identifier = UUID.randomUUID().toString(); //锁的值
 String lockKey = "lock:" + lockName; //锁的键
 int lockExpire = (int)(lockTimeout / 1000); //锁的过期时间
 long end = System.currentTimeMillis() + acquireTimeout; //尝试获取锁的时限
 while (System.currentTimeMillis() < end) { //判断是否超过获取锁的时限
 if (conn.setnx(lockKey, identifier) == 1){ //判断设置锁的值是否成功
 conn.expire(lockKey, lockExpire); //设置锁的过期时间
 return identifier; //返回锁的值
 }
 if (conn.ttl(lockKey) == -1) { //判断锁是否超时
 conn.expire(lockKey, lockExpire);
 }
 try {
 Thread.sleep(1000); //等待1秒后重新尝试设置锁的值
 }catch(InterruptedException ie){
 Thread.currentThread().interrupt();
 }
 }
 // 获取锁失败时返回null
 return null;
 }

锁的释放:

思路:使用WATCH命令监视代表锁的键,然后检查键的值是否和加锁时设置的值相同,并在确认值没有变化后删除该键。

代码:

public boolean releaseLock(Jedis conn, String lockName, String identifier) {
 String lockKey = "lock:" + lockName; //锁的键
 while (true){
 conn.watch(lockKey); //监视锁的键
 if (identifier.equals(conn.get(lockKey))){ //判断锁的值是否和加锁时设置的一致,即检查进程是否仍然持有锁
 Transaction trans = conn.multi();
 trans.del(lockKey); //在Redis事务中释放锁
 List<Object> results = trans.exec();
 if (results == null){
 continue; //事务执行失败后重试(监视的键被修改导致事务失败,重新监视并释放锁)
 }
 return true;
 }
 conn.unwatch(); //解除监视
 break;
 }
 return false;
 }

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • NoSQL和Redis简介及Redis在Windows下的安装和使用教程

    NoSQL简介 介绍redis前,我想还是先认识下NoSQL,即not only sql, 是一种非关系型的数据存储,key/value键值对存储.现有Nosql DB 产品: Redis/MongoDB/Memcached/Hbase/Cassandra/ Tokyo Cabinet/Voldemort/Dynomite/Riak/ CouchDB/Hypertable/Flare/Tin/Lightcloud/ KiokuDB/Scalaris/Kai/ThruDB, 等等~~~ 为什么需要

  • Redis教程(一):Redis简介

    一.简介: 在过去的几年中,NoSQL数据库一度成为高并发.海量数据存储解决方案的代名词,与之相应的产品也呈现出雨后春笋般的生机.然而在众多产品中能够脱颖而出的却屈指可数,如Redis.MongoDB.BerkeleyDB和CouchDB等.由于每种产品所拥有的特征不同,因此它们的应用场景也存在着一定的差异,下面仅给出简单的说明: 1). BerkeleyDB是一种极为流行的开源嵌入式数据库,在更多情况下可用于存储引擎,比如BerkeleyDB在被Oracle收购之前曾作为MySQL的存储引擎,

  • Redis教程(二):String数据类型

    一.概述: 字符串类型是Redis中最为基础的数据存储类型,它在Redis中是二进制安全的,这便意味着该类型可以接受任何格式的数据,如JPEG图像数据或Json对象描述信息等.在Redis中字符串类型的Value最多可以容纳的数据长度是512M. 二.相关命令列表: 命令原型 时间复杂度 命令描述 返回值 APPENDkeyvalue O(1) 如果该Key已经存在,APPEND命令将参数Value的数据追加到已存在Value的末尾.如果该Key不存在,APPEND命令将会创建一个新的Key/V

  • Redis教程(七):Key操作命令详解

    一.概述: 在该系列的前几篇博客中,主要讲述的是与Redis数据类型相关的命令,如String.List.Set.Hashes和Sorted-Set.这些命令都具有一个共同点,即所有的操作都是针对与Key关联的Value的.而该篇博客将主要讲述与Key相关的Redis命令.学习这些命令对于学习Redis是非常重要的基础,也是能够充分挖掘Redis潜力的利器.       在该篇博客中,我们将一如既往的给出所有相关命令的明细列表和典型示例,以便于我们现在的学习和今后的查阅. 二.相关命令列表: 命

  • 使用Redis实现用户积分排行榜的教程

    排行榜功能是一个很普遍的需求.使用 Redis 中有序集合的特性来实现排行榜是又好又快的选择. 一般排行榜都是有实效性的,比如"用户积分榜".如果没有实效性一直按照总榜来排,可能榜首总是几个老用户,对于新用户来说,那真是太令人沮丧了. 首先,来个"今日积分榜"吧,排序规则是今日用户新增积分从多到少. 那么用户增加积分时,都操作一下记录当天积分增加的有序集合. 假设今天是 2015 年 04 月 01 日,UID 为 1 的用户因为某个操作,增加了 5 个积分. Re

  • Redis教程(十):持久化详解

    一.Redis提供了哪些持久化机制: 1). RDB持久化:     该机制是指在指定的时间间隔内将内存中的数据集快照写入磁盘.        2). AOF持久化:     该机制将以日志的形式记录服务器所处理的每一个写操作,在Redis服务器启动之初会读取该文件来重新构建数据库,以保证启动后数据库中的数据是完整的.     3). 无持久化:     我们可以通过配置的方式禁用Redis服务器的持久化功能,这样我们就可以将Redis视为一个功能加强版的memcached了.     4).

  • Python与Redis的连接教程

    今天在写zabbix storm job监控脚本的时候用到了python的redis模块,之前也有用过,但是没有过多的了解,今天看了下相关的api和源码,看到有ConnectionPool的实现,这里简单说下. 在ConnectionPool之前,如果需要连接redis,我都是用StrictRedis这个类,在源码中可以看到这个类的具体解释:   redis.StrictRedis Implementation of the Redis protocol.This abstract class

  • 超强、超详细Redis数据库入门教程

    [本教程目录] 1.redis是什么 2.redis的作者何许人也 3.谁在使用redis 4.学会安装redis 5.学会启动redis 6.使用redis客户端 7.redis数据结构 – 简介 8.redis数据结构 – strings 9.redis数据结构 – lists 10.redis数据结构 – 集合 11.redis数据结构 – 有序集合 12.redis数据结构 – 哈希 13.聊聊redis持久化 – 两种方式 14.聊聊redis持久化 – RDB 15.聊聊redis持

  • Redis教程(十五):C语言连接操作代码实例

    在之前的博客中已经非常详细的介绍了Redis的各种操作命令.运行机制和服务器初始化参数配置.本篇博客是该系列博客中的最后一篇,在这里将给出基于Redis客户端组件访问并操作Redis服务器的代码示例.然而需要说明的是,由于Redis官方并未提供基于C接口的Windows平台客户端,因此下面的示例仅可运行于Linux/Unix平台.但是对于使用其它编程语言的开发者而言,如C#和Java,Redis则提供了针对这些语言的客户端组件,通过该方式,同样可以达到基于Windows平台与Redis服务器进行

  • 64位Windows下安装Redis教程

    Redis对于Linux是官方支持的,安装和使用没有什么好说的,普通使用按照官方指导,5分钟以内就能搞定.详情请参考:http://redis.io/download 但有时候又想在windows下折腾下Redis,可以从redis下载页面看到如下提示: 复制代码 代码如下: Win64 Unofficial The Redis project does not directly support Windows,  however the Microsoft Open Tech group de

随机推荐