Redis简介
Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的APIRedis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API
从2010年3月15日起,Redis的开发工作由VMware主持。从2013年5月开始,Redis的开发由Pivotal赞助
Redis是 NoSQL(not only SQL)技术阵营中的一员,它通过多种键值数据类型来适应不同场景下的存储需求,借助一些高层级的接口使用其可以胜任,如缓存、队列系统的不同角色
Redis可以保存在内存中(性能非常好),也可以持久化在文件中
Redis的官网: https://redis.io/
redis的中文手册:https://www.redis.com.cn/
经常用的数据成缓存,所以用redis
特性:
1.Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用
2.Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储
3.Redis支持数据的备份,即master-slave模式的数据备份
优势:
1.性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s
2.丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作
3.原子 – Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行
4.丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性
Redis下载及安装
Redis版本说明
Redis原生只支持linux系统。Redis官方是不支持windows平台,windows版本是微软自己建立的分支,基于官方的redis源码上进行编译、发布、维护的,所以window平台上的redis版本都略低于官方版本
当前redis最新版本是6.0
可以直接在windows上下载,通过上传工具上传到linux系统,也可以在linux下直接复制最新稳定版本下载链接的网址,然后wget+网址(这种方式必须要求可以上网)
Redis安装:
1.将redis.tar.gz解压:tar -zxvf redis-3.2.8.tar.gz
mkdir redis 建一个目录
2.进入redis-3.2.8目录:cd redis-3.2.8
3.对redis进行编译:make
4.安装到指定的目录:make PREFIX=/usr/local/redis install
如果安装redis是报下面的错误,是因为系统没有安装gcc环境,缺少依赖
解决方案:
安装完成后,对redis进行重新编译安装
redis安装完成后,我们进入目录bin中查看
redis-server redis服务器
redis-cli redis命令行客户端
redis-benchmark redis性能测试工具
redis-check-aof AOF文件修复工具
redis-check-rdb RDB文件检索工具
redis客户端测试:
1.启动redis服务
./redis-server是可以启动的,但是有风险,只在测试环境下可以。正常启动需要加载配置文件
注意:我们需要将redis编译后的目录中的redis.conf文件copy到我们自己的redis目录中。 cp redis.conf /usr/local/redis
这个redis.conf文件是redis的配置文件
在输入
就可以启动
注意:前面是redis-server服务,后面redis.conf配置文件
Redis占用的端口是6379
可以通过redis-cli命令行客户端来测试redis是否启动成功
其他ip地址不能连接是因为配置文件中只允许回环地址127.0.0.1连接
windows版redis
github下载或者百度网盘找到.msi的安装包,按步骤安装。
验证:
1、打开cmd窗口,输入redis-cli.exe -h 127.0.0.1 -p 6379后回车,出现127.0.0.1:6379说明redis安装且连接成功
2.试下是否能够存储成功:set key1 value1 get key1
3.设置redis密码:
查看密码:config get requirepass
设置密码:config set requirepass 123456
除去密码:config set requirepass ‘’
4.带密码登录,不然提示没有权限
redis-cli.exe -h 127.0.0.1 -p 6379 -a 密码
redis-cli.exe -h 127.0.0.1 -p 6379后进行操作提示没有权限时, auth 密码
以上为设置临时密码,也可以设置临时密码为root:
找到redis目录下redis-server.exe点击运行,再点击redis-cli.exe运行。输入config set requirepass root,回车即可
root 就是redis的临时密码。验证是否成功设置 ,输入auth root 如果返回OK就说明成功
设置永久的密码为root:
需要在redis.conf或redis.windows.conf中输入requirepass root,保存后,在该redis目录下的路径栏输入cmd,进入命令模式,输入redis-server.exe redis.windows.conf
打开redis
如果直接输入此命令不行,则将redis-server.exe直接拖进命令框中,即显示的是redis-server.exe的全路径 再跟上配置文件的命令 启动服务端
Redis数据类型与常见操作
redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set –有序集合)和hash(哈希类型)
string类型常见操作
字符串类型是Redis中最为基础的数据存储类型,它在Redis中是二进制安全的,这便意味着该类型可以接受任何格式的数据,如JPEG图像数据或Json对象描述信息等。
在Redis中字符串类型的Value最多可以容纳的数据长度是512M
常见操作(小写也可)
1 | SET key value |
list类型常见操作
在Redis中,List类型是按照插入顺序排序的字符串链表。和数据结构中的普通链表一样,我们可以在其头部(left)和尾部(right)添加新的元素。在插入时,如果该键并不存在,Redis将为该键创建一个新的链表。与此相反,如果链表中所有的元素均被移除,那么该键也将会被从数据库中删除。List中可以包含的最大元素数量是4294967295。
从元素插入和删除的效率视角来看,如果我们是在链表的两头插入或删除元素,这将会是非常高效的操作,即使链表中已经存储了百万条记录,该操作也可以在常量时间内完成。然而需要说明的是,如果元素插入或删除操作是作用于链表中间,那将会是非常低效的。
1 | 常见操作 |
hash类型常见操作
Redis中的Hashes类型可以看成具有String Key和String Value的map容器。所以该类型非常适合于存储值对象的信息。如用户信息:Username、Password和Age等。每一个Hash可以存储4294967295个键值对。
1 | 常见操作 |
set类型常见操作(不重复,可以完成交并差操作)
在Redis中,我们可以将Set类型看作为没有排序的字符串集合。Set可包含的最大元素数量是4294967295。
Set类型在功能上还存在着一个非常重要的特性,即在服务器端完成多个Sets之间的聚合计算操作,如unions、intersections和differences。由于这些操作均在服务端完成,因此效率极高,而且也节省了大量的网络IO开销。
1 | 常见操作 |
sortedSet类型常见操作
Sorted-Sets和Sets类型极为相似,它们都是字符串的集合,都不允许重复的成员出现在一个Set中。它们之间的主要差别是Sorted-Sets中的每一个成员都会有一个分数(score)与之关联,Redis正是通过分数来为集合中的成员进行从小到大的排序。然而需要额外指出的是,尽管Sorted-Sets中的成员必须是唯一的,但是分数(score)却是可以重复的。
在Sorted-Set中添加、删除或更新一个成员都是非常快速的操作,由于Sorted-Sets中的成员在集合中的位置是有序的,因此,即便是访问位于集合中部的成员也仍然是非常高效的。事实上,Redis所具有的这一特征在很多其它类型的数据库中是很难实现的,换句话说,在该点上要想达到和Redis同样的高效,在其它数据库中进行建模是非常困难的。
1 | 常见操作 |
Key通用操作
l KEYS pattern
获取所有匹配pattern参数(正则)的Keys。需要说明的是,在我们的正常操作中应该尽量避免对该命令的调用,因为对于大型数据库而言,该命令是非常耗时的,对Redis服务器的性能打击也是比较大的。pattern支持glob-style的通配符格式,如*表示任意一个或多个字符,?表示任意字符,[abc]表示方括号中任意一个字母。 匹配模式的键列表。
l DEL key [key …]
从数据库删除中参数中指定的keys,如果指定键不存在,则直接忽略。还需要另行指出的是,如果指定的Key关联的数据类型不是String类型,而是List、Set、Hashes和Sorted Set等容器类型,该命令删除每个键的时间复杂度为O(M),其中M表示容器中元素的数量。而对于String类型的Key,其时间复杂度为O(1)。
返回值:实际被删除的Key数量。
l EXISTS key
判断指定键是否存在。
返回值:1表示存在,0表示不存在。
l MOVE key db
将当前数据库中指定的键Key移动到参数中指定的数据库中。如果该Key在目标数据库中已经存在,或者在当前数据库中并不存在,该命令将不做任何操作并返回0。
返回值:移动成功返回1,否则0。
在redis.conf文件中定义了redis的默认库的数量
我们可以使用select 数值 来进行库的切换: select 10.
l RENAME key newkey
为指定的键重新命名,如果参数中的两个Keys的名字相同,或者是源Key不存在,该命令都会返回相关的错误信息。如果newKey已经存在,则直接覆盖。
l RENAMENX key newkey
如果新值不存在,则将参数中的原值修改为新值。其它条件和RENAME一致。
返回值:1表示修改成功,否则0。
l PERSIST key
如果Key存在过期时间,该命令会将其过期时间消除,使该Key不再有超时,而是可以持久化存储。
返回值:1表示Key的过期时间被移除,0表示该Key不存在或没有过期时间。
l EXPIRE key seconds
该命令为参数中指定的Key设定超时的秒数,在超过该时间后,Key被自动的删除。如果该Key在超时之前被修改,与该键关联的超时将被移除。
返回值:1表示超时被设置,0则表示Key不存在,或不能被设置。
l EXPIREAT key timestamp
该命令的逻辑功能和EXPIRE完全相同,唯一的差别是该命令指定的超时时间是绝对时间,而不是相对时间。该时间参数是Unix timestamp格式的,即从1970年1月1日开始所流经的秒数。
返回值:1表示超时被设置,0则表示Key不存在,或不能被设置。
l TTL key
获取该键所剩的超时描述。
返回值:返回所剩描述,如果该键不存在或没有超时设置,则返回-1。
l RANDOMKEY
从当前打开的数据库中随机的返回一个Key。
返回值:返回的随机键,如果该数据库是空的则返回nil。
l TYPE key
获取与参数中指定键关联值的类型,该命令将以字符串的格式返回。
返回值:返回的字符串为string、list、set、hash和zset,如果key不存在返回none
事务
Redis 事务可以一次执行多个命令, 并且带有以下两个重要的保证:
l 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
l 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
一个事务从开始到执行会经历以下三个阶段:
开始事务。MULTI
命令入队。执行的redis的操作
执行事务。EXEC
事务常用命令
l MULTI
Redis Multi 命令用于标记一个事务块的开始。
事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 EXEC 命令原子性(atomic)地执行。
返回值:总是返回OK
l EXEC
Redis Exec 命令用于执行所有事务块内的命令
返回值: 事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。
l DISCARD
Redis Discard 命令用于取消事务,放弃执行事务块内的所有命令。
返回值: 总是返回 OK 。
l WATCH
Redis Watch 命令用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断
l UNWATCH
Redis Unwatch 命令用于取消 WATCH 命令对所有 key 的监视
在exec执行后所产生的错误中,即使事务中有某个/某些命令在执行时产生了错误,事务中的其他命令仍然会继续执行。Redis在事务失败时不进行回滚,而是继续执行余下的命令。
Redis的这种设计原则是:Redis命令只会因为错误的语法而失败(这些问题不能在入队时发现),或是命令用在了错误类型的键上面
失败的命令不是Redis所致,而是由编程错误造成的,这样错误应该在开发的过程中被发现,生产环境中不应出现的错误。
就是在程序的运行环境中不应该出现语法的错误。而Redis能够保证正确的命令一定会被执行
实际做法:
(1)确认实际需求是否需要事务支持,如果需要则在对应方法上加上@Transaction注解
(2)如果不需要事务支持则将enableTransactionSupport设置为false
大佬推荐:
在 Spring 里配置两个单独的 RedisTemplate 是很好的做法:其中一个 RedisTemplates 的事务设为 false,用于大多数 Redis 操作,另一个 RedisTemplates 的事务已激活,仅用于 Redis 事务。当然必须要声明 PlatformTransactionManager 和 @Transactional,以防返回垃圾数值
nodejs应用redis
1.安装 redis 依赖包
1 | npm install redis --save |
2.连接
1 | var redis = require('redis'), |
3.使用
- 字符串(string)(一个字符串类型的值最大长度为512 M) get,set,mget,mset,incr(计数器++),decr(–)
1 | client.set('name', 'swx', function (err, res) { |
- 哈希(hash) 可用来存储对象,一个Redis 列表中最多可存储232-1(40亿)个元素
1 | client.hmset("hosts", "mjr", "1", "another", "23", "home", "1234"); |
- 列表(lists)(一个Redis 列表中最多可存储232-1(40亿)个元素)
对于lists,使用send_command进行操作;
队列操作
lpush(首位添加),rpush(末位添加),lset(修改首位值),lindex(删除首位元素),lpop(删除末位元素)
1 | client.send_command('lset',['mylist',0,1], function(err,data) { |
集合(sets)
sets 集合处理; 业务中用lodash进行交并补也是一个不错的选择。
常见操作: sadd、smembers、sinter(交)、sunion(并)、sdiff(补)、smove
集合中不允许重复成员的存在。当多次添加一个元素时,其结果会设置单个成员多次。一个Redis 集合中最多可包含232-1(40亿)个元素。
1 | let db1 = ['mysql','redis']; |
- 有序集合(SortedSets) zadd(设置元素), zrange(获取范围内的元素),zrank(获取指定元素的排名,从0开始), zscore(获取指定元素的score,用户指定的score)
1 | client.zadd(table, score, id, function (err, res) { |
- 事务(multi命令): 批量执行所有的命令,并统一返回结果
1 | client.multi() |
- 订阅发布模式
redis的订阅发布模式可用来做类似kafka的消息推送;
使用list + redis的订阅发布模式可以构建一个不错的消息队列;
1 | let sub = redis.createClient(6379, '127.0.0.1'); // 监听消费者 |
- 对整个redis的所有客户端操作进行监听 (monitor事件可以监听到redis收到的所有客户端命令)
1 | client.monitor(function(err, res) { |
java客户端jedis
Jedis介绍与快速入门
通过java来操作redis:使用jedis
搭建jedis环境:
新建一个javaproject,复制jar包到lib下,右键addpath构建路径
编写代码
程序执行报错:
原因:
1.redis服务器是否开启:linux上加载redis-server和redis.conf查看开启
2.linux的防火墙是否关闭:service iptables stop 关了之后在重启redis服务器
3.在redis.conf配置文件中 bind 127.0.0.1 代表的是外部不可以访问redis(可以把它注释掉,前面加#)
通过以上操作,错误信息不一样了:
原因:是我们没有设置密码
我们需要在redis.conf文件中设置密码admin:查找到requirepass,将前面的#删掉,然后将后面的改为admin,:wq保存退出
有密码的情况下,在命令行下连接redis需加上-a:
string类型常见操作
//string操作
*public* *class* JedisDemo2 {
Jedis jedis;
@Before
*public* *void* createJedis() {
jedis = *new* Jedis(“192.168.19.128”);
// 设置密码
jedis.auth(“admin”);
}
// 演示 set get
@Test
*public* *void* test1() {
jedis.set(“username”, “tom”);
String value = jedis.get(“username”);
System.****out****.println(value);
}
//演示mset mget
@Test
*public* *void* test2(){
jedis.mset(“password”,”123”,”age”,”20”);
List
System.****out****.println(values);
}
//演示 append setrange getrange
@Test
*public* *void* test3(){
//jedis.append(“username”,” is boy”);
//jedis.setrange(“username”, 7,”girl”);
System.****out****.println(jedis.get(“username”));
System.****out****.println(jedis.getrange(“username”, 7, -1));
}
}
list类型常见操作:
//list操作
*public* *class* JedisDemo3 {
Jedis jedis;
@Before
*public* *void* createJedis() {
jedis = *new* Jedis(“192.168.19.128”);
// 设置密码
jedis.auth(“admin”);
}
// 演示lpush lrange
@Test
*public* *void* test1() {
jedis.lpush(“names”, “tom”, “james”, “张三”, “李四”);
List
System.****out****.println(names);
}
// lset
@Test
*public* *void* test2() {
// jedis.lset(“names”, 1, “王五”);
// List
// System.out.println(names);
String value = jedis.lindex(“names”, 1);
System.****out****.println(value);
}
// linsert
@Test
*public* *void* test3() {
jedis.linsert(“names”, LIST_POSITION.****BEFORE****, “james”, “fox”);
List
System.****out****.println(names);
}
// lrem
@Test
*public* *void* test4(){
jedis.lrem(“names”, 1, “tom”);
List
System.****out****.println(names);
}
}
hash类型常见操作
//hash操作
*public* *class* JedisDemo4 {
Jedis jedis;
@Before
*public* *void* createJedis() {
jedis = *new* Jedis(“192.168.19.128”);
// 设置密码
jedis.auth(“admin”);
}
// 演示hset hget
@Test
*public* *void* test1() {
jedis.hset(“user”, “username”, “tom”);
String value = jedis.hget(“user”, “username”);
System.****out****.println(value);
}
// 演示hmset hmget
@Test
*public* *void* test2() {
Map<String, String> hash = *new* HashMap<String, String>();
hash.put(“password”, “123”);
hash.put(“sex”, “male”);
jedis.hmset(“user”, hash);
List
System.****out****.println(values);
}
//演示 hgetall hkeys kvals
@Test
*public* *void* test3(){
Map<String, String> map = jedis.hgetAll(“user”);
*for*(String key:map.keySet()){
System.****out****.println(key+” “+map.get(key));
}
Set
System.****out****.println(keys);
List
System.****out****.println(values);
}
// 演示hdel
@Test
*public* *void* test4(){
jedis.hdel(“user”, “username”,”password”);
Map<String, String> map = jedis.hgetAll(“user”);
*for*(String key:map.keySet()){
System.****out****.println(key+” “+map.get(key));
}
}
}
set类型常见操作
//set操作
*public* *class* JedisDemo5 {
Jedis jedis;
@Before
*public* *void* createJedis() {
jedis = *new* Jedis(“192.168.19.128”);
// 设置密码
jedis.auth(“admin”);
}
//演示sadd smembers
@Test
*public* *void* test1(){
jedis.sadd(“language1”,”java”,”c++”,”ruby”,”python”);
Set
System.****out****.println(smembers);
}
//演示srem
@Test
*public* *void* test2(){
jedis.srem(“language1”, “java”);
Set
System.****out****.println(smembers);
}
//差集 sdiff
@Test
*public* *void* test3(){
jedis.sadd(“language1”,”java”,”c++”,”ruby”,”python”);
jedis.sadd(“language2”,”ios”,”c++”,”c#”,”android”);
Set
System.****out****.println(sdiff);
}
//交集
@Test
*public* *void* test4(){
jedis.sadd(“language1”,”java”,”c++”,”ruby”,”python”);
jedis.sadd(“language2”,”ios”,”c++”,”c#”,”android”);
Set
System.****out****.println(sinter);
}
//并集
@Test
*public* *void* test5(){
jedis.sadd(“language1”,”java”,”c++”,”ruby”,”python”);
jedis.sadd(“language2”,”ios”,”c++”,”c#”,”android”);
Set
System.****out****.println(sunion);
}
}
sortedSet类型常见操作
//sortedset操作
*public* *class* JedisDemo6 {
Jedis jedis;
@Before
*public* *void* createJedis() {
jedis = *new* Jedis(“192.168.19.128”);
// 设置密码
jedis.auth(“admin”);
}
// 演示zadd zrange zrangeByScore
@Test
*public* *void* test1() {
Map<String, Double> sm = *new* HashMap<String, Double>();
sm.put(“张三”, 70.0);
sm.put(“李四”, 80.0);
sm.put(“王五”, 90.0);
sm.put(“赵六”, 60.0);
jedis.zadd(“zkey”, sm);
Set
System.****out****.println(set);
// 根据分数获取
Set
System.****out****.println(set1);
}
// 获取分数元素 zrangeWithScores
@Test
*public* *void* test2() {
Map<String, Double> sm = *new* HashMap<String, Double>();
sm.put(“张三”, 70.0);
sm.put(“李四”, 80.0);
sm.put(“王五”, 90.0);
sm.put(“赵六”, 60.0);
jedis.zadd(“zkey”, sm);
Set
*for* (Tuple t : zws) {
System.****out****.println(t.getScore() + “ “ + t.getElement());
}
}
// zrank
@Test
*public* *void* test3() {
Map<String, Double> sm = *new* HashMap<String, Double>();
sm.put(“张三”, 70.0);
sm.put(“李四”, 80.0);
sm.put(“王五”, 90.0);
sm.put(“赵六”, 60.0);
jedis.zadd(“zkey”, sm);
Long num = jedis.zrank(“zkey”, “赵六”);
System.****out****.println(num);
}
// zscore
@Test
*public* *void* test4() {
Map<String, Double> sm = *new* HashMap<String, Double>();
sm.put(“张三”, 70.0);
sm.put(“李四”, 80.0);
sm.put(“王五”, 90.0);
sm.put(“赵六”, 60.0);
jedis.zadd(“zkey”, sm);
Double zscore = jedis.zscore(“zkey”, “张三”);
System.****out****.println(zscore);
}
// zrem
@Test
*public* *void* test5() {
Map<String, Double> sm = *new* HashMap<String, Double>();
sm.put(“张三”, 70.0);
sm.put(“李四”, 80.0);
sm.put(“王五”, 90.0);
sm.put(“赵六”, 60.0);
jedis.zadd(“zkey”, sm);
jedis.zrem(“zkey”, “李四”);
Set
*for* (Tuple t : zws) {
System.****out****.println(t.getScore() + “ “ + t.getElement());
}
}
}
key的常见操作
//key的通用操作
*public* *class* JedisDemo7 {
Jedis jedis;
@Before
*public* *void* createJedis() {
jedis = *new* Jedis(“192.168.19.128”);
// 设置密码
jedis.auth(“admin”);
}
// keys patten
@Test
*public* *void* test1(){
Set
System.****out****.println(keys);
}
// del key
@Test
*public* *void* test2(){
Long del = jedis.del(“user”);
System.****out****.println(del);
}
//关于key时间设置
@Test
*public* *void* test3(){
//jedis.expire(“username”, 200); //设置生命周期为200秒
jedis.persist(“username”);
Long ttl = jedis.ttl(“username”); //获取生命周期值
System.****out****.println(ttl);
}
}
Redis数据持久化
Redis将内存存储和持久化存储相结合,既可提供数据访问的高效性,又可保证数据存储的安全性
1.Redis数据持久化机制介绍
1). RDB持久化:该机制是指在指定的时间间隔内将内存中的数据集快照写入磁盘。
2). AOF(append only file)持久化:该机制将以日志的形式记录服务器所处理的每一个写操作,在Redis服务器启动之初会读取该文件来重新构建数据库,以保证启动后数据库中的数据是完整的。
3). 同时应用AOF和RDB。
4). 无持久化:可通过配置的方式禁用Redis服务器的持久化功能,这样我们就可以将Redis视为一个功能加强版的memcached了
2.Redis数据持久化配置与测试
l RDB快照方式:缺省情况下,Redis会将数据集的快照dump到dump.rdb文件中。此外,我们也可以通过配置文件来修改Redis服务器dump快照的频率,在打开redis.conf文件之后,我们搜索save,可以看到下面的配置信息:
Ø save 900 1
#在900秒(15分钟)之后,如果至少有1个key发生变化,则dump内存快照。
Ø save 300 10
#在300秒(5分钟)之后,如果至少有10个key发生变化,则dump内存快照。
Ø save 60 10000
#在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照。
注意:关于dump.rdb文件存储的位置:它的设置是在redis.conf文件中dir ./
dir ./这段配置指的是服务器启动时的当前路径(在哪启动服务器时,这个rdb文件就在哪创建)。
l AOF日志文件方式:
Ø AOF日志持久化机制的开启:需要手动设置,在redis.conf文件中
将appendonly no 改为 appendonly yes
Ø AOF同步方式的配置:
在Redis的配置文件中存在三种同步方式,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件。
appendfsync everysec #每秒钟同步一次,该策略为AOF的缺省策略。
appendfsync no #从不同步。高效但是数据不会被持久化。
3.RDB与AOF对比总结
l RDB存在哪些优势呢?
1). 数据的备份和恢复非常方便,因为一个数据库只有一个持久化文件
2). 性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。
3). 相比于AOF机制,如果数据集很大,RDB的启动效率会更高。
l RDB又存在哪些劣势呢?
1).系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。
2). 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。
l AOF的优势有哪些呢?
1). 该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3种同步策略,即每秒同步、每修改同步和不同步。
2).对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。
3). 如果日志过大,Redis可以自动启用rewrite机制迅速“瘦身”(也可手动触发aof的rewrite操作,命令: bgrewriteaof)
4). AOF日志格式清晰、易于理解,很容易用AOF日志文件完成数据的重建。
l AOF的劣势有哪些呢?
1). 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。
2). 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。
Redis数据的备份
RDB方式(默认)
RDB方式的持久化是通过快照(snapshotting)完成的,当符合一定条件时Redis会自动将内存中的所有数据进行快照并存储在硬盘上。进行快照的条件可以由用户在配置文件中自定义,由两个参数构成:时间和改动的键的个数。当在指定的时间内被更改的键的个数大于指定的数值时就会进行快照。RDB是Redis默认采用的持久化方式,在配置文件中已经预置了3个条件:
1 | save 900 1 # 900秒内有至少1个键被更改则进行快照 |
可以存在多个条件,条件之间是“或”的关系,只要满足其中一个条件,就会进行快照。 如果想要禁用自动快照,只需要将所有的save参数删除即可。
Redis默认会将快照文件存储在当前目录(可CONFIG GET dir来查看)的dump.rdb文件中,可以通过配置dir和dbfilename两个参数分别指定快照文件的存储路径和文件名。
Redis实现快照的过程
- Redis使用fork函数复制一份当前进程(父进程)的副本(子进程);
- 父进程继续接收并处理客户端发来的命令,而子进程开始将内存中的数据写入硬盘中的临时文件;
- 当子进程写入完所有数据后会用该临时文件替换旧的RDB文件,至此一次快照操作完成。
在执行fork的时候操作系统(类Unix操作系统)会使用写时复制(copy-on-write)策略,即fork函数发生的一刻父子进程共享同一内存数据,当父进程要更改其中某片数据时(如执行一个写命令),操作系统会将该片数据复制一份以保证子进程的数据不受影响,所以新的RDB文件存储的是执行fork一刻的内存数据。
Redis在进行快照的过程中不会修改RDB文件,只有快照结束后才会将旧的文件替换成新的,也就是说任何时候RDB文件都是完整的。这使得我们可以通过定时备份RDB文件来实现Redis数据库备份。RDB文件是经过压缩(可以配置rdbcompression参数以禁用压缩节省CPU占用)的二进制格式,所以占用的空间会小于内存中的数据大小,更加利于传输。
除了自动快照,还可以手动发送SAVE或BGSAVE命令让Redis执行快照,两个命令的区别在于,前者是由主进程进 行快照操作,会阻塞住其他请求,后者会通过fork子进程进行快照操作。Redis启动后会读取RDB快照文件,将数据从硬盘载入到内存。根据数据量大小与结构和服务器性能不同,这个时间也不同。通常将一个记录一千万个字符串 类型键、大小为1GB的快照文件载入到内存中需要花费20~30秒钟。通过RDB方式实现持久化,一旦Redis异常退出,就会丢失最后一次快照以后更改的所有数据。这就需要开发者根据具体的应用场合,通过组合设置自动快照条件的方式来将可能发生的数据损失控制在能够接受的范围。如果数据很重要以至于无法承受任何损失,则可以考虑使用AOF方式进行持久化。
AOF方式
默认情况下Redis没有开启AOF(append only file)方式的持久化,可以在redis.conf中通过appendonly参数开启:
1 | appendonly yes |
在启动时Redis会逐个执行AOF文件中的命令来将硬盘中的数据载入到内存中,载入的速度相较RDB会慢一些,开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件。AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的,默认的文件名是appendonly.aof,可以通过appendfilename参数修改:
1 | appendfilename appendonly.aof |
配置redis自动重写AOF文件的条件
1 | auto-aof-rewrite-percentage 100 # 当目前的AOF文件大小超过上一次重写时的AOF文件大小的百分之多少时会再次进行重写,如果之前没有重写过,则以启动时的AOF文件大小为依据 |
配置写入AOF文件后,要求系统刷新硬盘缓存的机制
1 | # appendfsync always # 每次执行写入都会执行同步,最安全也最慢 |
Redis允许同时开启AOF和RDB,既保证了数据安全又使得进行备份等操作十分容易。此时重新启动Redis后Redis会使用AOF文件来恢复数据,因为AOF方式的持久化可能丢失的数据更少
复制
通过持久化功能,Redis保证了即使在服务器重启的情况下也不会损失(或少量损失)数据。但是由于数据是存储在一台服务器上的,如果这台服务器的硬盘出现故障,也会导致数据丢失。为了避免单点故障,我们希望将数据库复制多个副本以部署在不同的服务器上,即使有一台服务器出现故障其他服务器依然可以继续提供服务。这就要求当一台服务器上的数据库更新后,可以自动将更新的数据同步到其他服务器上,Redis提供了复制(replication)功能可以自动 实现同步的过程。
配置方法
Redis主从结构支持一主多从
主节点:192.168.100.200
从节点:192.168.100.150
注意:所有从节点的配置都一样
方式1:手动修改配置文件
1 | # slaveof <masterip> <masterport> |
1 | **方式2:动态设置** |
通过redis-cli 连接到从节点服务器,执行下面命令即可。
1 | 127.0.0.1:6379> slaveof 192.168.100.200 6379 |
注:slaveof no one 可以使当前数据库停止接收其他数据库的同步,转成主数据库
优点及应用场景
读写分离通过复制可以实现读写分离以提高服务器的负载能力。在常见的场景中,读的频率大于写,当单机的Redis无法应付大量的读请求时(尤其是较耗资源的请求,比如SORT命令等)可以通过复制功能建立多个从数据库,主数据库只进行写操作,而从数据库负责读操作。
从数据库持久化持久化通常相对比较耗时,为了提高性能,可以通过复制功能建立一个(或若干个)从数据库,并在从数据库中启用持久化,同时在主数据库禁用持久化。当从数据库崩溃时重启后主数据库会自动将数据同步过来,所以无需担心数据丢失。而当主数据库崩溃时,需要在从数据库中使用SLAVEOF NO ONE命令将从数据库提升成主数据库继续服务,并在原来的主数据库启动后使用SLAVEOF命令将其设置成新的主数据库的从数据库,即可将数据同步回来。
springboot项目里使用Redis做缓存
Spring Cache 操作Redis
当Spring Boot 结合Redis来作为缓存使用时,最简单的方式就是使用Spring Cache了,使用它我们无需知道Spring中对Redis的各种操作,仅仅通过它提供的@Cacheable 、@CachePut 、@CacheEvict 、@EnableCaching等注解就可以实现缓存功能。
@EnableCaching
开启缓存功能,一般放在启动类上。
@Cacheable
使用该注解的方法当缓存存在时,会从缓存中获取数据而不执行方法,当缓存不存在时,会执行方法并把返回结果存入缓存中。一般使用在查询方法上,可以设置如下属性:
- value:缓存名称(必填),指定缓存的命名空间;
- key:用于设置在命名空间中的缓存key值,可以使用SpEL表达式定义;
- unless:条件符合则不缓存;
- condition:条件符合则缓存。
@CachePut
使用该注解的方法每次执行时都会把返回结果存入缓存中。一般使用在新增方法上,可以设置如下属性:
- value:缓存名称(必填),指定缓存的命名空间;
- key:用于设置在命名空间中的缓存key值,可以使用SpEL表达式定义;
- unless:条件符合则不缓存;
- condition:条件符合则缓存。
@CacheEvict
使用该注解的方法执行时会清空指定的缓存。一般使用在更新或删除方法上,可以设置如下属性:
- value:缓存名称(必填),指定缓存的命名空间;
- key:用于设置在命名空间中的缓存key值,可以使用SpEL表达式定义;
- condition:条件符合则缓存。
使用步骤
- 在pom.xml中添加项目依赖:
1 | <!--redis依赖配置--> |
- 修改配置文件application.yml,添加Redis的连接配置;
1 | spring: |
- 在启动类上添加@EnableCaching注解启动缓存功能;
1 |
|
- 接下来在PmsBrandServiceImpl类中使用相关注解来实现缓存功能,可以发现我们获取品牌详情的方法中使用了@Cacheable注解,在修改和删除品牌的方法上使用了@CacheEvict注解;
1 | /** |
- 我们可以调用获取品牌详情的接口测试下效果,此时发现Redis中存储的数据有点像乱码,并且没有设置过期时间;
存储JSON格式数据
此时我们就会想到有没有什么办法让Redis中存储的数据变成标准的JSON格式,然后可以设置一定的过期时间,不设置过期时间容易产生很多不必要的缓存数据。
- 我们可以通过给RedisTemplate设置JSON格式的序列化器,并通过配置RedisCacheConfiguration设置超时时间来实现以上需求,此时别忘了去除启动类上的@EnableCaching注解,具体配置类RedisConfig代码如下;
1 | /** |
- 此时我们再次调用获取商品详情的接口进行测试,会发现Redis中已经缓存了标准的JSON格式数据,并且超时时间被设置为了1天。
使用Redis连接池
SpringBoot 1.5.x版本Redis客户端默认是Jedis实现的,SpringBoot 2.x版本中默认客户端是用Lettuce实现的,我们先来了解下Jedis和Lettuce客户端。
Jedis vs Lettuce
Jedis在实现上是直连Redis服务,多线程环境下非线程安全,除非使用连接池,为每个 RedisConnection 实例增加物理连接。
Lettuce是一种可伸缩,线程安全,完全非阻塞的Redis客户端,多个线程可以共享一个RedisConnection,它利用Netty NIO框架来高效地管理多个连接,从而提供了异步和同步数据访问方式,用于构建非阻塞的反应性应用程序。
使用步骤
- 修改application.yml添加Lettuce连接池配置,用于配置线程数量和阻塞等待时间;
1 | spring: |
- jedis的pom文件依赖
1 | <dependency> |
Lettuce的pom文件依赖
1 | <dependency> |
- 如果你没添加以上依赖的话,启动应用的时候就会产生如下错误;
1 | Caused by: java.lang.NoClassDefFoundError: org/apache/commons/pool2/impl/GenericObjectPoolConfig |
自由操作Redis
Spring Cache 给我们提供了操作Redis缓存的便捷方法,但是也有很多局限性。比如说我们想单独设置一个缓存值的有效期怎么办?我们并不想缓存方法的返回值,我们想缓存方法中产生的中间值怎么办?此时我们就需要用到RedisTemplate这个类了,接下来我们来讲下如何通过RedisTemplate来自由操作Redis中的缓存。
默认情况下RedisTemplate模板只能支持字符串,我们自定义一个RedisTemplate,设置序列化器,这样我们可以很方便的操作实例对象。将RedisTemplate模板的设置放在RedisConfig类中
RedisTemplate默认为我们实现了几种操作数据类型的方法:
redisTemplate有两个方法经常用到,一个是opsForXXX一个是boundXXXOps,XXX是value的类型,前者获取到一个Opercation,但是没有指定操作的key,可以在一个连接(事务)内操作多个key以及对应的value;后者会获取到一个指定了key的operation,在一个连接内只操作这个key对应的value
也可以自己定义:
RedisService
定义Redis操作业务类,在Redis中有几种数据结构,比如普通结构(对象),Hash结构、Set结构、List结构,该接口中定义了大多数常用操作方法。
1 | /** |
RedisServiceImpl
RedisService的实现类,使用RedisTemplate来自由操作Redis中的缓存数据。
1 | /** |
RedisController
测试RedisService中缓存操作的Controller,大家可以调用测试下。
1 | /** |
jedis实现缓存的实际操作:
jedis的maven依赖(不用lettuce,因为会出现一直连接超时的情况)
1 | <dependency> |
redis的配置
1 | @EnableCaching |
1 | redis: |
1 | 设置redis的缓存: |
个人理解:CacheManager和RedisTemplate都可以操纵redis的数据,规定key和value的存入和读取格式。(CacheManager更像是装饰后的对象,RedisTemplate更像是底层的对象)
设置缓存:CacheManager可以在配置类里设置多个,然后在注解里指定cacheManager。 也可以直接用redisService.set直接存入redis
更改缓存:可以利用注解更改。也可以用配置类定义好的CacheManager,从种获取到key并更改,删除。也可以直接用redisService操作
指定key和value的格式:
JdkSerializationRedisSerializer:当需要存储java对象时使用.
StringRedisSerializer:当需要存储string类型的字符串时使用.
JacksonJsonRedisSerializer:将对象序列化成json的格式存储在redis中,需要jackson-json工具的支持
opsForXXX和boundXXXOps的区别:
前者获取到一个Opercation,但是没有指定操作的key,可以在一个连接(事务)内操作多个key以及对应的value;
后者会获取到一个指定了key的operation,在一个连接内只操作这个key对应的value.
关于redis中缓存的问题
当数据库数据需要更改,怎么操作缓存
以下四种更新策略通过中途失败和多线程两种情况进行考虑:
1.先更新数据库,再更新缓存
读到旧数据:数据库更新成功,缓存更新失败,会读取到旧的缓存数据
读到错误数据:多线程情况下,线程A比B先进来,A更新完数据库卡住了,线程B此时顺利执行完,A又活过来才去更新缓存,就会出现读到的缓存是A的缓存不是B的缓存,导致读到错误数据(特别是写操作远大于读操作的项目场景)
2.先更新缓存,再更新数据库
读到错误数据:缓存更新成功,数据库更新失败,就会读取到错误的缓存数据
数据库更新不成功,缓存的数据应该也是不可以成功的
读到错误数据:多线程情况下,类似于1(也是特别是写操作远大于读操作的项目场景)
读多写少的场景,上面2种方式也勉强还行
3.先删除缓存,再更新数据库
如果删除缓存成功,但是更新数据库不成功,情况正常
读到错误数据:多线程情况下,A删除缓存后,停住了,B读数据库,设置缓存,A才去更新数据库,导致数据库和缓存不一致,导致读到错误数据(特别是读操作远大于写操作的项目场景)
4.先更新数据库,再删除缓存
读到错误数据:更新数据库成功,删除缓存失败,导致读到错误数据
读到错误数据:多线程情况下,A更新数据库,也删除缓存。B去读发现没有缓存,读到了数据,准备写的时候卡住了,C来更新数据库+删除缓存后,B活过来了,把读到的A数据存入到缓存,导致读到错误数据
对于3:也叫延时双删除策略,可以先删除缓存 ,再更新数据库,延时后再删除缓存,就是补一刀把后续的脏缓存数据删掉,这么具体的延时时间是多少,就得根据具体项目业务时间去衡量了
对于4:也叫Cache-Aside pattern,读操作实际上肯定比写操作快得多,所以发生上边描述的出现脏数据的场景的概率也是比较小
总结:
并发稍微低,那么我们可以用 先删除缓存,再更新数据库 再配上延时双删除策略
并发稍微高,那么我们可以用 Cache-Aside pattern , 先更新数据库 ,再删除缓存 。
脏数据 在不适用 分布式锁 或者 其他能保证数据顺序的方法的情况下,都是存在的。只不过是出现这种脏数据的概率以及严重性,是否是项目的业务需求可以接收。
上面存在的问题,都是有额外的补救方法,如加锁,重试,消息队列等等
1、想要提高应用的性能,可以引入「缓存」来解决
2、引入缓存后,需要考虑缓存和数据库一致性问题,可选的方案有:「更新数据库 + 更新缓存」、「更新数据库 + 删除缓存」
3、更新数据库 + 更新缓存方案,在「并发」场景下无法保证缓存和数据一致性,且存在「缓存资源浪费」和「机器性能浪费」的情况发生
4、在更新数据库 + 删除缓存的方案中,「先删除缓存,再更新数据库」在「并发」场景下依旧有数据不一致问题,解决方案是「延迟双删」,但这个延迟时间很难评估,所以推荐用「先更新数据库,再删除缓存」的方案
5、在「先更新数据库,再删除缓存」方案下,为了保证两步都成功执行,需配合「消息队列」或「订阅变更日志」的方案来做,本质是通过「重试」的方式保证数据一致性
6、在「先更新数据库,再删除缓存」方案下,「读写分离 + 主从库延迟」也会导致缓存和数据库不一致,缓解此问题的方案是「延迟双删」,凭借经验发送「延迟消息」到队列中,延迟删除缓存,同时也要控制主从库延迟,尽可能降低不一致发生的概率
缓存的名称
缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大
解决方案:
1.接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
2.从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
解决方案:
- 设置热点数据永远不过期。
- 加互斥锁
1)缓存中有数据,直接走上述代码13行后就返回结果了
2)缓存中没有数据,第1个进入的线程,获取锁并从数据库去取数据,没释放锁之前,其他并行进入的线程会等待100ms,再重新去缓存取数据。这样就防止都去数据库重复取数据,重复往缓存中更新数据情况出现。
3)当然这是简化处理,理论上如果能根据key值加锁就更好了,就是线程A从数据库取key1的数据并不妨碍线程B取key2的数据,上面代码明显做不到这点
缓存雪崩
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库
解决方案:
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
- 如果缓存数据库是分布式部署,将热点数据均匀分布在不同高的缓存数据库中。
- 设置热点数据永远不过期
疑问
对于基本不会变的数据,为什么不选择redis存储而是要使用mysql呢?
答:为了应对复杂的业务逻辑,可能一个表会涉及到多个相关联的字段,需要在查询一个表的数据时候,将其他关系表的数据查询出来。而redis只能存kv值,无法应对复杂的业务场景,即使Redis的读取效率再高,我们也没法用。但对于某些没有关联少,且需要高频率读写的数据,就可以使用(比如秒杀活动,可以在redis中高速读写,当达到一定阈值之后,再更新到mysql的数据库来实现短时间内的高并发场景)。redis只能作为一个辅助工具(可以当数据库,也可以当缓存工具)存储来提高性能。
什么时候使用redis?
答:当数据多、并发量大的时候,架构中可以引入Redis,帮助提升架构的整体性能,减少Mysql(或其他数据库)的压力
因为Redis的性能十分优越,可以支持每秒十几万此的读/写操作,并且它还支持持久化、集群部署、分布式、主从同步等,Redis在高并发的场景下数据的安全和一致性,所以它经常用于两个场景:
1.缓存
经常会被查询,但是不经常被修改或者删除的数据;比如数据字典,业务数据中的热点数据;这样不仅提升查询效率,还可以减少数据库的压力;
经常被查询,实时性要求不高数据,比如网站的最新列表、排行榜之类的数据,只需要定时统计一次,然后把统计结果放到Redis中提供查询(请不要使用select top 10 from xxxx)。
缓存可以方便数据共享,比如我先用电脑网页打开X东,选了两件商品放到购物车里面,再登录手机APP,也是可以看到购物车里面的商品的。
判断数据是否适合缓存到Redis中,可以从几个方面考虑:会经常查询么?命中率如何?写操作多么?数据大小?
我们经常采用这样的方式将数据刷到Redis中:查询的请求过来,现在Redis中查询,如果查询不到,就查询数据库拿到数据,再放到缓存中,这样第二次相同的查询请求过来,就可以直接在Redis中拿到数据;不过要注意【缓存穿透】的问题。
缓存的刷新会比较复杂,通常是修改完数据库之后,还需要对Redis中的数据进行操作;代码很简单,但是需要保证这两步为同一事务,或最终的事务一致性
2.高速读写
常见的就是计数器,比如一篇文章的阅读量,不可能每一次阅读就在数据库里面update一次。
高并发的场景很适合使用Redis,比如双11秒杀,库存一共就一千件,到了秒杀的时间,通常会在极为短暂的时间内,有数万级的请求达到服务器,如果使用数据库的话,很可能在这一瞬间造成数据库的崩溃,所以通常会使用Redis(秒杀的场景会比较复杂,Redis只是其中之一,例如如果请求超过某个数量的时候,多余的请求就会被限流)。
这种高并发的场景,是当请求达到服务器的时候,直接在Redis上读写,请求不会访问到数据库;程序会在合适的时间,比如一千件库存都被秒杀,再将数据批量写到数据库中。
所以通常来说,在必要的时候引入Redis,可以减少MySQL(或其他)数据库的压力,两者不是替代的关系
redis实现延迟队列
(在电商项目里有具体的实现方式)
应用场景:
- 在淘宝、京东等购物平台上下单,超过一定时间未付款,订单会自动取消。
- 打车的时候,在规定时间没有车主接单,平台会取消你的单并提醒你暂时没有车主接单。
- 点外卖的时候,如果商家在10分钟还没接单,就会自动取消订单。
- 收快递的时候,如果我们没有点确认收货,在一段时间后程序会自动完成订单。
- 在平台完成订单后,如果我们没有在规定时间评论商品,会自动默认买家不评论
这类场景都可以用定时任务去轮询实现,但是当数据量过大的时候,高频轮询数据库会消耗大量的资源,此时用延迟队列来应对这类场景比较好。
需求功能:
- 消息存储
- 过期延时消息实时获取
- 高可用性
消息延迟原理:
当用户发送一个消息请求给服务器后台的时候,服务器会检测这条消息是否需要进行延时处理,如果需要就放入到延时队列中,由延时任务检测器进行检测和处理,对于不需要进行延时处理的任务,服务器会立马对消息进行处理,并把处理后的结果返会给用户
对于在延时任务检测器内部的话,有查询延迟任务和执行延时任务两个职能,任务检测器会先去延时任务队列进行队列中信息读取,判断当前队列中哪些任务已经时间到期并将已经到期的任务输出执行(设置一个定时任务)
在redis中,我们可以使用 zset(sortedset)这个命令,用设置好的时间戳作为score进行排序,使用 zadd score1 value1 ….命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务,通过循环执行队列任务即可。也可以通过 zrangebyscore key min max withscores limit 0 1 查询最早的一条任务,来进行消费。
(1)使用zrangebyscore来查询当前延时队列中所有任务,找出所有需要进行处理的延时任务,在依次进行操作。
(2)查找当前最早的一条任务,通过score值来判断任务执行的时候是否大于了当前系统的时候,比如说:最早的任务执行时间在3点,系统时间在2点58分,表示这个应该需要立马被执行。
优势:
1.Redis zset支持高性能的 score 排序。
2.Redis是在内存上进行操作的,速度非常快。
3.Redis可以搭建集群,当消息很多时候,我们可以用集群来提高消息处理的速度,提高可用性。
4.Redis具有持久化机制,当出现故障的时候,可以通过AOF和RDB方式来对数据进行恢复,保证了数据的可靠性
注意:
队列最重要的就是保证消息被成功消费,所以需要Redis 实现 ACK
- 需要在业务代码中处理消息失败的情况,回滚消息到原始等待队列。
- Consumer 挂掉,仍然需要回滚消息到等待队列中。
其他实现延时队列的方法
消息中间件
(1)通过 RabbitMQ 来实现延时队列
优点:消息持久化,分布式
缺点:延时相同的消息必须扔在同一个队列,每一种延时就需要建立一个队列。因为当后面的消息比前面的消息先过期,还是只能等待前面的消息过期,这里的过期检测是惰性的。
使用:
方法一:RabbitMQ 可以针对 Queue 设置 x-expires 或者针对 Message 设置 x-message-ttl ,来控制消息的生存时间(可以根据 Queue 来设置,也可以根据 message 设置)。
方法二:Queue 还可以配置 x-dead-letter-exchange 和 x-dead-letter-routing-key(可选)两个参数,如果队列内出现了 dead letter ,则按照这两个参数重新路由转发到指定的队列,此时就可以实现延时队列了。(我们可以用RabbitMQ的插件rabbitmq-delayed-message-exchange插件来实现延时队列。达到可投递时间时并将其通过 x-delayed-type 类型标记的交换机类型投递至目标队列。)
(2)RocketMQ实现延时队列
rocketmq在发送延时消息时,是先把消息按照延迟时间段发送到指定的队列中(把延时时间段相同的消息放到同一个队列中,保证了消息处理的顺序性,可以让同一个队列中消息延时时间是相同的,整个RocketMQ中延时消息时按照递增顺序排序,保证信息处理的先后顺序性。)。之后,通过一个定时器来轮询处理这些队列里的信息,判断是否到期。对于到期的消息会发送到相应的处理队列中,进行处理。
注意 :目前RocketMQ只支持特定的延时时间段,1s,5s,10s,…2h,不能支持任意时间段的延时设置。
Kafka实现延时队
Kafka基于时间轮自定义了一个用于实现延迟功能的定时器(SystemTimer),Kafka中的时间轮(TimingWheel)是一个存储定时任务的环形队列,可以进行相关的延时队列设置。
Netty实现延时队列
Netty也有基于时间轮算法来实现延时队列。Netty在构建延时队列主要用HashedWheelTimer,HashedWheelTimer底层数据结构是使用DelayedQueue,采用时间轮的算法来实现。
DelayQueue来实现延时队列
优点:无界、延迟、阻塞队列
缺点:非持久化
介绍:JDK 自带的延时队列,没有过期元素的话,使用 poll() 方法会返回 null 值,超时判定是通过getDelay(TimeUnit.NANOSECONDS) 方法的返回值小于等于0来判断,并且不能存放空元素。
使用:getDelay 方法定义了剩余到期时间,compareTo 方法定义了元素排序规则。poll() 是非阻塞的获取数据,take() 是阻塞形式获取数据。实现 Delayed 接口即可使用延时队列。
注意:DelayQueue 实现了 Iterator 接口,但 iterator() 遍历顺序不保证是元素的实际存放顺序。
Java中有自带的DelayQueue数据类型,我们可以用这个来实现延时队列。DelayQueue是封装了一个PriorityQueue(优先队列),在向DelayQueue队列中添加元素时,会给元素一个Delay(延迟时间)作为排序条件,队列中最小的元素会优先放在队首,对于队列中的元素只有到了Delay时间才允许从队列中取出。这种实现方式是数据保存在内存中,可能面临数据丢失的情况,同时它是无法支持分布式系统的。
1 | /** |
Scala 的 Await & Future (非java)
优点:消息实时性
缺点:非持久化
介绍:Scala 的 ExecutionContext 中使用Await 的 result(awaitable: Awaitable[T], atMost: Duration)方法可以根据传入的 atMost 间隔时间异步执行 awaitable。