Redis全面实战教程

1.缓存的需求

前端页面广告位数据无需每次查询后台系统的接口,可以在前台系统添加缓存,提高访问首页的速度。

电商网站首页左侧商品类目一栏的数据也可以缓存起来,不用每次打开首页都去数据库读取数据,读取数据库IO开销大。

解决方案:使用缓存。

1.1目前缓存的主流技术

1、Redis
2、Memcached

二者谁的性能更高?
1、单纯从缓存命中的角度来说,是Memcached要高,Redis和Memcache的差距不大
2、但是,Redis提供的功能更加的强大

二者的区别是什么?
1、Memcache是多线程
2、Redis是单线程

1.2什么是缓存?

 ☞ 缓存就是数据交换的缓冲区(称作:Cache),当某一硬件要读取数据时,会首先从缓存汇总查询数据,有则直接执行,不存在时从内存中获取。由于缓存的数据比内存快的多,所以缓存的作用就是帮助硬件更快的运行。
      ☞ 缓存往往使用的是RAM(断电既掉的非永久存储),所以在用完后还是会把文件送到硬盘等存储器中永久存储。电脑中最大缓存就是内存条,硬盘上也有16M或者32M的缓存。
      ☞ 高速缓存是用来协调CPU与主存之间存取速度的差异而设置的。一般CPU工作速度高,但内存的工作速度相对较低,为了解决这个问题,通常使用高速缓存,高速缓存的存取速度介于CPU与主存之间。系统将一些CPU在最近几个时间段经常访问的内容存在高速缓存,这样就在一定程度上缓解了由于主存速度低造成的CPU“停工待料”的情况。
      ☞ 缓存就是把一些外存上的数据保存在内存上而已,为什么保存在内存上,我们运行的所有程序里面的变量都是存放在内存中的,所以如果想将值放入内存上,可以通过变量的方式存储。在JAVA中一些缓存一般都是通过Map集合来实现的。
 ▁▂▃▅▆ :缓存在不同的场景下,作用是不一样的具体举例说明:
         ✔  操作系统磁盘缓存 ——> 减少磁盘机械操作。
         ✔  数据库缓存——>减少文件系统IO。
         ✔  应用程序缓存——>减少对数据库的查询。
         ✔  Web服务器缓存——>减少应用服务器请求。
         ✔  客户端浏览器缓存——>减少对网站的访问。

2.Redis

2.1.NoSQL

2.2.主流的NoSQL产品

 

 

NoSQL数据库的四大分类如下:

键值(Key-Value)存储数据库:
相关产品:Tokyo Cabinet/Tyrant、Redis、Voldemeort、Berkeley DB
典型应用:内容缓存,主要用于处理大量数据的高访问负载
数据模型:一系列键值对
优势:快速查询
劣势:存储的数据缺少结构化

列存储数据库:
相关产品:Cassandra、HBase、Riak
典型应用:分布式的文件系统
数据模型:以列簇式存储,将同一列数据存在一起
优势:查找速度快,可扩展性强,更容易进行分布式扩展
劣势:功能相对局限

文档型数据库:
相关产品:CouchDB、MongoDB
典型应用:Web应用(与Key-Value类似,Value是结构化的)
数据模型:一系列键值对
优势:数据结构要求不严格
劣势:查询性能不高,而且缺乏统一的查询语法

图形(Graph)数据库:
相关产品:Neo4J、InfoGrid、Infinite Graph
典型应用:社交网络
数据模型:图结构
优势:利用图结构相关算法
劣势:需要对整个图做计算才能得出结果,不容易做分布式的集群方案

2.3.Redis简介

Redis官网: http://redis.io/

redis是Nosql数据库中使用较为广泛的非关系型内存数据库,redis内部是一个key-value存储系统。它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set –有序集合)和hash(哈希类型,类似于Java中的map)。Redis基于内存运行并支持持久化的NoSQL数据库,是当前最热门的NoSql数据库之一,也被人们称为数据结构存储服务务器。

2.4.Redis的特性

与其他NoSQL数据库相比,Redis有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis的数据类型都是基于基本数据结构的,同时对程序员透明,无需进行额外的抽象。
Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。内存数据库的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。
Redis特性:
性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
原子 – Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。
其它特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。

2.8.下载Redis

Linux版本 2.8.11 :
http://download.redis.io/releases/redis-2.8.11.tar.gz
Linux所有版本
http://download.redis.io/releases/

Windows(64位)版本 2.8.9 :
https://github.com/MSOpenTech/redis/blob/2.8/bin/release/redis-2.8.9.zip?raw=true
Windows(32位)版本 2.6 :
https://github.com/MSOpenTech/redis/blob/2.6/bin/release/redisbin.zip?raw=true
Windows所有版本:
https://github.com/MicrosoftArchive/redis/releases

2.8.Redis的安装

2.8.1.安装文件

 

2.8.2.安装方式一

