缓存大概可以分为两类,一种是应用内的缓存,比如Map(简单的数据结构),以及EH cache(java 第三方库),另一种就是缓存组件,比如Memached,Redis;
redis支持五种存储结构
String
string 类型支持的数据格式有 字符串,整数,浮点。
对于整数 可以使用 incr 命令实现原子递增
内部数据结构
在Redis内部,String类型通过 int、SDS(simple dynamic string)作为结构存储,int用来存放整型数据,sds存放字节/字符串和浮点型数据。在C的标准字符串结构下进行了封装,用来提升基本操作的性能,同时也充分利用已有的 C的标准库,简化实现逻辑。我们可以在redis的源码中【sds.h】中看到sds的结构如下;
typedef char *sds;
redis3.2分支引入了五种sdshdr类型,目的是为了满足不同长度字符串可以使用不同大小的Header,从而节省内存,,每次在创建一个sds时根据sds的实际长度判断应该选择什么类型的sdshdr,不同类型的sdshdr占用的内存空间不同。这样细分一下可以省去很多不必要的内存开销,下面是3.2的sdshdr定义
`struct __attribute__ ((__packed__)) sdshdr8 //8表示字符串最大长度是2^8-1 (长度为255)``
{
//表示当前sds的长度(单位是字节)``
uint8_t len;
//表示已为sds分配的内存大小(单位是字节)``
uint8_t alloc;
//用一个字节表示当前sdshdr的类型,因为有sdshdr有五种类型,所以至少需要3位来表示
//000:sdshdr5,
//001:sdshdr8,
//010:sdshdr16,
//011:sdshdr32,
//100:sdshdr64。
//高5位 用不到所以都为0。``
unsigned char flags;
//sds实际存放的位置``
char buf[];
};`
sdshdr8的内存布局
list
列表类型(list)可以存储一个有序的字符串列表,常用的操作是向列表两端添加元素或者获得列表的某一个片段。列表类型内部使用双向链表实现,所以向列表两端添加元素的时间复杂度为O(1), 获取越接近两端的元素速度就越 快。这意味着即使是一个有几千万个元素的列表,获取头部或尾部的10条记录也是很快的
内部存储结构
redis3.2之前,List类型的value对象内部以linkedlist或者ziplist来实现。
当list的元素个数和单个元素的长度比较小 的时候,Redis会采用ziplist(压缩列表)来实现来减少内存占用。否则就会采用linkedlist(双向链表)结构。
这两种存储方式都有优缺点:
双向链表linkedlist在链表两端进行push和pop操作,在插入节点上复杂度比较低,但是内存开销比较大;
ziplist存储在一段连续的内存上,所以存储效率很高,但是插入和删除都需要频繁申请和释放内存;
redis3.2之后,采用的一种叫quicklist的数据结构来存储list,列表的底层都由quicklist实现。
quicklist仍然是一个双向链表,只是列表的每个节点都是一个ziplist,其实就是linkedlist和ziplist的结合,quicklist 中每个节点ziplist都能够存储多个数据元素,在源码中的文件为【quicklist.c】,在源码第一行中有解释为:A doubly linked list of ziplists意思为一个由ziplist组成的双向链表;
/* quicklist is a 32 byte struct (on 64-bit systems) describing a quicklist.
* 'count' is the number of total entries.
* 'len' is the number of quicklist nodes.
* 'compress' is: -1 if compression disabled, otherwise it's the number
* of quicklistNodes to leave uncompressed at ends of quicklist.
* 'fill' is the user-requested (or default) fill factor. */
typedef struct quicklist {
quicklistNode *head; ?/*指向头节点(左侧第一个节点)的指针。*/
quicklistNode *tail; /*指向尾节点(右侧第一个节点)的指针。*/
unsigned long count; /* quicklist所有entries in all ziplists*/
unsigned int len; /* number of quicklistNodes */
int fill : 16; /* ziplist大小设置,存放list-max-ziplist-size参数的值 */
unsigned int compress : 16; /* 节点压缩深度设置,存放list-compress-depth参数的值 */
}
typedef struct quicklistNode {
struct quicklistNode *prev; /*指向链表前一个节点的指针*/
struct quicklistNode *next; /*指向链表后一个节点的指针*/
unsigned char *zl;/*数据指针。如果当前节点的数据没有压缩,那么它指向一个ziplist结构;否则,它指向一个quicklistLZF结构。*/
unsigned int sz; /*表示zl指向的ziplist的总大小(包括zlbytes, zltail, zllen, zlend和各个数据项)。需要注意的是:如果ziplist被压缩了,那么这个sz的值仍然是压缩前的ziplist大小。/*
unsigned int count : 16; /* 表示ziplist里面包含的数据项个数。 */
unsigned int encoding : 2; /* RAW==1(未压缩) or LZF==2 (压缩了并采用LZF压缩算法)*/
unsigned int container : 2; /* 使用的容器 NONE==1 or ZIPLIST==2(默认值) */
unsigned int recompress : 1; /* 我们使用类似lindex这样的命令查看了某一项本来压缩的数据时,需要把数据暂时解压,这时就设置recompress=1做一个标记,等有机会再把数据重新压缩 */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* 其他扩展字段(未使用) */
} quicklistNode;
list可以干什么?
栈:后进先出 lpush lpop
队列:先进先出 lpush rpop
消息队列: lpush brpop
blpop key[key...] timeout lpop的阻塞版本,若给定列表中没有任何元素可供弹出时,链接会被blpop命令阻塞,直到等待超时(单位:秒)或发现可弹出元素时为止,若发现其中任何一个列表中有值则返回列表key和第一个元素的值。
hash类型
https://www.jianshu.com/p/7f53f5e683cf
hash提供了两种数据结构来存储,1种是hashtable,另一种是前面讲的ziplist,数据量小时候用ziplist,在redis中hash表分为三层,
dict--->dictht---->dictEntry
Redis定义了dictEntry(哈希表结点),dictType(字典类型函数),dictht(哈希表)和dict(字典)四个结构体来实现字典结构,下面来分别介绍这四个结构体。
//哈希表的table指向的数组存放这dictEntry类型的地址。定义在dict.h/dictEntryt中
typedef struct dictEntry {//字典的节点
void *key;
union {//使用的联合体 //因为value有多种类型,所以value用了union来存储
void *val;
uint64_t u64;//这两个参数很有用
int64_t s64;
} v;
struct dictEntry *next;//指向下一个hash节点,用来解决hash键冲突(collision)
} dictEntry;
//dictType类型保存着 操作字典不同类型key和value的方法 的指针
typedef struct dictType {
unsigned int (*hashFunction)(const void *key); //计算hash值的函数
void *(*keyDup)(void *privdata, const void *key); //复制key的函数
void *(*valDup)(void *privdata, const void *obj); //复制value的函数
int (*keyCompare)(void *privdata, const void *key1, const void *key2); //比较key的函数
void (*keyDestructor)(void *privdata, void *key); //销毁key的析构函数
void (*valDestructor)(void *privdata, void *obj); //销毁val的析构函数
} dictType;
//redis中哈希表定义dict.h/dictht
typedef struct dictht { //哈希表
dictEntry **table; //存放一个数组的地址,数组存放着哈希表节点dictEntry的地址。
unsigned long size; //哈希表table的大小,初始化大小为4
unsigned long sizemask; //用于将哈希值映射到table的位置索引。它的值总是等于(size-1)。
unsigned long used; //记录哈希表已有的节点(键值对)数量。
} dictht;
typedef struct dict {
dictType *type;//dictType里存放的是一堆工具函数的函数指针,
void *privdata;//保存type中的某些函数需要作为参数的数据
dictht ht[2];//两个dictht,ht[0]平时用,ht[1] rehash时用
long rehashidx; //当前rehash到buckets的哪个索引,-1时表示非rehash状态
int iterators; //安全迭代器的计数。
} dict;
从源码中可以看出,dict 结构内部包含两个 hashtable,通常情况下只有一个 hashtable 是有值的。
但是在 dict 扩容缩容时,需要分配新的 hashtable,然后进行渐进式搬迁,这时候两个 hashtable 存储的分别是旧的 hashtable 和新的 hashtable。待搬迁结束后,旧的 hashtable 被删除,新的 hashtable 取而代之。
字典数据结构的精华就落在了dictht所表示的 hashtable 结构上了。hashtable 的结构和 Java 的 HashMap 几乎是一样的,都是通过分桶的方式解决 hash 冲突。第一维是数组,第二维是链表。数组中存储的是第二维链表的第一个元素的指针。
Set 集合
集合类型中,每个元素都是不同的,也就是不能有重复数据,同时集合类型中的数据是无序的。一个集合类型键可以存储至多232-1个 。集合类型和列表类型的最大的区别是有序性和唯一性
集合类型的常用操作是向集合中加入或删除元素、判断某个元素是否存在。由于集合类型在redis内部是使用的值为空的散列表(hash table),所以这些操作的时间复杂度都是O(1).
数据结构
Set在的底层数据结构以intset或者hashtable来存储。当set中只包含整数型的元素时,采用intset来存储,否则, 采用hashtable存储,但是对于set来说,该hashtable的value值用于为NULL。通过key来存储元素
sorted-set
有序集合类型,顾名思义,和前面讲的集合类型的区别就是多了有序的功能
在集合类型的基础上,有序集合类型为集合中的每个元素都关联了一个分数,这使得我们不仅可以完成插入、删除 和判断元素是否存在等集合类型支持的操作,还能获得分数最高(或最低)的前N个元素、获得指定分数范围内的元 素等与分数有关的操作。虽然集合中每个元素都是不同的,但是他们的分数却可以相同
数据结构
zset类型的数据结构就比较复杂一点,内部是以ziplist或者skiplist+hashtable来实现,这里面最核心的一个结构就 是skiplist,也就是跳跃表
功能
1 可以为每个key设置超时时间
2 通过列表类型来实现分布式队列的操作
3 支持发布订阅的消息模式
应用场景
1 数据缓存
2 单点登录
3 抢购秒杀
4网站访问排名(通过有序集合)
5 应用的模块开发
启动停止 redis
/redis-server ../redis.conf
/redis-cli shutdown
以后台进程方式启动 ,修改redis.conf 中的 deamonize=yes
连接到Redis的命令
/redis-cli -h 127.0.0.1 6379
redis命令
redis-server 启动服务
redis-cli 访问redis控制台
redis-benchmark 性能测试的工具
redis-check-aof AOF文件进行检测的工具
redis-check-dump rdb文件检测的工具
redis-sentinel sentinel 服务器配置
多数据库支持
默认支持16个数据库;每个库可以理解为一个命名空间
跟关系型数据库的区别
1 redis不支持自定义数据库名称
2 每个库不能单独设置授权
3 每个数据库之间不是完全隔离的。可以通过flushall命令清空redis实例中的所有数据库中的数据
通过select dbid(0···15)选择不同的数据库命名空间
redis.conf 配置
bind 设置可以访问redis的ip白名单
# By default protected mode is enabled. You should disable it only if
# you are sure you want clients from other hosts to connect to Redis
# even if no authentication is configured, nor a specific set of interfaces
# are explicitly listed using the "bind" directive.
protected-mode no
# By default Redis does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
daemonize yes 设置redis server 后台运行
# Set the number of databases. The default database is DB 0, you can select
# a different one on a per-connection basis using SELECT <dbid> where
# dbid is a number between 0 and 'databases'-1
databases 16
# Master-Slave replication. Use slaveof to make a Redis instance a copy of
# another Redis server. A few things to understand ASAP about Redis replication.
#
# 1) Redis replication is asynchronous, but you can configure a master to
# stop accepting writes if it appears to be not connected with at least
# a given number of slaves.
# 2) Redis slaves are able to perform a partial resynchronization with the
# master if the replication link is lost for a relatively small amount of
# time. You may want to configure the replication backlog size (see the next
# sections of this file) with a sensible value depending on your needs.
# 3) Replication is automatic and does not need user intervention. After a
# network partition slaves automatically try to reconnect to masters
# and resynchronize with them.
#
# slaveof <masterip> <masterport>
# When a slave loses its connection with the master, or when the replication
# is still in progress, the slave can act in two different ways:
#
# 1) if slave-serve-stale-data is set to 'yes' (the default) the slave will
# still reply to client requests, possibly with out of date data, or the
# data set may just be empty if this is the first synchronization.
#
# 2) if slave-serve-stale-data is set to 'no' the slave will reply with
# an error "SYNC with master in progress" to all the kind of commands
# but to INFO and SLAVEOF.
#
slave-serve-stale-data yes
# Require clients to issue AUTH <PASSWORD> before processing any other
# commands. This might be useful in environments in which you do not trust
# others with access to the host running redis-server.
requirepass foobared 设置访问密码
字符类型
一个字符类型的key默认存储的最大容量是512M
赋值和取值
SET key value
GET key
递增数字
incr key
错误的演示
int value= get key;
value =value +1;
set key value;
key的设计
对象类型:对象id:对象属性:对象子属性
建议对key进行分类,同步在wiki统一管理
短信重发机制:sms:limit:mobile 138。。。。。 expire
incryby key increment 递增指定的整数
decr key 原子递减
append key value 向指定的key追加字符串
strlen key 获得key对应的value的长度
mget key key.. 同时获得多个key的value
mset key value key value key value …
setnx
列表类型
list, 可以存储一个有序的字符串列表
LPUSH/RPUSH: 从左边或者右边push数据
LPUSH/RPUSH key value value …
{17 20 19 18 16}
llen num 获得列表的长度
lrange key start stop ; 索引可以是负数, -1表示最右边的第一个元素
lrem key count value
lset key index value
LPOP/RPOP : 取数据
应用场景:可以用来做分布式消息队列
散列类型
hash key value 不支持数据类型的嵌套
比较适合存储对象
person
age 18
sex 男
name mic
..
hset key field value
hget key filed
hmset key filed value [filed value …] 一次性设置多个值
hmget key field field … 一次性获得多个值
hgetall key 获得hash的所有信息,包括key和value
hexists key field 判断字段是否存在。 存在返回1. 不存在返回0
hincryby
hsetnx
hdel key field [field …] 删除一个或者多个字段
集合类型
set 跟list 不一样的点。 集合类型不能存在重复的数据。而且是无序的
sadd key member [member ...] 增加数据; 如果value已经存在,则会忽略存在的值,并且返回成功加入的元素的数量
srem key member 删除元素
smembers key 获得所有数据
sdiff key key … 对多个集合执行差集运算
sunion 对多个集合执行并集操作, 同时存在在两个集合里的所有值
有序集合
zadd key score member
zrange key start stop [withscores] 去获得元素。 withscores是可以获得元素的分数
如果两个元素的score是相同的话,那么根据(0<9<A<Z<a<z) 方式从小到大
网站访问的前10名。
分布式锁的实现:
多进程架构出现的问题:1 资源共享竞争的问题。 2 数据的安全性问题
分布式锁的解决方案:
基本问题 怎么去获取锁 怎么去释放锁
数据库: 唯一索引的方式,方案1 如例:创建一个单独的表
lock(
id int(11)
methodName varchar(100),
memo varchar(1000)
modifyTime timestamp
unique key mn (methodName) --唯一约束
)
获取锁的伪代码
try{
exec insert into lock(methodName,memo) values(‘method’,’desc’); method
return true;
}Catch(DuplicateException e){
return false;
}
释放锁
delete from lock where methodName=’’;
方案2 加行锁 forUpdate
存在的需要思考的问题
- 锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁
- 锁是非阻塞的,数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作
- 锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁
改造方案
lock(
id int(11)
threadID int(11) --当前线程ID
methodName varchar(100),
memo varchar(1000)
modifyTime timestamp
unique key mn (methodName) --唯一约束
)
获取锁的伪代码
try{
exec insert into lock(threadID,methodName,memo) values(‘method’,’desc’); method
return true;
}Catch(DuplicateException e){
//根据代码当前线程id select 是否能获取该条数据 如果可以表示同一线程 可以重新获取锁
}
zookeeper实现分布式锁
利用zookeeper的唯一节点特性或者有序临时节点特性获得最小节点作为锁. zookeeper 的实现相对简单,通过curator客户端,已经对锁的操作进行了封装,原理如下
zookeeper的优势
1. 可靠性高、实现简单
2. zookeeper因为临时节点的特性,如果因为其他客户端因为异常和zookeeper连接中断了,那么节点会被删除,意味着锁会被自动释放
3. zookeeper本身提供了一套很好的集群方案,比较稳定
4. 释放锁操作,会有watch通知机制,也就是服务器端会主动发送消息给客户端这个锁已经被释放了
基于缓存的分布式锁实现
redis中有一个setNx命令,这个命令只有在key不存在的情况下为key设置值。所以可以利用这个特性来实现分布式锁的操作
具体实现代码
LUA脚本语言
Linux 系统上安装
Linux & Mac上安装 Lua 安装非常简单,只需要下载源码包并在终端解压编译即可,本文使用了5.3.0版本进行安装:
curl -R -O http://www.lua.org/ftp/lua-5.3.0.tar.gz
tar zxf lua-5.3.0.tar.gz
cd lua-5.3.0
make linux test
make install
如果报错,说找不到readline/readline.h, 可以通过yum命令安装
yum -y install readline-devel ncurses-devel
安装完以后再make linux / make install
最后,直接输入 lua命令即可进入lua的控制台
Redis与Lua
在Lua脚本中调用Redis命令,可以使用redis.call函数调用。比如我们调用string类型的命令
redis.call(‘set’,’hello’,’world’)
redis.call 函数的返回值就是redis命令的执行结果。前面我们介绍过redis的5中类型的数据返回的值的类型也都不一样。redis.call函数会将这5种类型的返回值转化对应的Lua的数据类型
从Lua脚本中获得返回值
在很多情况下我们都需要脚本可以有返回值,在脚本中可以使用return 语句将值返回给redis客户端,通过return语句来执行,如果没有执行return,默认返回为nil。
如何在redis中执行lua脚本
Redis提供了EVAL命令可以使开发者像调用其他Redis内置命令一样调用脚本。
[EVAL] [脚本内容] [key参数的数量] [key …] [arg …]
可以通过key和arg这两个参数向脚本中传递数据,他们的值可以在脚本中分别使用KEYS和ARGV 这两个类型的全局变量访问。比如我们通过脚本实现一个set命令,通过在redis客户端中调用,那么执行的语句是:
lua脚本的内容为: return redis.call(‘set’,KEYS[1],ARGV[1]) //KEYS和ARGV必须大写
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 hello world
EVAL命令是根据 key参数的数量-也就是上面例子中的1来将后面所有参数分别存入脚本中KEYS和ARGV两个表类型的全局变量。当脚本不需要任何参数时也不能省略这个参数。如果没有参数则为0
eval "return redis.call(‘get’,’hello’)" 0
EVALSHA命令
考虑到我们通过eval执行lua脚本,脚本比较长的情况下,每次调用脚本都需要把整个脚本传给redis,比较占用带宽。为了解决这个问题,redis提供了EVALSHA命令允许开发者通过脚本内容的SHA1摘要来执行脚本。该命令的用法和EVAL一样,只不过是将脚本内容替换成脚本内容的SHA1摘要
- Redis在执行EVAL命令时会计算脚本的SHA1摘要并记录在脚本缓存中
- 执行EVALSHA命令时Redis会根据提供的摘要从脚本缓存中查找对应的脚本内容,如果找到了就执行脚本,否则返回“NOSCRIPT No matching script,Please use EVAL”
通过以下案例来演示EVALSHA命令的效果
script load "return redis.call('get','hello')" 将脚本加入缓存并生成sha1命令
evalsha "a5a402e90df3eaeca2ff03d56d99982e05cf6574" 0
我们在调用eval命令之前,先执行evalsha命令,如果提示脚本不存在,则再调用eval命令
lua脚本实战
实现一个针对某个手机号的访问频次, 以下是lua脚本,保存为phone_limit.lua
local num=redis.call('incr',KEYS[1])
if tonumber(num)==1 then
redis.call('expire',KEYS[1],ARGV[1])
return 1
elseif tonumber(num)>tonumber(ARGV[2]) then
return 0
else
return 1
end
通过如下命令调用
./redis-cli --eval phone_limit.lua rate.limiting:13700000000 , 10 3
语法为 ./redis-cli –eval [lua脚本] [key…]空格,空格[args…]
脚本的原子性
redis的脚本执行是原子的,即脚本执行期间Redis不会执行其他命令。所有的命令必须等待脚本执行完以后才能执行。为了防止某个脚本执行时间过程导致Redis无法提供服务。Redis提供了lua-time-limit参数限制脚本的最长运行时间。默认是5秒钟。
当脚本运行时间超过这个限制后,Redis将开始接受其他命令但不会执行(以确保脚本的原子性),而是返回BUSY的错误
实践操作
打开两个客户端窗口
在第一个窗口中执行lua脚本的死循环
eval “while true do end” 0
在第二个窗口中运行get hello
最后第二个窗口的运行结果是Busy, 可以通过script kill命令终止正在执行的脚本。如果当前执行的lua脚本对redis的数据进行了修改,比如(set)操作,那么script kill命令没办法终止脚本的运行,因为要保证lua脚本的原子性。如果执行一部分终止了,就违背了这一个原则
在这种情况下,只能通过 shutdown nosave命令强行终止
代码中使用
public static void main(String[] args) throws Exception{
Jedis jedis=RedisManager.getJedis();
//对某个ip的频率进行限制 1分钟访问不大于5次
String lua="local num=redis.call('incr',KEYS[1])\n" +
"if tonumber(num)==1 then\n" +
" redis.call('expire',KEYS[1],ARGV[1])\n" +
" return 1\n" +
"elseif tonumber(num)>tonumber(ARGV[2]) then\n" +
" return 0\n" +
"else\n" +
" return 1\n" +
"end \n";
List<String> keys=new ArrayList<>();
keys.add("ip:limit:127.0.0.1");
List<String> argss=new ArrayList<>();
argss.add("6000");
argss.add("5");
String luaZhaiYao=jedis.scriptLoad(lua);
System.out.println(luaZhaiYao);
Object obj=jedis.evalsha(luaZhaiYao,keys,argss);
System.out.println(obj);
}
redis持久化机制
redis提供了两种持久化策略
RDB
RDB的持久化策略: 按照规则定时讲内从的数据同步到磁盘
snapshot
redis在指定的情况下会触发快照
1,自己配置的快照规则
save <seconds> <changes>
save 900 1 当在900秒内被更改的key的数量大于1的时候,就执行快照
save 300 10
save 60 10000
2,save或者bgsave
save: 执行内存的数据同步到磁盘的操作,这个操作会阻塞客户端的请求
bgsave: 在后台异步执行快照操作,这个操作不会阻塞客户端的请求
3,执行flushall的时候
清除内存的所有数据,只要快照的规则不为空,也就是第一个规则存在。那么redis会执行快照
4,执行复制的时候
快照的实现原理
1:redis使用fork函数复制一份当前进程的副本(子进程)
2:父进程继续接收并处理客户端发来的命令,而子进程开始将内存中的数据写入硬盘中的临时文件
3:当子进程写入完所有数据后会用该临时文件替换旧的RDB文件,至此,一次快照操作完成。
注意:redis在进行快照的过程中不会修改RDB文件,只有快照结束后才会将旧的文件替换成新的,也就是说任何时候RDB文件都是完整的。 这就使得我们可以通过定时备份RDB文件来实现redis数据库的备份, RDB文件是经过压缩的二进制文件,占用的空间会小于内存中的数据,更加利于传输。
RDB的优缺点
- 使用RDB方式实现持久化,一旦Redis异常退出,就会丢失最后一次快照以后更改的所有数据。这个时候我们就需要根据具体的应用场景,通过组合设置自动快照条件的方式来将可能发生的数据损失控制在能够接受范围。如果数据相对来说比较重要,希望将损失降到最小,则可以使用AOF方式进行持久化
- RDB可以最大化Redis的性能:父进程在保存RDB文件时唯一要做的就是fork出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无序执行任何磁盘I/O操作。同时这个也是一个缺点,如果数据集比较大的时候,fork可以能比较耗时,造成服务器在一段时间内停止处理客户端的请求;
AOF
AOF可以将Redis执行的每一条写命令追加到硬盘文件中,这一过程显然会降低Redis的性能,但大部分情况下这个影响是能够接受的,另外使用较快的硬盘可以提高AOF的性能
实践
默认情况下Redis没有开启AOF(append only file)方式的持久化,可以通过appendonly参数启用,在redis.conf中找到 appendonly yes
开启AOF持久化后每执行一条会更改Redis中的数据的命令后,Redis就会将该命令写入硬盘中的AOF文件。AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的,默认的文件名是apendonly.aof. 可以在redis.conf中的属性 appendfilename appendonlyh.aof修改
修改redis.conf中的appendonly yes ; 重启后执行对数据的变更命令, 会在bin目录下生成对应的.aof文件, aof文件中会记录所有的操作命令
如下两个参数可以去对aof文件做优化
auto-aof-rewrite-percentage 100 表示当前aof文件大小超过上一次aof文件大小的百分之多少的时候会进行重写。如果之前没有重写过,以启动时aof文件大小为准
auto-aof-rewrite-min-size 64mb 限制允许重写最小aof文件大小,也就是文件大小小于64mb的时候,不需要进行优化
aof重写的原理
Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 重写的流程是这样,主进程会fork一个子进程出来进行AOF重写,这个重写过程并不是基于原有的aof文件来做的,而是有点类似于快照的方式,全量遍历内存中的数据,然后逐个序列到aof文件中。在fork子进程这个过程 中,服务端仍然可以对外提供服务,那这个时候重写的aof文件的数据和redis内存数据不一致了怎么办?不用担 心,这个过程中,主进程的数据更新操作,会缓存到aof_rewrite_buf中,也就是单独开辟一块缓存来存储重写期间 收到的命令,当子进程重写完以后再把缓存中的数据追加到新的aof文件。
同步磁盘数据
redis每次更改数据的时候, aof机制都会讲命令记录到aof文件,但是实际上由于操作系统的缓存机制,数据并没有实时写入到硬盘,而是进入硬盘缓存。再通过硬盘缓存机制去刷新到保存到文件
# appendfsync always 每次执行写入都会进行同步 , 这个是最安全但是是效率比较低的方式
appendfsync everysec 每一秒执行
# appendfsync no 不主动进行同步操作,由操作系统去执行,这个是最快但是最不安全的方式
aof文件损坏以后如何修复
服务器可能在程序正在对 AOF 文件进行写入时停机, 如果停机造成了 AOF 文件出错(corrupt), 那么 Redis 在重启时会拒绝载入这个 AOF 文件, 从而确保数据的一致性不会被破坏。
当发生这种情况时, 可以用以下方法来修复出错的 AOF 文件:
- 为现有的 AOF 文件创建一个备份。
- 使用 Redis 附带的 redis-check-aof 程序,对原来的 AOF 文件进行修复。
redis-check-aof --fix
重启 Redis 服务器,等待服务器载入修复后的 AOF 文件,并进行数据恢复。
RDB 和 AOF ,如何选择
一般来说,如果对数据的安全性要求非常高的话,应该同时使用两种持久化功能。如果可以承受数分钟以内的数据丢失,那么可以只使用 RDB 持久化。有很多用户都只使用 AOF 持久化, 但并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快 。
两种持久化策略可以同时使用,也可以使用其中一种。如果同时使用的话, 那么Redis重启时,会优先使用AOF文件来还原数据
集群
master、slave 主从,读写分离
配置过程
修改11.140和11.141的redis.conf文件,增加slaveof masterip masterport
slaveof 192.168.11.138 6379
实现原理
- slave第一次或者重连到master上以后,会向master发送一个SYNC的命令
- master收到SYNC的时候,会做两件事
- 执行bgsave(rdb的快照文件)
- master会把新收到的修改命令存入到缓冲区
缺点
没有办法对master进行动态选举
复制的方式
- 基于rdb文件的复制(第一次连接或者重连的时候)
- 无硬盘复制
- 增量复制
PSYNC master run id. offset
哨兵机制
sentinel
- 监控master和salve是否正常运行
- 如果master出现故障,那么会把其中一台salve数据升级为master
集群(redis3.0以后的功能)
根据key的hash值取模 服务器的数量 。
hash
集群的原理
Redis Cluster中,Sharding采用slot(槽)的概念,一共分成16384个槽,这有点儿类似前面讲的pre sharding思路。对于每个进入Redis的键值对,根据key进行散列,分配到这16384个slot中的某一个中。使用的hash算法也比较简单,就是CRC16后16384取模。Redis集群中的每个node(节点)负责分摊这16384个slot中的一部分,也就是说,每个slot都对应一个node负责处理。当动态添加或减少node节点时,需要将16384个槽做个再分配,槽中的键值也要迁移。当然,这一过程,在目前实现中,还处于半自动状态,需要人工介入。Redis集群,要保证16384个槽对应的node都正常工作,如果某个node发生故障,那它负责的slots也就失效,整个集群将不能工作。为了增加集群的可访问性,官方推荐的方案是将node配置成主从结构,即一个master主节点,挂n个slave从节点。这时,如果主节点失效,Redis Cluster会根据选举算法从slave节点中选择一个上升为主节点,整个集群继续对外提供服务。这非常类似服务器节点通过Sentinel监控架构成主从结构,只是Redis Cluster本身提供了故障转移容错的能力。
slot(槽)的概念,在redis集群中一共会有16384个槽,
根据key 的CRC16算法,得到的结果再对16384进行取模。 假如有3个节点
node1 0 5460
node2 5461 10922
node3 10923 16383
节点新增
node4 0-1364,5461-6826,10923-12287
删除节点
先将节点的数据移动到其他节点上,然后才能执行删除
市面上提供了集群方案
- redis shardding 而且jedis客户端就支持shardding操作 SharddingJedis ; 增加和减少节点的问题; pre shardding
3台虚拟机 redis 。但是我部署了9个节点 。每一台部署3个redis增加cpu的利用率
9台虚拟机单独拆分到9台服务器
- codis基于redis2.8.13分支开发了一个codis-server
- twemproxy twitter提供的开源解决方案