Redis介绍
NoSql
NoSQL,即Not-Only SQL,泛指非关系型的数据库,它是为了解决高并发、高可用、高可扩展、大数据存储问题而产生的数据库解决方案,NoSQL可以作为关系型数据库的良好补充,但是不能替代关系型数据库
NoSql数据库分类
键值(Key-Value)存储数据库
相关产品: Tokyo Cabinet/Tyrant、Redis、Voldemort、Berkeley DB
典型应用: 内容缓存,主要用于处理大量数据的高访问负载
数据模型: 一系列键值对
优势: 快速查询
劣势: 存储的数据缺少结构化
列存储数据库
相关产品:Cassandra, HBase, Riak
典型应用:分布式的文件系统
数据模型:以列簇式存储,将同一列数据存在一起
优势:查找速度快,可扩展性强,更容易进行分布式扩展
劣势:功能相对局限
文档型数据库
相关产品:CouchDB、MongoDB
典型应用:Web应用(与Key-Value类似,Value是结构化的)
数据模型: 一系列键值对
优势:数据结构要求不严格
劣势:查询性能不高,而且缺乏统一的查询语法
图形(Graph)数据库
相关数据库:Neo4J、InfoGrid、Infinite Graph
典型应用:社交网络
数据模型:图结构
优势:利用图结构相关算法
劣势:需要对整个图做计算才能得出结果,不容易做分布式的集群方案
Redis
Redis是用C语言开发的一个开源的高性能键值对(key-value)数据库。它通过提供多种键值数据类型来适应不同场景下的存储需求,目前为止Redis支持的键值数据类型如下:
字符串类型
散列类型
列表类型
集合类型
有序集合类型
Redis历史发展
2008年,意大利的一家创业公司Merzia推出了一款基于MySQL的网站实时统计系统LLOOGG,然而没过多久该公司的创始人 Salvatore Sanfilippo便 对MySQL的性能感到失望,于是他决定亲自为LLOOGG量身定做一个数据库,并于2009年开发完成,这个数据库就是Redis。 不过Salvatore Sanfilippo并不满足只将Redis用于LLOOGG这一款产品,而是希望更多的人使用它,于是在同一年Salvatore Sanfilippo将Redis开源发布,并开始和Redis的另一名主要的代码贡献者Pieter Noordhuis一起继续着Redis的开发,直到今天。
Salvatore Sanfilippo自己也没有想到,短短的几年时间,Redis就拥有了庞大的用户群体。Hacker News在2012年发布了一份数据库的使用情况调查,结果显示有近12%的公司在使用Redis。国内如新浪微博、街旁网、知乎网,国外如GitHub、Stack Overflow、Flickr等都是Redis的用户。
VMware公司从2010年开始赞助Redis的开发, Salvatore Sanfilippo和Pieter Noordhuis也分别在3月和5月加入VMware,全职开发Redis。
Redis的应用场景
缓存(数据查询、短连接、新闻内容、商品内容等等)(最多使用)
分布式集群架构中的session分离
聊天室的在线好友列表
任务队列(秒杀、抢购、12306等等)
应用排行榜
网站访问统
数据过期处理(可以精确到毫秒)
Redis单机版安装配置
Redis下载
官网地址:http://redis.io/
中文官网地址:http://www.redis.cn/
下载地址:http://download.redis.io/releases/
Redis安装
在VMware中安装CentOS
安装C语言需要的GCC环境
yum install gcc-c++
解压缩Redis源码压缩包
tar -zxvf redis-3.2.9.tar.gz
编译Redis源码,进入redis-3.2.9目录,执行编译命令make
安装Redis,需要通过PREFIX指定安装路径
make install PREFIX=/usr/local/redis
Redis启动
前端启动
启动命令:redis-server,直接运行bin/redis-server将以前端模式启动
./redis-server
关闭命令:ctrl+c
启动缺点:客户端窗口关闭则redis-server程序结束
后端启动(守护进程启动)
拷贝redis-3.2.9/redis.conf配置文件到Redis安装目录的bin目录
cp redis.conf /usr/local/redis/bin/
修改redis.conf,将daemonize由no改为yes
vim redis.conf
执行命令
./redis-server redis.conf
后端启动的关闭方式
正常关闭:
./redis-cli shutdown
其他命令说明
redis-server :启动redis服务
redis-cli :进入redis命令客户端
redis-benchmark: 性能测试的工具
redis-check-aof : aof文件进行检查的工具
redis-check-dump : rdb文件进行检查的工具
redis-sentinel : 启动哨兵监控服务
Redis客户端
命令格式:
./redis-cli -h 127.0.0.1 -p 6379
修改redis配置文件(解决IP绑定问题)
# bind 127.0.0.1 绑定的IP才能访问redis服务器,注释掉该配置
protected-mode yes 是否开启保护模式,由yes该为no
参数说明:
-h:redis服务器的ip地址
-p:redis实例的端口号
默认方式
如果不指定主机和端口也可以
./redis-cli
默认主机地址是127.0.0.1
默认端口是6379
连接超时解决
远程连接redis服务,需要关闭或者修改防火墙配置
修改防火墙设置:
编辑iptables
vim /etc/sysconfig/iptables
在命令模式下,选定要复制的那一行的末尾,然后点击键盘yyp,就完成复制,然后修改
重启防火墙
service iptables restart
iptables:清除防火墙规则: [确定]
iptables:将链设置为政策 ACCEPT:filter [确定]
iptables:正在卸载模块: [确定]
iptables:应用防火墙规则: [确定]
多数据库支持
默认一共是16个数据库,每个数据库之间是相互隔离(但是可以使用flushall一次清空所有的库)。数据库的数量是在redis.conf中配置的
切换数据库使用命令:select数据库编号(0-15)
程序客户端之Java客户端Jedis
Jedis介绍
Redis不仅使用命令客户端来操作,而且可以使用程序客户端操作
现在基本上主流的语言都有客户端支持,比如Java、C、C#、C++、php、Node.js、Go等
在官方网站里列一些Java的客户端,有Jedis、Redisson、Jredis、JDBC-Redis、等其中官方推荐使用Jedis和Redisson
在企业中用的最多的就是Jedis
Jedis同样也是托管在github上,地址:https://github.com/xetorthio/jedis
添加jar包
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
单实例连接
注意事项:需要去设置redis服务器的防火墙策略(临时关闭、永久关闭、端口暴露)
@Test
public void testJedis() {
//创建一个Jedis的连接
Jedis jedis = new Jedis("192.168.148.129", 6379);
//执行redis命令
jedis.set("key1", "hello world");
//从redis中取值
String result = jedis.get("key1");
//打印结果
System.out.println(result);
//关闭连接
jedis.close();
}
连接池连接
@Test
public void testJedisPool() {
//创建一连接池对象
JedisPool jedisPool = new JedisPool("192.168.148.129", 6379);
//从连接池中获得连接
Jedis jedis = jedisPool.getResource();
String result = jedis.get("key1") ;
System.out.println(result);
//关闭连接
jedis.close();
//关闭连接池
jedisPool.close();
}
Spring整合JedisPool
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd ">
<!-- 连接池配置 -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!-- 最大连接数 -->
<property name="maxTotal" value="30" />
<!-- 最大空闲连接数 -->
<property name="maxIdle" value="10" />
<!-- 每次释放连接的最大数目 -->
<property name="numTestsPerEvictionRun" value="1024" />
<!-- 释放连接的扫描间隔(毫秒) -->
<property name="timeBetweenEvictionRunsMillis" value="30000" />
<!-- 连接最小空闲时间 -->
<property name="minEvictableIdleTimeMillis" value="1800000" />
<!-- 连接空闲多久后释放, 当空闲时间>该值 且 空闲连接>最大空闲连接数 时直接释放 -->
<property name="softMinEvictableIdleTimeMillis" value="10000" />
<!-- 获取连接时的最大等待毫秒数,小于零:阻塞不确定的时间,默认-1 -->
<property name="maxWaitMillis" value="1500" />
<!-- 在获取连接的时候检查有效性, 默认false -->
<property name="testOnBorrow" value="false" />
<!-- 在空闲时检查有效性, 默认false -->
<property name="testWhileIdle" value="true" />
<!-- 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true -->
<property name="blockWhenExhausted" value="false" />
</bean>
<!-- redis单机 通过连接池 -->
<bean id="jedisPool" class="redis.clients.jedis.JedisPool"
destroy-method="close">
<constructor-arg name="poolConfig" ref="jedisPoolConfig" />
<constructor-arg name="host" value="192.168.242.130" />
<constructor-arg name="port" value="6379" />
</bean>
</beans>
Redis数据类型
官方命令大全网址:http://www.redis.cn/commands.html
Redis中存储数据是通过key-value格式存储数据的,其中value可以定义五种数据类型:
String(字符类型)
Hash(散列类型)
List(列表类型)
Set(集合类型)
SortedSet(有序集合类型,简称zset)
注意:在redis中的命令语句中,命令是忽略大小写的,而key是不忽略大小写的
String类型
命令
赋值
语法:SET key value
取值
语法:GET key
取值并赋值
语法:GETSET key value
数值增减
注意实现:
当value为整数数据时,才能使用以下命令操作数值的增减
数值递增都是原子操作
递增数字
语法:INCR key
增加指定的整数
语法:INCRBY key increment
递减数值
语法:DECR key
减少指定的整数
语法:DECRBY key decrement
仅当不存在时赋值
使用该命令可以实现分布式锁的功能
语法:setnx key value
其他命令
向尾部追加值
APPEND命令,向键值的末尾追加value。如果键不存在则将该键的值设置为value,即相当于 SET key value。返回值是追加后字符串的总长度
语法:APPEND key value
获取字符串长度
STRLEN命令,返回键值的长度,如果键不存在则返回0
语法:STRLEN key
同时设置/获取很多值
语法:
MSET key value [key value …]
MGET key [key …]
Hash类型
hash叫散列类型,它提供了字段和字段值的映射。字段值只能是字符串类型,不支持散列类型、集合类型等其它类型。如下:
命令
赋值
HSET命令不区分插入和更新操作,当执行插入操作时HSET命令返回1,当执行更新操作时返回0
一次只能设置一个字段值
语法:HSET key field value
一次可以设置多个字段值
语法:HMSET key field value [field value ...]
当字段不存在时赋值,类似HSET,区别在于如果字段存在,该命令不执行任何操作
语法:HSETNX key field value
取值
一次只能获取一个字段值
语法:HGET key field
192.168.148.129:6379> hget user username
"lisi"
一次可以获取多个字段值
语法:HMGET key field [field ...]
获取所有字段值
语法:HGETALL key
删除字段
可以删除一个或多个字段,返回值是被删除的字段个数
语法:HDEL key field [field ...]
增加数字
语法:HINCRBY key field increment
判断字段是否存在
语法:HEXISTS key field
只获取字段名或字段值
语法:
HKEYS key
HVALS key
获取字段数量
语法:HLEN key
获取所有字段
作用:获得hash的所有信息,包括key和value
语法:hgetall key
List类型
ArrayList与LinkedList的区别
ArrayList使用数组方式存储数据,所以根据索引查询数据速度快,而新增或者删除元素时需要设计到位移操作,所以比较慢
LinkedList使用双向链表方式存储数据,每个元素都记录前后元素的指针,所以插入、删除数据时只是更改前后元素的指针指向即可,速度非常快。然后通过下标查询元素时需要从头开始索引,所以比较慢,但是如果查询前几个元素或后几个元素速度比较快
list
Redis的列表类型(list)可以存储一个有序的字符串列表,常用的操作是向列表两端添加元素,或者获得列表的某一个片段
列表类型内部是使用双向链表(double linked list)实现的,所以向列表两端添加元素的时间复杂度为0(1),获取越接近两端的元素速度就越快。这意味着即使是一个有几千万个元素的列表,获取头部或尾部的10条记录也是极快的
命令
向列表两端增加元素
语法:LPUSH key value [value ...]
向列表右边增加元素
语法:RPUSH key value [value ...]
查看列表
语法:LRANGE key start stop
LRANGE命令是列表类型最常用的命令之一,获取列表中的某一片段,将返回start、stop之间的所有元素(包含两端的元素),索引从0开始。索引可以是负数,如:“-1”代表最后边的一个元素
从列表两端弹出元素
LPOP命令从列表左边弹出一个元素,会分两步完成:
第一步是将列表左边的元素从列表中移除
第二步是返回被移除的元素值
语法:
LPOP key
RPOP key
获取列表中元素个数
语法:LLEN key
删除列表指定个数的值
LREM命令会删除列表中前count个值为value的元素,返回实际删除的元素个数。根据count值的不同,该命令的执行方式会有所不同:
当count>0时, LREM会从列表左边开始删除
当count<0时, LREM会从列表后边开始删除
当count=0时, LREM删除所有值为value的元素
语法:LREM key count value
获得/设置指定索引的元素值
获得指定索引的元素值
语法:LINDEX key index
设置指定索引元素的值
语法:LSET key index value
只保留列表指定片段
指定范围和LRANGE一致
语法:LTRIM key start stop
向列表中插入元素
该命令首先会在列表中从左到右查找值为pivot的元素,然后根据第二个参数是BEFORE还是AFTER来决定将value插入到该元素的前面还是后面
语法:LINSERT key BEFORE|AFTER pivot value
将元素从一个列表转移到另一个列表中
语法:RPOPLPUSH source destination
Set类型
set类型即集合类型,其中的数据是不重复且没有顺序
集合类型和列表类型的对比
集合类型的常用操作是向集合中加入或删除元素、判断某个元素是否存在等,由于集合类型的Redis内部是使用值为空的散列表实现,所有这些操作的时间复杂度都为0(1)
Redis还提供了多个集合之间的交集、并集、差集的运算
命令
增加/删除元素
语法:SADD key member [member ...]
语法:SREM key member [member ...]
获得集合中的所有元素
语法:SMEMBERS key
判断元素是否在集合中
语法:SISMEMBER key member
集合运算命令
集合的差集运算 A-B
语法:SDIFF key [key ...]
集合的交集运算 A ∩ B
语法:SINTER key [key ...]
集合的并集运算 A ∪ B
属于A或者属于B的元素构成的集合
语法:SUNION key [key ...]
获得集合中元素
语法:SCARD key
从集合中弹出一个元素
注意:由于集合是无序的,所有SPOP命令会从集合中随机选择一个元素弹出
语法:SPOP key
Zset类型 (sortedset)
在集合类型的基础上,有序集合类型为集合中的每个元素都关联一个分数,这不仅可以完成插入、删除和判断元素是否存在在集合中,还能够获得分数最高或最低的前N个元素、获取指定分数范围内的元素等与分数有关的操作
在某些方面有序集合和列表类型有些相似
二者都是有序的
二者都可以获得某一范围的元素
但是,二者有着很大区别:
列表类型是通过链表实现的,获取靠近两端的数据速度极快,而当元素增多后,访问中间数据的速度会变慢
有序集合类型使用散列表实现,所有即使读取位于中间部分的数据也很快
列表中不能简单的调整某个元素的位置,但是有序集合可以(通过更改分数实现)
有序集合要比列表类型更耗内存
命令
增加元素
向有序集合中加入一个元素和该元素的分数,如果该元素已经存在则会用新的分数替换原有的分数。返回值是新加入到集合中的元素个数,不包含之前已经存在的元素
语法:ZADD key score member [score member ...]
获得排名在某个范围的元素列表
获得排名在某个范围的元素列表
按照元素分数从小到大的顺序返回索引从start到stop之间的所有元素(包含两端的元素)
语法:ZRANGE key start stop [WITHSCORES]
按照元素分数从大到小的顺序返回索引从start到stop之间的所有元素(包含两端的元素)
语法:ZREVRANGE key start stop [WITHSCORES]
如果需要获得元素的分数的可以在命令尾部加上WITHSCORES参数
获取元素分数
语法:ZSCORE key member
删除分数
移除有序集key中的一个或多个成员,不存在的成员将被忽略
当key存在但不是有序集类型时,返回一个错误
语法:ZREM key member [member ...]
获取指定分数范围的元素
语法:ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
增加某个元素分数
返回值是更改后的分数
语法:ZINCRBY key increment member
获得集合中元素数量
语法:ZCARD key
获取指定分数范围的元素个数
语法:ZCOUNT key min max
按照排名范围删除元素
语法:ZREMRANGEBYRANK key start stop
按照分数范围删除元素
语法:ZREMRANGEBYSCORE key min max
获取元素排名
从小到大
语法:ZRANK key member
从大到小
语法:ZREVRANK key member
通用命令
keys
返回满足给定pattern 的所有key
语法:keys pattern
del
语法:DEL key
exists
作用:确认一个key 是否存在
语法:exists key
expire
Redis在实际使用过程中更多的用作缓存,然而缓存的数据一般都是需要设置生存时间的,即:到期后数据销毁
EXPIRE key seconds 设置key的生存时间(单位:秒)key在多少秒后会自动删除
TTL key 查看key生于的生存时间
PERSIST key 清除生存时间
PEXPIRE key milliseconds 生存时间设置单位为:毫秒
rename
作用:重命名key
语法:rename oldkey newkey
type
作用:显示指定key的数据类型
语法:type key
Redis事务
Redis的事务是通过MULTI,EXEC,DISCARD和WATCH这四个命令来完成的
Redis的单个命令都是原子性的,所以这里确保事务性的对象是命令集合
Redis将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行
Redis不支持回滚操作
相关命令
MULTI
用于标记事务块的开始。
Redis会将后续的命令逐个放入队列中,然后使用EXEC命令原子化地执行这个命令序列。
语法:multi
EXEC
在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态
语法:exec
DISCARD
清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态
语法:discard
WATCH
当某个事务需要按条件执行时,就要使用这个命令将给定的键设置为受监控的状态
语法:watch key [key…]
注意事项:使用该命令可以实现redis的乐观锁
UNWATCH
清除所有先前为一个事务监控的键
语法:unwatch
事务失败处理
Redis语法错误(可以理解为编译期错误)
Redis类型错误(可以理解为运行期错误)
Redis不支持事务回滚
redis不支持事务回滚原因
大多数事务失败是因为语法错误或者类型错误,这两种错误,在开发阶段都是可以预见的
redis为了性能方面就忽略了事务回滚
Redis实现分布式锁
获取锁
方式1(使用set命令实现) --推荐:
/**
* 使用redis的set命令实现获取分布式锁
* @param lockKey 可以就是锁
* @param requestId 请求ID,保证同一性
* @param expireTime 过期时间,避免死锁
* @return
*/
public static boolean getLock(String lockKey,String requestId,int expireTime) {
//NX:保证互斥性
String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);
if("OK".equals(result)) {
return true;
}
return false;
}
方式2(使用setnx命令实现):
public static boolean getLock(String lockKey,String requestId,int expireTime) {
Long result = jedis.setnx(lockKey, requestId);
if(result == 1) {
jedis.expire(lockKey, expireTime);
return true;
}
return false;
}
释放锁
方式1(del命令实现):
/**
* 释放分布式锁
* @param lockKey
* @param requestId
*/
public static void releaseLock(String lockKey,String requestId) {
if (requestId.equals(jedis.get(lockKey))) {
jedis.del(lockKey);
}
}
方式2(redis+lua脚本实现)--推荐:
public static boolean releaseLock(String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (result.equals(1L)) {
return true;
}
return false;
}
Redis持久化方案
Redis是一个内存数据库,为了保证数据的持久性,它提供了两种持久化方案
RDB方式(默认)
AOF方式
RDB方式
RDB是Redis默认采用的持久化方式。
RDB方式是通过快照(snapshotting)完成的,当符合一定条件时Redis会自动将内存中的数据进行快照并持久化到硬盘。
Redis会在指定的情况下触发快照
符合自定义配置的快照规则
执行save或者bgsave命令
执行flushall命令
执行主从复制操作
在redis.conf中设置自定义快照规则
1.RDB持久化条件
格式:save <seconds> <changes>
示例:
save 900 1 : 表示15分钟(900秒钟)内至少1个键被更改则进行快照。
save 300 10 : 表示5分钟(300秒)内至少10个键被更改则进行快照。
save 60 10000 :表示1分钟内至少10000个键被更改则进行快照。
可以配置多个条件(每行配置一个条件),每个条件之间是“或”的关系
2.配置dir指定rdb快照文件的位置
# Note that you must specify a directory here, not a file name.
dir ./
3.配置dbfilename指定rdb快照文件的名称
# The filename where to dump the DB
dbfilename dump.rdb
特别说明:
Redis启动后会读取RDB快照文件,将数据从硬盘载入到内存
根据数据量大小与结构和服务器性能不同,这个时间也不同。通常将记录一千万个字符串类型键、大小为1GB的快照文件载入到内存中需要花费20~30秒钟
快照的实现原理
快照过程
1.redis使用fork函数复制一份当前进程的副本(子进程)
2.父进程继续接收并处理客户端发来的命令,而子进程开始将内存中的数据写入硬盘中的临时文件
3.当子进程写入完所有数据后会用该临时文件替换旧的RDB文件,至此,一次快照操作完成
注意事项
1.redis在进行快照的过程中不会修改RDB文件,只有快照结束后才会将旧的文件替换成新的,也就是说任何时候RDB文件都是完整的
2.这就使得我们可以通过定时备份RDB文件来实现redis数据库的备份, RDB文件是经过压缩的二进制文件,占用的空间会小于内存中的数据,更加利于传输
RDB优缺点
缺点:使用RDB方式实现持久化,一旦Redis异常退出,就会丢失最后一次快照以后更改的所有数据。这个时候我们就需要根据具体的应用场景,通过组合设置自动快照条件的方式来将可能发生的数据损失控制在能够接受范围。如果数据相对来说比较重要,希望将损失降到最小,则可以使用AOF方式进行持久化
优点: RDB可以最大化Redis的性能:父进程在保存RDB文件时唯一要做的就是fork出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无序执行任何磁盘I/O操作。同时这个也是一个缺点,如果数据集比较大的时候,fork可以能比较耗时,造成服务器在一段时间内停止处理客户端的请求
AOF方式
默认情况下Redis没有开启AOF(append only file)方式的持久化
开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件,这一过程显然会降低Redis的性能,但大部分情况下这个影响是能够接受的,另外使用较快的硬盘可以提高AOF的性能
可以通过修改redis.conf配置文件中的appendonly参数开启
appendonly yes
AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的
dir ./
默认的文件名是appendonly.aof,可以通过appendfilename参数修改:
appendfilename appendonly.aof
AOF重写原理(优化AOF文件)
Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写
重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合
整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作
AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松
参数说明
# auto-aof-rewrite-percentage 100 表示当前aof文件大小超过上一次aof文件大小的百分之多少的时候会进行重写。如果之前没有重写过,以启动时aof文件大小为准
# auto-aof-rewrite-min-size 64mb 限制允许重写最小aof文件大小,也就是文件大小小于64mb的时候,不需要进行优化
同步磁盘数据
Redis每次更改数据的时候, aof机制都会将命令记录到aof文件,但是实际上由于操作系统的缓存机制,数据并没有实时写入到硬盘,而是进入硬盘缓存。再通过硬盘缓存机制去刷新到保存到文件
参数说明:
appendfsync always 每次执行写入都会进行同步 , 这个是最安全但是是效率比较低的方式
appendfsync everysec 每一秒执行
appendfsync no 不主动进行同步操作,由操作系统去执行,这个是最快但是最不安全的方式
AOF文件损坏以后如何修复
服务器可能在程序正在对 AOF 文件进行写入时停机, 如果停机造成了 AOF 文件出错(corrupt), 那么 Redis 在重启时会拒绝载入这个 AOF 文件, 从而确保数据的一致性不会被破坏
当发生这种情况时, 可以用以下方法来修复出错的 AOF 文件:
1.为现有的 AOF 文件创建一个备份
2.使用 Redis 附带的 redis-check-aof 程序,对原来的 AOF 文件进行修复
redis-check-aof --fix readonly.aof
3.重启 Redis 服务器,等待服务器载入修复后的 AOF 文件,并进行数据恢复
如何选择RDB和AOF
一般来说,如果对数据的安全性要求非常高的话,应该同时使用两种持久化功能
如果可以承受数分钟以内的数据丢失,那么可以只使用 RDB 持久化
有很多用户都只使用 AOF 持久化, 但并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快
两种持久化策略可以同时使用,也可以使用其中一种。如果同时使用的话, 那么Redis重启时,会优先使用AOF文件来还原数据
Redis的主从复制
持久化保证了即使redis服务重启也不会丢失数据,因为redis服务重启后会将硬盘上持久化的数据恢复到内存中,但是当redis服务器的硬盘损坏了可能会导致数据丢失,不过通过redis的主从复制机制就可以避免这种单点故障,如下图:
说明:
主redis中的数据有两个副本(replication)即从redis1和从redis2,即使一台redis服务器宕机其它两台redis服务也可以继续提供服务
主redis中的数据和从redis上的数据保持实时同步,当主redis写入数据时通过主从复制机制会复制到两个从redis服务上
只有一个主redis,可以有多个从redis
主从复制不会阻塞master,在同步数据时,master 可以继续处理client 请求
一个redis可以即是主又是从,如下图:
主从配置
主redis配置
无需特殊配置
从redis配置
修改从服务器上的redis.conf文件
# slaveof <masterip> <masterport>
slaveof 192.168.148.129 6379
实现原理
redis的主从同步,分为全量同步和增量同步
只有从机第一次连接上主机是全量同步
断线重连有可能触发全量同步也有可能是增量同步(master判断runid是否一致)
除此之外的情况都是增量同步
全量同步
Redis的全量同步过程主要分三个阶段:
同步快照阶段:Master创建并发送快照给Slave,Slave载入并解析快照。Master同时将此阶段所产生的新的写命令存储到缓冲区
同步写缓冲阶段:Master向Slave同步存储在缓冲区的写操作命令
同步增量阶段:Master向Slave同步写操作命令
增量同步
Redis增量同步主要指Slave完成初始化后开始正常工作时,Master发生的写操作同步到Slave的过程
通常情况下,Master每执行一个写命令就会向Slave发送相同的写命令,然后Slave接收并执行
Redis Sentinel哨兵机制
Redis主从复制的缺点:没有办法对master进行动态选举,需要使用Sentinel机制完成动态选举
Sentinel(哨兵)进程是用于监控redis集群中Master主服务器工作的状态
在Master主服务器发生故障的时候,可以实现Master和Slave服务器的切换,保证系统的高可用(HA)
其已经被集成在redis2.6+的版本中,Redis的哨兵模式到了2.8版本之后就稳定了下来
哨兵进程的作用
监控(Monitoring): 哨兵(sentinel) 会不断地检查你的Master和Slave是否运作正常
提醒(Notification): 当被监控的某个Redis节点出现问题时, 哨兵(sentinel) 可以通过 API 向管理员或者其他应用程序发送通知
自动故障迁移(Automatic failover):当一个Master不能正常工作时,哨兵(sentinel) 会开始一次自动故障迁移操作
它会将失效Master的其中一个Slave升级为新的Master, 并让失效Master的其他Slave改为复制新的Master
当客户端试图连接失效的Master时,集群也会向客户端返回新Master的地址,使得集群可以使用现在的Master替换失效Master
Master和Slave服务器切换后,Master的redis.conf、Slave的redis.conf和sentinel.conf的配置文件的内容都会发生相应的改变,即,Master主服务器的redis.conf配置文件中会多一行slaveof的配置,sentinel.conf的监控目标会随之调换
哨兵进程的工作方式
每个Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的Master主服务器,Slave从服务器以及其他Sentinel(哨兵)进程发送一个 PING 命令
如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel(哨兵)进程标记为主观下线(SDOWN)
如果一个Master主服务器被标记为主观下线(SDOWN),则正在监视这个Master主服务器的所有 Sentinel(哨兵)进程要以每秒一次的频率确认Master主服务器的确进入了主观下线状态
当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认Master主服务器进入了主观下线状态(SDOWN), 则Master主服务器会被标记为客观下线(ODOWN)
在一般情况下, 每个 Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有Master主服务器、Slave从服务器发送 INFO 命令
当Master主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master主服务器的所有 Slave从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次
若没有足够数量的 Sentinel(哨兵)进程同意 Master主服务器下线, Master主服务器的客观下线状态就会被移除。若 Master主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master主服务器的主观下线状态就会被移除
案例演示
修改从机的sentinel.conf
#sentinel monitor <master-name> <master ip> <master port> <quorum>
sentinel monitor mymaster 192.168.148.129 6379 1
其他配置项说明
# Example sentinel.conf
# 哨兵sentinel实例运行的端口 默认26379
port 26379
# 哨兵sentinel的工作目录
dir /tmp
# 哨兵sentinel监控的redis主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2
# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
这个数字越小,完成failover所需的时间就越长,
但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1
# 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
#1. 同一个sentinel对同一个master两次failover之间的间隔时间。
#2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
# 默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,
这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,
一个是事件的类型,
一个是事件的描述。
如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前<state>总是“failover”,
# <role>是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
通过redis-sentinel启动哨兵服务
./redis-sentinel sentinel.conf
Redis Cluster集群
redis3.0以后推出的redis cluster 集群方案,redis cluster集群保证了高可用、高性能、高可扩展性
redis-cluster架构图
架构细节:
(1)所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.
(2)节点的fail是通过集群中超过半数的节点检测失效时才生效.
(3)客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
(4)redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->value
Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点
示例如下:
Redis-cluster 投票:容错
(1)节点失效判断:集群中所有master参与投票,如果半数以上master节点与其中一个master节点通信超过(cluster-node-timeout),认为该master节点挂掉.
(2)集群失效判断:什么时候整个集群不可用(cluster_state:fail)?
如果集群任意master挂掉,且当前master没有slave,则集群进入fail状态。也可以理解成集群的[0-16383]slot映射不完全时进入fail状态。
如果集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态
安装Ruby环境
redis集群需要使用集群管理脚本redis-trib.rb,它的执行相应依赖ruby环境
安装ruby
yum install ruby
yum install rubygems
安装ruby和redis的接口程序redis-3.2.2.gem
gem install redis -V 3.2.2
复制redis-3.2.9/src/redis-trib.rb文件到/usr/local/redis目录
cp redis-3.2.9/src/redis-trib.rb /usr/local/redis-cluster/ -r
安装Redis集群(RedisCluster)
Redis集群最少需要三台主服务器,三台从服务器。
端口号分别为:7001~7006
第一步:创建7001实例,并编辑redis.conf文件,修改port为7001。
注意:创建实例,即拷贝单机版安装时,生成的bin目录,为7001目录
第二步:修改redis.conf配置文件,打开Cluster-enable yes
第三步:复制7001,创建7002~7006实例,注意端口修改
第四步:启动所有的实例
第五步:创建Redis集群
./redis-trib.rb create --replicas 1 192.168.148.129:7001 192.168.148.129:7002 192.168.148.129:7003 192.168.148.129:7004 192.168.148.129:7005 192.168.148.129:7006
命令客户端连接集群
命令:./redis-cli –p 7001 –c
注意:-c 表示是以redis集群方式进行连接
查看集群命令
查看集群状态
查看集群中的节点
维护节点
集群创建成功后可以继续向集群中添加节点
添加主节点
先创建7007节点
添加7007结点作为新节点
执行命令:./redis-trib.rb add-node 127.0.0.1:7007 127.0.0.1:7001
查看集群结点发现7007已添加到集群中
hash槽重新分配(数据迁移)
redis集群有16384个槽,集群中的每个结点分配自已槽,通过查看集群结点可以看到槽占用情况
给刚添加的7007结点分配槽
连接上集群(连接集群中任意一个可用结点都行)
./redis-trib.rb reshard 192.168.148.129:7001
输入要分配的槽数量
输入:3000,表示要给目标节点分配3000个槽
输入接收槽的结点id
80ce0a4f795e34d8c3bb3eae9f2f6740eb4a1f5e
输入源结点id
输入:all
输入yes开始移动槽到目标结点id
输入:yes
添加从节点
添加7008从结点,将7008作为7007的从结点
命令:./redis-trib.rb add-node --slave --master-id 主节点id 新节点的ip和端口 旧节点ip和端口(集群中任一节点都可以)
执行如下命令:
./redis-trib.rb add-node --slave --master-id 80ce0a4f795e34d8c3bb3eae9f2f6740eb4a1f5e 192.168.148.129:7008 192.168.148.129:7001
80ce0a4f795e34d8c3bb3eae9f2f6740eb4a1f5e是7007结点的id
注意:如果原来该结点在集群中的配置信息已经生成到cluster-config-file指定的配置文件中(如果cluster-config-file没有指定则默认为nodes.conf),这时可能会报错:
[ERR] Node XXXXXX is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0\
|
解决方法是删除生成的配置文件nodes.conf,删除后再执行./redis-trib.rb add-node指令
查看集群中的结点,刚添加的7008为7007的从节点:
删除节点
命令:./redis-trib.rb del-node 127.0.0.1:7005 419e09e183e8ac89d686ffa54c18e7d6b83a5883
删除已经占有hash槽的结点会失败,报错如下:
[ERR] Node 127.0.0.1:7005 is not empty! Reshard data away and try again
需要将该结点占用的hash槽分配出去
Jedis连接集群
需要开启防火墙,或者直接关闭防火墙
创建JedisCluster类连接redis集群
@Test
public void testJedisCluster() throws Exception {
//创建一连接,JedisCluster对象,在系统中是单例存在
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("192.168.148.129", 7001));
nodes.add(new HostAndPort("192.168.148.129", 7002));
nodes.add(new HostAndPort("192.168.148.129", 7003));
nodes.add(new HostAndPort("192.168.148.129", 7004));
nodes.add(new HostAndPort("192.168.148.129", 7005));
nodes.add(new HostAndPort("192.168.148.129", 7006));
JedisCluster cluster = new JedisCluster(nodes);
//执行JedisCluster对象中的方法,方法和redis一一对应。
cluster.set("cluster-test", "my jedis cluster test");
String result = cluster.get("cluster-test");
System.out.println(result);
//程序结束时需要关闭JedisCluster对象
cluster.close();
}
Spring配置集群
<!-- 连接池配置 -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!-- 最大连接数 -->
<property name="maxTotal" value="30" />
<!-- 最大空闲连接数 -->
<property name="maxIdle" value="10" />
<!-- 每次释放连接的最大数目 -->
<property name="numTestsPerEvictionRun" value="1024" />
<!-- 释放连接的扫描间隔(毫秒) -->
<property name="timeBetweenEvictionRunsMillis" value="30000" />
<!-- 连接最小空闲时间 -->
<property name="minEvictableIdleTimeMillis" value="1800000" />
<!-- 连接空闲多久后释放, 当空闲时间>该值 且 空闲连接>最大空闲连接数 时直接释放 -->
<property name="softMinEvictableIdleTimeMillis" value="10000" />
<!-- 获取连接时的最大等待毫秒数,小于零:阻塞不确定的时间,默认-1 -->
<property name="maxWaitMillis" value="1500" />
<!-- 在获取连接的时候检查有效性, 默认false -->
<property name="testOnBorrow" value="true" />
<!-- 在空闲时检查有效性, 默认false -->
<property name="testWhileIdle" value="true" />
<!-- 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true -->
<property name="blockWhenExhausted" value="false" />
</bean>
<!-- redis集群 -->
<bean id="jedisCluster" class="redis.clients.jedis.JedisCluster">
<constructor-arg index="0">
<set>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.148.129"></constructor-arg>
<constructor-arg index="1" value="7001"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.148.129"></constructor-arg>
<constructor-arg index="1" value="7002"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.148.129"></constructor-arg>
<constructor-arg index="1" value="7003"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.148.129"></constructor-arg>
<constructor-arg index="1" value="7004"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.148.129"></constructor-arg>
<constructor-arg index="1" value="7005"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.148.129"></constructor-arg>
<constructor-arg index="1" value="7006"></constructor-arg>
</bean>
</set>
</constructor-arg>
<constructor-arg index="1" ref="jedisPoolConfig"></constructor-arg>
</bean>
Redis+LUA整合使用
LUA
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能
Redis中使用LUA的好处
减少网络开销,在Lua脚本中可以把多个命令放在同一个脚本中运行
原子操作,redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。换句话说,编写脚本的过程中无需担心会出现竞态条件
复用性,客户端发送的脚本会永远存储在redis中,这意味着其他客户端可以复用这一脚本来完成同样的逻辑
下载
地址:http://www.lua.org/download.html
可以本地下载上传到linux,也可以使用curl命令在linux系统中进行在线下载
curl -R -O http://www.lua.org/ftp/lua-5.3.5.tar.gz
安装
yum -y install readline-devel ncurses-devel
tar -zxvf lua-5.3.5.tar.gz
make linux
make install
如果报错,说找不到readline/readline.h, 可以通过yum命令安装
yum -y install readline-devel ncurses-devel
安装完以后再make linux / make install
最后,直接输入 lua命令即可进入lua的控制台
LUA常见语法
详见http://www.runoob.com/lua/lua-tutorial.html
Redis + LUA整合使用
从Redis2.6.0版本开始,通过内置的Lua解释器,可以使用EVAL命令对Lua脚本进行求值
EVAL命令
命令的格式如下:
EVAL script numkeys key [key ...] arg [arg ...]
命令说明:
script参数:是一段Lua脚本程序,它会被运行在Redis服务器上下文中,这段脚本不必(也不应该)定义为一个Lua函数
numkeys参数:用于指定键名参数的个数
key [key ...]参数: 从EVAL的第三个参数开始算起,使用了numkeys个键(key),表示在脚本中所用到的那些Redis键(key),这些键名参数可以在Lua中通过全局变量KEYS数组,用1为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推
arg [arg ...]参数:,可以在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)
例如:
lua脚本中调用redis命令
redis.call():
redis.pcall():
这两个函数的唯一区别在于它们使用不同的方式处理执行命令所产生的错误
EVALSHA
EVAL 命令要求你在每次执行脚本的时候都发送一次脚本主体(script body)
Redis 有一个内部的缓存机制,因此它不会每次都重新编译脚本,不过在很多场合,付出无谓的带宽来传送脚本主体并不是最佳选择
为了减少带宽的消耗, Redis 实现了 EVALSHA 命令,它的作用和 EVAL 一样,都用于对脚本求值,但它接受的第一个参数不是脚本,而是脚本的 SHA1 校验和(sum)
EVALSHA 命令的表现如下:
如果服务器还记得给定的 SHA1 校验和所指定的脚本,那么执行这个脚本
如果服务器不记得给定的 SHA1 校验和所指定的脚本,那么它返回一个特殊的错误,提醒用户使用 EVAL 代替 EVALSHA
SCRIPT命令
SCRIPT FLUSH :清除所有脚本缓存
SCRIPT EXISTS :根据给定的脚本校验和,检查指定的脚本是否存在于脚本缓存
SCRIPT LOAD :将一个脚本装入脚本缓存,返回SHA1摘要,但并不立即运行它
SCRIPT KILL :杀死当前正在运行的脚本
redis-cli --eval
可以使用redis-cli 命令直接执行脚本
格式如下:
$ redis-cli --eval script KEYS[1] KEYS[2] , ARGV[1] ARGV[2] ...
Redis消息模式
队列模式
使用list类型的lpush和rpop实现消息队列
注意事项:
消息接收方如果不知道队列中是否有消息,会一直发送rpop命令,如果这样的话,会每一次都建立一次连接,这样显然不好
可以使用brpop命令,它如果从队列中取不出来数据,会一直阻塞,在一定范围内没有取出则返回null
发布订阅模式
订阅消息(subscribe)
示例:subscribe hh-channel
发布消息(publish)
publish hh-channel “呵呵”
Redis发布订阅命令
缓存穿透、缓存击穿、缓存失效
缓存数据的步骤
查询缓存,如果没有数据,则查询数据库
查询数据库,如果数据不为空,将结果写入缓存
缓存穿透
缓存穿透
一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。如果key对应的value是一定不存在的,并且对该key并发请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透
如何解决
对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存
对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。(布隆表达式)
缓存雪崩
缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力
如何解决
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待
不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀
做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期(此点为补充)
缓存击穿
缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮
如何解决
使用redis的setnx互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大并发操作去操作数据库
if(redis.sexnx()==1){
//查询数据库
//加入线程
}
缓存淘汰策略之LRU
Redis内置缓存淘汰策略
最大缓存
在 redis 中,允许用户设置最大使用内存大小maxmemory,默认为0,没有指定最大缓存,如果有新的数据添加,超过最大内存,则会使redis崩溃,所以一定要设置。
redis 内存数据集大小上升到一定大小的时候,就会实行数据淘汰策略
淘汰策略
redis淘汰策略配置:maxmemory-policy voltile-lru,支持热配置
redis 提供 6种数据淘汰策略:
voltile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据
LRU原理
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”
LRU实现
最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:
新数据插入到链表头部
每当缓存命中(即缓存数据被访问),则将数据移到链表头部
当链表满的时候,将链表尾部的数据丢弃
在Java中可以使用LinkHashMap去实现LRU
分析
【命中率】
当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重
【复杂度】
实现简单
【代价】
命中时需要遍历链表,找到命中的数据块索引,然后需要将数据移到头部