开一个 cmd 窗口 使用 cd 命令切换目录到 E:\redis-3.1.2 运行:

redis-server.exe redis.windows.conf

测试:
双击打开redis-cli.exe

2.8.3.安装方式二(安装到系统服务)

进到redis目录执行如下命令安装服务:

开一个 cmd 窗口 使用 cd 命令切换目录到 E:\redis-3.1.2 运行:

redis-server.exe --service-install redis.windows.conf

2.8.4.注意事项

1、32位操作系统安装只能通过双击打开redis-server.exe启动,不能安装到系统服务。

2、由于文件系统非NTFS,导致Redis启动失败

3、限制Redis的最大内存(redis.windows-service.conf)

2.9.Redis-cli使用

cmd  启动redis-cli.exe

启动时添加相关的ip 端口密码

命令:[注意空格]
1.windows
redis-cli.exe –h [远程ip] –p [端口号] –a [密码]
2.linux
redis-cli –h [远程ip] –p [端口号] –a [密码]

 使用ping命令测试与客户端和服务端链接是否正常

 

2.10.Redis的多数据库

注意:Redis支持多个数据库,并且每个数据库的数据是隔离的不能共享,并且基于单机才有,如果是集群就没有数据库的概念。

Redis是一个字典结构的存储服务器,而实际上一个Redis实例提供了多个用来存储数据的字典,客户端可以指定将数据存储在哪个字典中。这与我们熟知的在一个关系数据库实例中可以创建多个数据库类似,所以可以将其中的每个字典都理解成一个独立的数据库。

每个数据库对外都是一个从0开始的递增数字命名,Redis默认支持16个数据库(可以通过配置文件支持更多,无上限),可以通过配置databases来修改这一数字。客户端与Redis建立连接后会自动选择0号数据库,不过可以随时使用SELECT命令更换数据库,如要选择1号数据库:

redis> SELECT 1
OK
redis [1] > GET foo
(nil)

然而这些以数字命名的数据库又与我们理解的数据库有所区别。

  • 首先Redis不支持自定义数据库的名字,每个数据库都以编号命名,开发者必须自己记录哪些数据库存储了哪些数据。
  • Redis也不支持为每个数据库设置不同的访问密码,所以一个客户端要么可以访问全部数据库,要么连一个数据库也没有权限访问。
  • 多个数据库之间并不是完全隔离的,比如FLUSHALL命令可以清空一个Redis实例中所有数据库中的数据。FLUSHALL – 清空所有数据库的所有数据,FLUSHDB – 清空当前所在数据库的数据

综上所述,这些数据库更像是一种命名空间,而不适宜存储不同应用程序的数据。比如可以使用0号数据库存储某个应用生产环境中的数据,使用1号数据库存储测试环境中的数据,但不适宜使用0号数据库存储A应用的数据而使用1号数据库B应用的数据,不同的应用应该使用不同的Redis实例存储数据。由于Redis非常轻量级,一个空Redis实例占用的内在只有1M左右,所以不用担心多个Redis实例会额外占用很多内存。

2.10.1.配置数据库数量

redis的数据库个数是可以配置的,默认为16个,见redis.windows.conf/redis.conf的databases 16。
对应数据库的索引值为0 - (databases -1),即16个数据库,索引值为0-15。默认存储的数据库为0。

2.11.Redis的基本命令(http://doc.redisfans.com/index.html

redis是一种高级的key-value的存储系统其中的key是字符串类型,尽可能满足如下几点:
1)key不要太长,最好不要操作1024个字节,这不仅会消耗内存还会降低查找效率
2)key不要太短,如果太短会降低key的可读性
3)在项目中,key最好有一个统一的命名规范(根据企业的需求)
其中value 支持五种数据类型:
1)字符串型 string
2)字符串列表 lists
3)字符串集合 sets
4)有序字符串集合 sorted sets
5)哈希类型 hashs

key
    keys * 获取所有的key
    select 0 选择第一个库
    move myString 1 将当前的数据库key移动到某个数据库,目标库有,则不能移动
    flush db      清除指定库
    randomkey     随机key
    type key      类型
    
    set key1 value1 设置key
    get key1    获取key
    mset key1 value1 key2 value2 key3 value3
    mget key1 key2 key3
    del key1   删除key
    exists key      判断是否存在key
    expire key 10   10过期
    pexpire key 1000 毫秒
    persist key     删除过期时间

 

3.Redis的字符串数据类型(String)

在Redis中字符串类型的Value最多可以容纳的数据长度是512M

 string
 SET key value 此命令设置指定键的值。
 GET key 获取指定键的值。
 GETRANGE key start end 获取存储在键上的字符串的子字符串。
 GETSET key value 设置键的字符串值并返回其旧值。
 GETBIT key offset 返回在键处存储的字符串值中偏移处的位值。
 MGET key1 [key2…] 获取所有给定键的值
 SETBIT key offset value 存储在键上的字符串值中设置或清除偏移处的位
 SETEX key seconds value 使用键和到期时间来设置值
 SETNX key value 设置键的值,仅当键不存在时
 SETRANGE key offset value 在指定偏移处开始的键处覆盖字符串的一部分
 STRLEN key 获取存储在键中的值的长度
 MSET key value [key value …] 为多个键分别设置它们的值
 MSETNX key value [key value …] 为多个键分别设置它们的值,仅当键不存在时
 PSETEX key milliseconds value 设置键的值和到期时间(以毫秒为单位)
 INCR key 将键的整数值增加1
 INCRBY key increment 将键的整数值按给定的数值增加
 INCRBYFLOAT key increment 将键的浮点值按给定的数值增加
 DECR key 将键的整数值减1
 DECRBY key decrement 按给定数值减少键的整数值
 APPEND key value 将指定值附加到键

4. 列表类型(List)

Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
一个列表最多可以包含 232 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。

List
BLPOP key1 [key2 ] timeout
移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
 BRPOP key1 [key2 ] timeout
移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
 BRPOPLPUSH source destination timeout
从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
 LINDEX key index
通过索引获取列表中的元素
 LINSERT key BEFORE|AFTER pivot value
在列表的元素前或者后插入元素
 LLEN key
获取列表长度
 LPOP key
移出并获取列表的第一个元素
 LPUSH key value1 [value2]
将一个或多个值插入到列表头部
 LPUSHX key value
将一个值插入到已存在的列表头部
 LRANGE key start stop
获取列表指定范围内的元素
 LREM key count value
移除列表元素
 LSET key index value
通过索引设置列表元素的值
 LTRIM key start stop
对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
 RPOP key
移除并获取列表最后一个元素
 RPOPLPUSH source destination
移除列表的最后一个元素,并将该元素添加到另一个列表并返回
 RPUSH key value1 [value2]
在列表中添加一个或多个值
 RPUSHX key value
为已存在的列表添加值

5.Redis 集合(Set)

5.1.set

Redis的Set是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。

set
 SADD key member1 [member2]
向集合添加一个或多个成员
 SCARD key
获取集合的成员数
 SDIFF key1 [key2]
返回给定所有集合的差集
 SDIFFSTORE destination key1 [key2]
返回给定所有集合的差集并存储在 destination 中
 SINTER key1 [key2]
返回给定所有集合的交集
 SINTERSTORE destination key1 [key2]
返回给定所有集合的交集并存储在 destination 中
 SISMEMBER key member
判断 member 元素是否是集合 key 的成员
 SMEMBERS key
返回集合中的所有成员
 SMOVE source destination member
将 member 元素从 source 集合移动到 destination 集合
 SPOP key
移除并返回集合中的一个随机元素
 SRANDMEMBER key [count]
返回集合中一个或多个随机数
 SREM key member1 [member2]
移除集合中一个或多个成员
 SUNION key1 [key2]
返回所有给定集合的并集
 SUNIONSTORE destination key1 [key2]
所有给定集合的并集存储在 destination 集合中
 SSCAN key cursor [MATCH pattern] [COUNT count]
迭代集合中的元素

5.2.有序集合(sorted set)

Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。

sorted set
ZADD key score1 member1 [score2 member2]
向有序集合添加一个或多个成员,或者更新已存在成员的分数
 ZCARD key
获取有序集合的成员数
 ZCOUNT key min max
计算在有序集合中指定区间分数的成员数
 ZINCRBY key increment member
有序集合中对指定成员的分数加上增量 increment
 ZINTERSTORE destination numkeys key [key …]
计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中
 ZLEXCOUNT key min max
在有序集合中计算指定字典区间内成员数量
 ZRANGE key start stop [WITHSCORES]
通过索引区间返回有序集合成指定区间内的成员
 ZRANGEBYLEX key min max [LIMIT offset count]
通过字典区间返回有序集合的成员
 ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT]
通过分数返回有序集合指定区间内的成员
 ZRANK key member
返回有序集合中指定成员的索引
 ZREM key member [member …]
移除有序集合中的一个或多个成员
 ZREMRANGEBYLEX key min max
移除有序集合中给定的字典区间的所有成员
 ZREMRANGEBYRANK key start stop
移除有序集合中给定的排名区间的所有成员
 ZREMRANGEBYSCORE key min max
移除有序集合中给定的分数区间的所有成员
 ZREVRANGE key start stop [WITHSCORES]
返回有序集中指定区间内的成员,通过索引,分数从高到底
 ZREVRANGEBYSCORE key max min [WITHSCORES]
返回有序集中指定分数区间内的成员,分数从高到低排序
 ZREVRANK key member
返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序
 ZSCORE key member
返回有序集中,成员的分数值
 ZUNIONSTORE destination numkeys key [key …]
计算给定的一个或多个有序集的并集,并存储在新的 key 中
 ZSCAN key cursor [MATCH pattern] [COUNT count]
迭代有序集合中的元素(包括元素成员和元素分值)

6.Redis的Hash数据结构

6.1.数据结构

Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。
Redis 中每个 hash 可以存储 232 - 1 键值对(40多亿)。

Map<String,Map<String,String>>

 Hash
 HDEL key field2 [field2]
删除一个或多个哈希表字段
 HEXISTS key field
查看哈希表 key 中,指定的字段是否存在。
 HGET key field
获取存储在哈希表中指定字段的值。
 HGETALL key
获取在哈希表中指定 key 的所有字段和值
 HINCRBY key field increment
为哈希表 key 中的指定字段的整数值加上增量 increment 。
 HINCRBYFLOAT key field increment
为哈希表 key 中的指定字段的浮点数值加上增量 increment 。
 HKEYS key
获取所有哈希表中的字段
 HLEN key
获取哈希表中字段的数量
 HMGET key field1 [field2]
获取所有给定字段的值
 HMSET key field1 value1 [field2 value2 ]
同时将多个 field-value (域-值)对设置到哈希表 key 中。
 HSET key field value
将哈希表 key 中的字段 field 的值设为 value 。
 HSETNX key field value
只有在字段 field 不存在时,设置哈希表字段的值。
 HVALS key
获取哈希表中所有值
 HSCAN key cursor [MATCH pattern] [COUNT count]
迭代哈希表中的键值对。

7.Redis之生存时间

7.1.redis中可以使用expire命令设置一个键的生存时间,到时间后redis会自动删除它

expire 设置生存时间(单位/秒)
pexpire设置生存时间(单位/毫秒)
ttl/pttl 查看键的剩余生存时间
persist 取消生存时间
expireat [key] unix时间戳1351858600
pexpireat [key] unix时间戳(毫秒)1351858700000

7.2.应用场景

  • 限时的优惠活动
  • 网站数据缓存(对于一些需要定时更新的数据)
  • 限制网站访客访问频率(例如:1分钟最多访问10次)

8.Redis事务

8.1.11.1、Redis事务

Redis 事务可以一次执行多个命令, 并且带有以下两个重要的保证:
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
一个事务从开始到执行会经历以下三个阶段:
开始事务。
命令入队。
执行事务。

8.3.11.2、Redis 事务命令

下表列出了 redis 事务的相关命令:
序号 命令及描述
1 DISCARD
取消事务,放弃执行事务块内的所有命令。
2 EXEC
执行所有事务块内的命令。
3 MULTI
标记一个事务块的开始。
4 UNWATCH
取消 WATCH 命令对所有 key 的监视。
5 WATCH key [key …]
监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

9.消息队列(发布订阅模式)

一般来说,消息队列有两种场景,一种是发布者订阅者模式,一种是生产者消费者模式。利用redis这两种场景的消息队列都能够实现。

定义:
        生产者消费者模式:生产者生产消息放到队列里,多个消费者同时监听队列,谁先抢到消息谁就会从队列中取走消息;即对于每个消息只能被最多一个消费者拥有。(pop操作)
        发布者订阅者模式:发布者生产消息放到队列里,多个监听队列的消费者都会收到同一份消息;即正常情况下每个消费者收到的消息应该都是一样的。

Redis不仅可作为缓存服务器,还可用作消息队列。它的列表类型天生支持用作消息队列。

由于Redis的列表是使用双向链表实现的,保存了头尾节点,所以在列表头尾两边插取元素都是非常快的。

在Redis中,List类型是按照插入顺序排序的字符串链表。和数据结构中的普通链表一样,我们可以在其头部(left)和尾部(right)添加新的元素。在插入时,如果该键并不存在,Redis将为该键创建一个新的链表。与此相反,如果链表中所有的元素均被移除,那么该键也将会被从数据库中删除。List中可以包含的最大元素数量是4294967295。
从元素插入和删除的效率视角来看,如果我们是在链表的两头插入或删除元素,这将会是非常高效的操作,即使链表中已经存储了百万条记录,该操作也可以在常量时间内完成。然而需要说明的是,如果元素插入或删除操作是作用于链表中间,那将会是非常低效的。相信对于有良好数据结构基础的开发者而言,这一点并不难理解。
Redis List的主要操作为lpush/lpop/rpush/rpop四种,分别代表从头部和尾部的push/pop,除此之外List还提供了两种pop操作的阻塞版本blpop/brpop,用于阻塞获取一个对象

Redis 发布/订阅模式实现消息队列的简单例子(此外用的是redisTemplater操作redis)

 实例

pom.xml相关依赖

   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
            <version>1.3.5.RELEASE</version>
        </dependency>

  创建Redis消息监听者容器

package com.demo.redisandwebsocket.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
/**
 * 创建Redis消息监听者容器
 * @author 15838
 *
 */
@Configuration //相当于xml中的beans
public class RedisConfig {
    /**
     * 需要手动注册RedisMessageListenerContainer加入IOC容器
     * @author lijt
     * @return
     */
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        //订阅了一个叫chat 的通道
        container.addMessageListener(new MessageListener(){
            @Override
            public void onMessage(Message message, byte[] pattern) {
                String msg = new String(message.getBody());
                System.out.println(new String(pattern) + "主题发布:" + msg);
            }
        }, new PatternTopic("TOPIC"));
        return container;
    }

}

  创建Websocket配置类

package com.demo.redisandwebsocket.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * 创建Websocket配置类
 * 首先要注入ServerEndpointExporter,这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint。
 * 要注意,如果使用独立的servlet容器,而不是直接使用springboot的内置容器,就不要注入ServerEndpointExporter,因为它将由容器自己提供和管理
 */
@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
这个配置类的作用是要注入ServerEndpointExporter,这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint。如果是使用独立的servlet容器,而不是直接使用springboot的内置容器,就不要注入ServerEndpointExporter,因为它将由容器自己提供和管理。
本文的例子是采用的springboot的内置tomcat容器,所以还是要创建这个配置类,作用就是注入ServerEndpointExporter。
创建消息订阅监听者类
package com.demo.redisandwebsocket.listener;

import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;

import javax.websocket.Session;
import java.io.IOException;

/**
 * 创建消息订阅监听者类
 *  描述:消息订阅监听类
 *  这个消息订阅监听者类持有websocket的客户端会话对象(session),当接收到订阅的消息时,通过这个会话对象(session)将消息发送到前端,从而实现消息的主动推送。
 */
public class SubscribeListener implements MessageListener {

    private Session session; //websocker的客户端连接会话对象
    public Session getSession() {   return session;  }
    public void setSession(Session session) {
        this.session = session;
    }

    /**
     * 接收发布者的消息
     */
    @Override
    public void onMessage(Message message, byte[] pattern) {
        String msg = new String(message.getBody());
        System.out.println(new String(pattern) + "主题发布:" + msg);
        if (null != session && session.isOpen()) {
            try {
                session.getBasicRemote().sendText(msg);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

  这个消息订阅监听者类持有websocket的客户端会话对象(session),当接收到订阅的消息时,通过这个会话对象(session)将消息发送到前端,从而实现消息的主动推送。

工具类SpringUtils获取IOC实例
package com.demo.redisandwebsocket.util;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;

@Component
public final class SpringUtils implements BeanFactoryPostProcessor {

    private static ConfigurableListableBeanFactory beanFactory; // Spring应用上下文环境

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        SpringUtils.beanFactory = beanFactory;
    }

    public static ConfigurableListableBeanFactory getBeanFactory() {
        return beanFactory;
    }

    /**
     * 获取对象
     *
     * @param name
     * @return Object 一个以所给名字注册的bean的实例
     * @throws org.springframework.beans.BeansException
     *
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) throws BeansException {
        return (T) getBeanFactory().getBean(name);
    }

    /**
     * 获取类型为requiredType的对象
     *
     * @param clz
     * @return
     * @throws org.springframework.beans.BeansException
     *
     */
    public static <T> T getBean(Class<T> clz) throws BeansException {
        T result = (T) getBeanFactory().getBean(clz);
        return result;
    }

    /**
     * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
     *
     * @param name
     * @return boolean
     */
    public static boolean containsBean(String name) {
        return getBeanFactory().containsBean(name);
    }

    /**
     * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
     *
     * @param name
     * @return boolean
     * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
     *
     */
    public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
        return getBeanFactory().isSingleton(name);
    }

    /**
     * @param name
     * @return Class 注册对象的类型
     * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
     *
     */
    public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
        return getBeanFactory().getType(name);
    }

    /**
     * 如果给定的bean名字在bean定义中有别名,则返回这些别名
     *
     * @param name
     * @return
     * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
     *
     */
    public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
        return getBeanFactory().getAliases(name);
    }

}
创建Websocket服务端类
package com.demo.redisandwebsocket.web;

import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;

import com.demo.redisandwebsocket.listener.SubscribeListener;
import com.demo.redisandwebsocket.util.SpringUtils;

/**
 * @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,
 * 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
 * 使用springboot的唯一区别是要@Component声明下,而使用独立容器是由容器自己管理websocket的,但在springboot中连容器都是spring管理的。
 * 虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
 */
@Component
@ServerEndpoint("/websocket/server")
public class WebSocketServer {
    /**
     * 因为@ServerEndpoint不支持注入,所以使用SpringUtils获取IOC实例
     */
    private RedisMessageListenerContainer redisMessageListenerContainer = SpringUtils.getBean(RedisMessageListenerContainer.class);

    //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
     private static  AtomicInteger onlineCount=new AtomicInteger(0);
     //concurrent包的线程安全Set,用来存放每个客户端对应的webSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
     private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();

     //与某个客户端的连接会话,需要通过它来给客户端发送数据
     private Session session;

     private SubscribeListener subscribeListener;

    /**
     * 连接建立成功调用的方法
     * @param session  可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    @OnOpen
    public void onOpen(Session session){
        this.session = session;
        webSocketSet.add(this);     //加入set中
        addOnlineCount();           //在线数加1
        System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
        subscribeListener = new SubscribeListener();
        subscribeListener.setSession(session);
        //设置订阅topic
        redisMessageListenerContainer.addMessageListener(subscribeListener, new ChannelTopic("TOPIC"));
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() throws IOException {
        webSocketSet.remove(this);  //从set中删除
        subOnlineCount();           //在线数减1
        redisMessageListenerContainer.removeMessageListener(subscribeListener);
        System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
    }

    /**
     * 收到客户端消息后调用的方法
     * @param message 客户端发送过来的消息
     * @param session 可选的参数
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("来自客户端的消息:" + message);
        //群发消息
        for(WebSocketServer item: webSocketSet){
            try {
                item.sendMessage(message);
            } catch (IOException e) {
                e.printStackTrace();
                continue;
            }
        }
    }

    /**
     * 发生错误时调用
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error){
        System.out.println("发生错误");
        error.printStackTrace();
    }

    /**
     * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。
     * @param message
     * @throws IOException
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }

    public   int getOnlineCount() {
        return onlineCount.get();
    }

    public   void addOnlineCount() {
        WebSocketServer.onlineCount.getAndIncrement();
    }

    public   void subOnlineCount() {
        WebSocketServer.onlineCount.getAndDecrement();
    }

}
@ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端使用springboot的唯一区别是要@Component声明下,而使用独立容器是由容器自己管理websocket的,但在springboot中连容器都是spring管理的。
虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
注意的是在客户端链接关闭的方法onClose中,一定要 删除之前的订阅监听对象,就是下面这行代码:redisMessageListenerContainer.removeMessageListener(subscribeListener);否则在浏览器刷一下之后,后台会报如下错误:
java.lang.IllegalStateException: The WebSocket session [0] has been closed and no method (apart from close()) may be called on a closed session
原因就是当链接关闭之后,session对象就没有了,而订阅者对象还是会接收消息,在用session对象发送消息时会报错。
虽然代码中加了判断if (null != session && session.isOpen()) {  可以避免报错,但是为了防止内存泄漏,应该把没有用的监听者对象从容器中删除。
创建前端页面,在resource\static目录下创建html页面,命名为websocket.html
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8"></meta>
    <title>websocket</title>
</head>
<h4>
使用redis订阅消息和websocket实现消息推送
</h4>
<br/>
<h5>收到的订阅消息:</h5>
<div id="message_id"></div>
</body>
<script type="text/javascript">
    var websocket = null;
    //当前浏览前是否支持websocket
    if("WebSocket" in window){
        var url = "ws://localhost:8080/demo/websocket/server";
        websocket = new WebSocket(url);
    }else{
        alert("浏览器不支持websocket");
    }

    websocket.onopen = function(event){
        setMessage("打开连接");
    }

    websocket.onclose = function(event){
        setMessage("关闭连接");
    }

    websocket.onmessage = function(event){
        setMessage(event.data);
    }

    websocket.onerror = function(event){
        setMessage("连接异常");
    }

    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function(){
        closeWebsocket();
    }

    //关闭websocket
    function closeWebsocket(){
        //3代表已经关闭
        if(3!=websocket.readyState){
            websocket.close();
        }else{
            alert("websocket之前已经关闭");
        }
    }

    //将消息显示在网页上
    function setMessage(message){
        document.getElementById('message_id').innerHTML += message + '<br/>';
    }

</script>
</html> 

启动服务进行测试

  1. 启动springboot服务,浏览器输入地址:http://localhost:8080/websocket.html,此时页面显示如下:

 

2.打开redis客户端,在命令行输入publish  TOPIC   “this is test message”

浏览器页面显示如下:

 

 

 说明刚刚发布的消息已经主动推送到浏览器显示了

10、数据备份和恢复

Redis所有数据都是保存在内存中,Redis数据备份可以定期的通过异步方式保存到磁盘上,该方式称为半持久化模式,如果每一次数据变化都写入aof文件里面,则称为全持久化模式。同时还可以基于Redis主从复制实现Redis备份与恢复。

10.1、 半持久化RDB模式

    半持久化RDB模式也是Redis备份默认方式,是通过快照(snapshotting)完成的,当符合在Redis.conf配置文件中设置的条件时Redis会自动将内存中的所有数据进行快照并存储在硬盘上,完成数据备份。

Redis进行RDB快照的条件由用户在配置文件中自定义,由两个参数构成:时间和改动的键的个数。当在指定的时间内被更改的键的个数大于指定的数值时就会进行快照。在配置文件中已经预置了3个条件:

save       900    1       #900秒内有至少1个键被更改则进行快照;
save       300    10      #300秒内有至少10个键被更改则进行快照;
save       60     10000        #60秒内有至少10000个键被更改则进行快照。

        默认可以存在多个条件,条件之间是“或”的关系,只要满足其中一个条件,就会进行快照。 如果想要禁用自动快照,只需要将所有的save参数删除即可。Redis默认会将快照文件存储在Redis数据目录,默认文件名为:dump.rdb文件,可以通过配置dir和dbfilename两个参数分别指定快照文件的存储路径和文件名。也可以在Redis命令行执行config  get  dir获取Redis数据保存路径,如图12-15(a)、12-15(b)所示:

                                                   

 

                                                                             图12-15(a) 获取Redis数据目录

                                                  

                                                                         图12-15(b) Redis数据目录及dump.rdb文件

        Redis实现快照的过程,Redis使用fork函数复制一份当前进程(父进程)的副本(子进程),父进程继续接收并处理客户端发来的命令,而子进程开始将内存中的数据写入硬盘中的临时文件,当子进程写入完所有数据后会用该临时文件替换旧的RDB文件,至此一次快照操作完成。

        执行fork的时操作系统会使用写时复制(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异常退出,就会丢失最后一次快照以后更改的所有数据。此时需要开发者根据具体的应用场合,通过组合设置自动快照条件的方式来将可能发生的数据损失控制在能够接受的范围。

10.2、全持久化AOF模式

       如果数据很重要无法承受任何损失,可以考虑使用AOF方式进行持久化,默认Redis没有开启AOF(append only file)方式的全持久化模式。

在启动时Redis会逐个执行AOF文件中的命令来将硬盘中的数据载入到内存中,载入的速度相较RDB会慢一些,开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件。AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的,默认的文件名是appendonly.aof,可以通过appendfilename参数修改该名称。

Redis允许同时开启AOF和RDB,既保证了数据安全又使得进行备份等操作十分容易。此时重新启动Redis后Redis会使用AOF文件来恢复数据,因为AOF方式的持久化可能丢失的数据更少,可以在redis.conf中通过appendonly参数开启Redis AOF全持久化模式:

appendonly  yes
appendfilename appendonly.aof
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
appendfsync always  
#appendfsync everysec
#appendfsync no

Redis AOF持久化参数配置详解:

appendonly  yes                    #开启AOF持久化功能;
appendfilename appendonly.aof          #AOF持久化保存文件名;
appendfsync always                     #每次执行写入都会执行同步,最安全也最慢;
#appendfsync everysec                  #每秒执行一次同步操作;
#appendfsync no                    #不主动进行同步操作,而是完全交由操作系统来做,每30秒一次,最快也最不安全;
auto-aof-rewrite-percentage  100      #当AOF文件大小超过上一次重写时的AOF文件大小的百分之多少时会再次进行重写,如果之前没有重写过,则以启动时的AOF文件大小为依据;
auto-aof-rewrite-min-size    64mb      #允许重写的最小AOF文件大小配置写入AOF文件后,要求系统刷新硬盘缓存的机制。

10.3 、 Redis主从复制备份

       通过持久化功能,Redis保证了即使在服务器重启的情况下也不会损失(或少量损失)数据。但是由于数据是存储在一台服务器上的,如果这台服务器的硬盘出现故障,也会导致数据丢失。

为了避免单点故障,我们希望将数据库复制多个副本以部署在不同的服务器上,即使只有一台服务器出现故障其他服务器依然可以继续提供服务,这就要求当一台服务器上的数据库更新后,可以自动将更新的数据同步到其他服务器上,Redis提供了复制(replication)功能可以自动实现同步的过程。通过配置文件在Redis从数据库中配置文件中加入slaveof  master-ip  master-port即可,主数据库无需配置。

       Redis主从复制优点及应用场景, WEB应用程序可以基于主从同步实现读写分离以提高服务器的负载能力。在常见的场景中,读的频率一般比较大,当单机Redis无法应付大量的读请求时,可以通过复制功能建立多个从数据库,主数据库只进行写操作,而从数据库负责读操作,还可以基于LVS+keepalived+Redis对Redis实现均和高可用。

       从数据库持久化持久化通常相对比较耗时,为了提高性能,可以通过复制功能建立一个(或若干个)从数据库,并在从数据库中启用持久化,同时在主数据库禁用持久化。

        当从数据库崩溃时重启后主数据库会自动将数据同步过来,所以无需担心数据丢失。而当主数据库崩溃时,需要在从数据库中使用SLAVEOF NO ONE命令将从数据库提升成主数据库继续服务,并在原来的主数据库启动后使用SLAVE  OF命令将其设置成新的主数据库的从数据库,即可将数据同步回来。

11、redis集群

Redis有三种集群模式,分别是

* 主从模式

* Sentinel模式(哨兵模式)
* Cluster模式

11.1主从模式

主从模式是三种模式中最简单的,在主从复制中,数据库分为两类:主数据库(master)和从数据库(slave)。

其中主从复制有如下特点:

  • * 主数据库可以进行读写操作,当读写操作导致数据变化时会自动将数据同步给从数据库
  • * 从数据库一般都是只读的,并且接收主数据库同步过来的数据
  • * 一个master可以拥有多个slave,但是一个slave只能对应一个master
  • * slave挂了不影响其他slave的读和master的读和写,重新启动后会将数据从master同步过来
  • * master挂了以后,不影响slave的读,但redis不再提供写服务,master重启后redis将重新对外提供写服务
  • * master挂了以后,不会在slave节点中重新选一个master

工作机制:

当slave启动后,主动向master发送SYNC命令。master接收到SYNC命令后在后台保存快照(RDB持久化)和缓存保存快照这段时间的命令,然后将保存的快照文件和缓存的命令发送给slave。slave接收到快照文件和命令后加载快照文件和缓存的执行命令。

复制初始化后,master每次接收到的写命令都会同步发送给slave,保证主从数据一致性。

安全设置:

  • 当master节点设置密码后,
  • 客户端访问master需要密码
  • 启动slave需要密码,在配置文件中配置即可
  • 客户端访问slave不需要密码

缺点:

从上面可以看出,master节点在主从模式中唯一,若master挂掉,则redis无法对外提供写服务。

11.2Sentinel模式(哨兵模式)

sentinel中文含义为哨兵,顾名思义,它的作用就是监控redis集群的运行状况,特点如下:

  • * sentinel模式是建立在主从模式的基础上,如果只有一个Redis节点,sentinel就没有任何意义
  • * 当master挂了以后,sentinel会在slave中选择一个做为master,并修改它们的配置文件,其他slave的配置文件也会被修改,比如slaveof属性会指向新的master
  • * 当master重新启动后,它将不再是master而是做为slave接收新的master的同步数据
  • * sentinel因为也是一个进程有挂掉的可能,所以sentinel也会启动多个形成一个sentinel集群
  • * 多sentinel配置的时候,sentinel之间也会自动监控
  • * 当主从模式配置密码时,sentinel也会同步将配置信息修改到配置文件中,不需要担心
  • * 一个sentinel或sentinel集群可以管理多个主从Redis,多个sentinel也可以监控同一个redis
  • * sentinel最好不要和Redis部署在同一台机器,不然Redis的服务器挂了以后,sentinel也挂了

工作机制

  • * 每个sentinel以每秒钟一次的频率向它所知的master,slave以及其他sentinel实例发送一个 PING 命令
  • * 如果一个实例距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被sentinel标记为主观下线。
  • * 如果一个master被标记为主观下线,则正在监视这个master的所有sentinel要以每秒一次的频率确认master的确进入了主观下线状态
  • * 当有足够数量的sentinel(大于等于配置文件指定的值)在指定的时间范围内确认master的确进入了主观下线状态, 则master会被标记为客观下线
  • * 在一般情况下, 每个sentinel会以每 10 秒一次的频率向它已知的所有master,slave发送 INFO 命令
  • * 当master被sentinel标记为客观下线时,sentinel向下线的master的所有slave发送 INFO 命令的频率会从 10 秒一次改为 1 秒一次
  • * 若没有足够数量的sentinel同意master已经下线,master的客观下线状态就会被移除;
  • 若master重新向sentinel的 PING 命令返回有效回复,master的主观下线状态就会被移除

当使用sentinel模式的时候,客户端就不要直接连接Redis,而是连接sentinel的ip和port,由sentinel来提供具体的可提供服务的Redis实现,这样当master节点挂掉以后,sentinel就会感知并将新的master节点提供给使用者。

11.3Cluster模式

sentinel模式基本可以满足一般生产的需求,具备高可用性。但是当数据量过大到一台服务器存放不下的情况时,主从模式或sentinel模式就不能满足需求了,这个时候需要对存储的数据进行分片,将数据存储到多个Redis实例中。cluster模式的出现就是为了解决单机Redis容量有限的问题,将Redis的数据根据一定的规则分配到多台机器。

cluster可以说是sentinel和主从模式的结合体,通过cluster可以实现主从和master重选功能,所以如果配置两个副本三个分片的话,就需要六个Redis实例。因为Redis的数据是根据一定规则分配到cluster的不同机器的,当数据量过大时,可以新增机器进行扩容。

使用集群,只需要将redis配置文件中的cluster-enable配置打开即可。每个集群中至少需要三个主数据库才能正常运行,新增节点非常方便。

cluster集群特点:

  • * 多个redis节点网络互联,数据共享
  • * 所有的节点都是一主一从(也可以是一主多从),其中从不提供服务,仅作为备用
  • * 不支持同时处理多个key(如MSET/MGET),因为redis需要把key均匀分布在各个节点上,
  • 并发量很高的情况下同时创建key-value会降低性能并导致不可预测的行为
  • * 支持在线增加、删除节点
  • * 客户端可以连接任何一个主节点进行读写
posted @ 2021-03-08 14:04  hzy_叶子  阅读(726)  评论(0编辑  收藏  举报