Redis 简介、安装、主从复制、哨兵、pub/sub、javaAPI

本文全面解析Redis,涵盖其原理、特性、数据类型、持久化机制、主从复制、哨兵模式及发布订阅功能,深入探讨Redis在高并发场景下的应用。

Redis

Redis简介及原理

什么是redis

Redis是由意大利人Salvatore Sanfilippo开发的一款内存高速缓存数据库。Redis本质上是一种键值数据库,但是它在保持键值数据库简单快捷特点的同时,又吸收了部分关系型数库的优点。从而使它的位置处于关系数据库和键值数据库之间。Redis不仅能保存String类型的数据,还能保持lists类型(有序)和Sets类型(无序)的数据,而且还能完成排序(sort)等高级功能,在实现INCR,SETNX等功能的时候,保证了其操作的原子性,除此之外,还支持主从复制功能。

Redis是一款开源的、高性能的键-值存储(key-value store)。它常被称作是一款数据结构服务器(data structure server)。

Redis的键值可以包括字符串(strings)类型,同时它还包括哈希(hashes)、列表(lists)、集合(sets)和 有序集合(sorted sets)等数据类型。 对于这些数据类型,你可以执行原子操作。例如:对字符串进行附加操作(append);递增哈希中的值;向列表中增加元素;计算集合的交集、并集与差集等。

为了获得优异的性能,Redis采用了内存中(in-memory)数据集(dataset)的方式。同时,Redis支持数据的持久化,你可以每隔一段时间将数据集转存到磁盘上(snapshot),或者在日志尾部追加每一条操作命令(append only file,aof)。

Redis同样支持主从复制(master-slave replication),并且具有非常快速的非阻塞首次同步( non-blocking first synchronization)、网络断开自动重连等功能。同时Redis还具有其它一些特性,其中包括简单的事物支持、发布订阅 ( pub/sub)、管道(pipeline)和虚拟内存(vm)等 。
Redis具有丰富的客户端,支持现阶段流行的大多数编程语言。

NoSQL数据库

在过去几年中,NoSQL数据库一度成为高并发、海量数据存储解决方案的代名词,与之相应的产品也如同雨后春笋般出现,然而在众多产品中,能够脱颖而出的却  屈指可数,如Redis、MongoDB、BerkeleyDB和CouchDB等。由于每种产品所拥有的特性不同,因此它们的应用场景也存在着一定差异。

BerkeleyDB是一种极为流行的开源嵌入式数据库,在更多情况下可用于存储引擎,比如BerlkeyDB再被Oracle收购之前曾作为MySQL的存储引擎,由此可见,该产品拥有极好的并发伸缩性,支持事物及嵌套事物,海量数据存储等重要特性,在用于存储实时数据方面具有极高的可用价值。

MongDB定义为Oriented-Document数据库服务器,和BerkeleyDB不同的是,该数据库可以像其他关系型数据库服务器那样独立的运行并提供相关的数据服务。MongoDB主要适用于论坛或博客等类型的网站,这些网站具有并发访问量高、多读少些、数据量大、逻辑关系简单、以文档数据作为主要数据源等特点,适合用MongoDB提供数据服务。

Memcahced,数据缓存服务器。在使用方式上,它和Redis最为相似,它们之间最大的区别是,memcached只是提供了数据缓存服务,而没有提供任何形式的数据持久化功能,而Redis则提供了这样的功能。一旦Memcached服务器宕机,之前在内存中缓存的数据也将全部消失。再有就是,Redis提供了更为丰富的数据存储结构

Redis,典型的NoSQL数据库服务器。与BerkeleyDB相比,它可以作为服务程序独立运行于自己的服务器主机。Redis除了Key/Value之外还支持List、Hash、Set和Ordered Set等数据结构,因此它的用途也更广泛。

redis特点

  • 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)。Redis用c语言编写,以内存作为数据存储介质,所以读写数据的效率极高,以设置和获取一个256字节字符串为例,它的读取速度可高达110000次/s,写速度高达81000次/s。

  • 支持丰富数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。

  • 支持事务:Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。

  • 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除

  • Redis支持主从模式:可以配置集群,这样更利于支撑大型的项目

  • 丰富的特性: Redis还支持 publish/subscribe, 通知, key 过期等等特性。

  • 存储在Redis中的数据是持久化的,断电或重启后,数据也不会丢失。因为Redis的存储分为内存存储、硬盘存储和log文件三部分。重启后,Redis可以从磁盘重新将数据加载到内存中,保证数据不会丢失

Redis安装

下载安装redis

$ wget http://download.redis.io/releases/redis-5.0.3.tar.gz

$ tar -zxvf redis-5.0.3.tar.gz

$ cd redis-5.0.3

$ make 

$ cd src

$ make test

# make test 报错 You need tcl 8.5 or newer in order to run the Redis test
$ yum install tcl

$ make install PREFIX=/usr/local/redis

# 拷贝redis的配置文件到etc目录
cp redis.conf /etc/redis/6379.conf

# 拷贝redis启动脚本到系统服务目录
$ cp utils/redis_init_script /etc/init.d/redis

# 修改redis启动脚本把这两行修改为你安装redis的路下的文件
$ vim /etc/init.d/redis

EXEC=/usr/local/redis/bin/redis-server
CLIEXEC=/usr/local/redis/bin/redis-cli

# 启动redis
$ service redis start

# 如果发现redis启动成功但是无法使用客户端远程连接需要修改redis的配置文件
`bind 127.0.0.1`改为`bind 0.0.0.0`

Redis 配置详解

  1. Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程

    daemonize yes

  2. 当Redis以守护进程方式运行时,Redis默认会把pid写入/var/run/redis_6379.pid文件,可以通过pidfile指定

    pidfile /var/run/redis_${REDISPORT}.pid

  3. 指定Redis监听端口,默认端口为6379,作者在自己的一篇博文中解释了为什么选用6379作为默认端口,因为6379在手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字

    port 6379

  4. 绑定的主机地址

    bind 127.0.0.1

  5. 当 客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能

    timeout 300

  6. 指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为verbose

    loglevel verbose

  7. 日志记录方式,默认为标准输出,如果配置Redis为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给/dev/null

    logfile stdout

  8. 设置数据库的数量,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id

    databases 16

  9. 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合

    save <seconds> <changes>
    Redis 默认配置文件中提供了三个条件:
    save 900 1
    save 300 10
    save 60 10000
    

    分别表示900秒(15分钟)内有1个更改,300秒(5分钟)内有10个更改以及60秒内有10000个更改。

  10. 指定存储至本地数据库时是否压缩数据,默认为yes,Redis采用LZF压缩,如果为了节省CPU时间,可以关闭该选项,但会导致数据库文件变的巨大

    rdbcompression yes

  11. 指定本地数据库文件名,默认值为dump.rdb

    dbfilename dump.rdb

  12. 指定本地数据库存放目录

    dir ./

  13. 设置当本机为slav服务时,设置master服务的IP地址及端口,在Redis启动时,它会自动从master进行数据同步

    slaveof <masterip> <masterport>

  14. 当master服务设置了密码保护时,slav服务连接master的密码

    masterauth <master-password>

  15. 设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过AUTH 命令提供密码,默认关闭

    requirepass foobared

  16. 设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回max number of clients reached错误信息

    maxclients 128

  17. 指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区

    maxmemory <bytes>

  18. 指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no

    appendonly no

  19. 指定更新日志文件名,默认为appendonly.aof

    appendfilename appendonly.aof

  20. 指定更新日志条件,共有3个可选值: no:表示等操作系统进行数据缓存同步到磁盘(快) always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全 everysec:表示每秒同步一次(折衷,默认值)

    appendfsync everysec

  21. 指定是否启用虚拟内存机制,默认值为no,简单的介绍一下,VM机制将数据分页存放,由Redis将访问量较少的页即冷数据swap到磁盘上,访问多的页面由磁盘自动换出到内存中(在后面的文章我会仔细分析Redis的VM机制)

    vm-enabled no

  22. 虚拟内存文件路径,默认值为/tmp/redis.swap,不可多个Redis实例共享

    vm-swap-file /tmp/redis.swap

  23. 将所有大于vm-max-memory的数据存入虚拟内存,无论vm-max-memory设置多小,所有索引数据都是内存存储的(Redis的索引数据 就是keys),也就是说,当vm-max-memory设置为0的时候,其实是所有value都存在于磁盘。默认值为0

    vm-max-memory 0

  24. Redis swap文件分成了很多的page,一个对象可以保存在多个page上面,但一个page上不能被多个对象共享,vm-page-size是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page大小最好设置为32或者64bytes;如果存储很大大对象,则可以使用更大的page,如果不 确定,就使用默认值

    vm-page-size 32

  25. 设置swap文件中的page数量,由于页表(一种表示页面空闲或使用的bitmap)是在放在内存中的,,在磁盘上每8个pages将消耗1byte的内存。

    vm-pages 134217728

  26. 设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4

    vm-max-threads 4

  27. 设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启

    glueoutputbuf yes

  28. 指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法

    hash-max-zipmap-entries 64
    hash-max-zipmap-value 512

  29. 指定是否激活重置哈希,默认为开启

    activerehashing yes

  30. 指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件

    include /path/to/local.conf

Redis数据类型

  • String

Strings 数据结构是简单的key-value类型,value其实不仅是String,也可以是数字.
常用命令: set,get,decr,incr,mget 等。

应用场景:String是最常用的一种数据类型,普通的key/ value 存储都可以归为此类.即可以完全实现目前 Memcached 的功能,并且效率更高。还可以享受Redis的定时持久化,操作日志及 Replication等功能。

  • Hash

常用命令:hget,hset,hgetall 等。

应用场景:在Memcached中,我们经常将一些结构化的信息打包成HashMap,在客户端序列化后存储为一个字符串的值,比如用户的昵称、年龄、性别、积分等,这时候在需要修改其中某一项时,通常需要将所有值取出反序列化后,修改某一项的值,再序列化存储回去。这样不仅增大了开销,也不适用于一些可能并发操作的场合(比如两个并发的操作都需要修改积分)。而Redis的Hash结构可以使你像在数据库中Update一个属性一样只修改某一项属性值。我们简单举个实例来描述下Hash的应用场景,比如我们要存储一个用户信息对象数据,包含以下信息:用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,

  • List

常用命令:lpush,rpush,lpop,rpop,lrange等。

应用场景:Redis list的应用场景非常多,也是Redis最重要的数据结构之一,比如twitter的关注列表,粉丝列表等都可以用Redis的list结构来实现。Lists 就是链表,相信略有数据结构知识的人都应该能理解其结构。使用Lists结构,我们可以轻松地实现最新消息排行等功能。Lists的另一个应用就是消息队列,
可以利用Lists的PUSH操作,将任务存在Lists中,然后工作线程再用POP操作将任务取出进行执行。Redis还提供了操作Lists中某一段的api,你可以直接查询,删除Lists中某一段的元素。

  • Set

常用命令:sadd,spop,smembers,sunion 等。

应用场景:Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。

Sets 集合的概念就是一堆不重复值的组合。利用Redis提供的Sets数据结构,可以存储一些集合性的数据,比如在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。

  • Sorted Set

常用命令:zadd,zrange,zrem,zcard等

使用场景:Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构,比如twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。

另外还可以用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。

Redis的持久化方式RDB和AOF

二者的区别

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

二者优缺点

RDB存在哪些优势呢?
1). 一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。

2). 对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。

3). 性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。

4). 相比于AOF机制,如果数据集很大,RDB的启动效率会更高。

RDB又存在哪些劣势呢?

1). 如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。

2). 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。

AOF的优势有哪些呢?
1). 该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。至于无同步,无需多言,我想大家都能正确的理解它。

2). 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。

3). 如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。

4). AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。

AOF的劣势有哪些呢?

1). 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

2). 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。

二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。rdb这个就更有些 eventually consistent的意思了。

Redis持久化常用配置

RDB持久化配置

Redis会将数据集的快照dump到dump.rdb文件中。此外,我们也可以通过配置文件来修改Redis服务器dump快照的频率,在打开6379.conf文件之后,我们搜索save,可以看到下面的配置信息:

# RDB持久化配置

save 900 1             #在900秒(15分钟)之后,如果至少有1个key发生变化,则dump内存快照。

save 300 10            #在300秒(5分钟)之后,如果至少有10个key发生变化,则dump内存快照。

save 60 10000          #在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照。

# AOF持久化配置,在Redis的配置文件中存在三种同步方式,它们分别是:

appendfsync always     #每次有数据修改发生时都会写入AOF文件。

appendfsync everysec   #每秒钟同步一次,该策略为AOF的缺省策略。

appendfsync no          #从不同步。高效但是数据不会被持久化。

主从复制

机器规划

需要准备三台安装好redis linux服务器我这里用的是CentOS7.5

192.168.79.15 master
192.168.79.16 slave1
192.168.79.17 slave1

# 编辑salve1、slave2 的配置文件

$ vim /etc/redis/6379.conf

# 添加如下配置(新版本的redis 使用 replicaof 代替了 slaveof)
replicaof 192.168.79.15 6379

# 配置完成后启动redis集群,查看redis的角色

> info replication

# 模拟主节点挂掉后,手动将slave1子节点升级为Master
# 在 master redis 客户端执行
> shutdown

# 在是 slave redis 客户度执行
> slaveof no one

Redis哨兵(Sentinel)模式

哨兵模式概述

Sentinel(哨兵)进程是用于监控redis集群中Master主服务器工作的状态,在Master主服务器发生故障的时候,可以实现Master和Slave服务器的切换,保证系统的高可用,其已经被集成在redis2.6+的版本中,Redis的哨兵模式到了2.8版本之后就稳定了下来。一般在生产环境也建议使用Redis的2.8版本的以后版本。哨兵(Sentinel) 是一个分布式系统,你可以在一个架构中运行多个哨兵(sentinel) 进程,这些进程使用流言协议(gossipprotocols)来接收关于Master主服务器是否下线的信息,并使用投票协议(Agreement Protocols)来决定是否执行自动故障迁移,以及选择哪个Slave作为新的Master。每个哨兵(Sentinel)进程会向其它哨兵(Sentinel)、Master、Slave定时发送消息,以确认对方是否”活”着,如果发现对方在指定配置时间(可配置的)内未得到回应,则暂时认为对方已掉线,也就是所谓的”主观认为宕机” ,英文名称:Subjective Down,简称SDOWN。有主观宕机,肯定就有客观宕机。当“哨兵群”中的多数Sentinel进程在对Master主服务器做出 SDOWN 的判断,并且通过 SENTINEL is-master-down-by-addr 命令互相交流之后,得出的Master Server下线判断,这种方式就是“客观宕机”,英文名称是:Objectively Down, 简称 ODOWN。通过一定的vote算法,从剩下的slave从服务器节点中,选一台提升为Master服务器节点,然后自动修改相关配置,并开启故障转移(failover)。

哨兵(sentinel) 虽然有一个单独的可执行文件 redis-sentinel ,但实际上它只是一个运行在特殊模式下的 Redis 服务器,你可以在启动一个普通 Redis 服务器时通过给定 --sentinel 选项来启动哨兵(sentinel),哨兵(sentinel) 的一些设计思路和zookeeper非常类似。

Sentinel集群之间会互相通信,沟通交流redis节点的状态,做出相应的判断并进行处理,这里的主观下线状态和客观下线状态是比较重要的状态,它们决定了是否进行故障转移,可以 通过订阅指定的频道信息,当服务器出现故障得时候通知管理员,客户端可以将 Sentinel 看作是一个只提供了订阅功能的 Redis 服务器,你不可以使用 PUBLISH 命令向这个服务器发送信息,但你可以用 SUBSCRIBE 命令或者 PSUBSCRIBE 命令, 通过订阅给定的频道来获取相应的事件提醒。一个频道能够接收和这个频道的名字相同的事件。 比如说, 名为 +sdown 的频道就可以接收所有实例进入主观下线(SDOWN)状态的事件。

Sentinel(哨兵)进程的作用:

1.监控(Monitoring): 哨兵(sentinel) 会不断地检查你的Master和Slave是否运作正常。

2.提醒(Notification):当被监控的某个Redis节点出现问题时, 哨兵(sentinel) 可以通过 API 向管理员或者其他应用程序发送通知。

3.自动故障迁移(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的监控目标会随之调换。


# 拷贝redis解压目录中的哨兵(sentinel.conf)配置文件
$ cp sentinel.conf /etc/redis

# 主要修改一下配置文件其他默认即可

# 哨兵进程的端口
port 26379

# 后台运行
daemonize yes

# 监听的节点
sentinel monitor mymaster 192.168.79.15 6379 1

# 哨兵的日志文件
logfile "/export/logs/redis/sentinel.log"

# redis 的密码
sentinel auth-pass mymaster "123456"

redis 哨兵模式java客户端调用

/**
 * <p> redis 哨兵集群
 *
 * @author leone
 * @since 2019-01-29
 **/
public class RedisSentinelTest {

    @SuppressWarnings("deprecation")
    public static void main(String[] args) {

        Set<String> sentinels = new HashSet<>();

        sentinels.add("192.168.79.15:26379");
        sentinels.add("192.168.79.16:26379");
        sentinels.add("192.168.79.17:26379");

        String clusterName = "mymaster";
        String password = "123456";

        JedisSentinelPool redisSentinelPool = new JedisSentinelPool(clusterName, sentinels, password);

        Jedis jedis = null;
        try {
            jedis = redisSentinelPool.getResource();
            jedis.set("k10", "v10");
            System.out.println(jedis.get("k10"));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            redisSentinelPool.returnBrokenResource(jedis);
        }
        redisSentinelPool.close();
    }

}

发布订阅(pub/sub)

特性介绍:

什么是redis的发布订阅(pub/sub)? Pub/Sub功能(means Publish, Subscribe)即发布及订阅功能。基于事件的系统中,Pub/Sub是目前广泛使用的通信模型,它采用事件作为基本的通信机制,提供大规模系统所要求的松散耦合的交互模式:订阅者(如客户端)以事件订阅的方式表达出它有兴趣接收的一个事件或一类事件;发布者(如服务器)可将订阅者感兴趣的事件随时通知相关订阅者。熟悉设计模式的朋友应该了解这与23种设计模式中的观察者模式极为相似。

同样,Redis的pub/sub是一种消息通信模式,主要的目的是解除消息发布者和消息订阅者之间的耦合, Redis作为一个pub/sub的server, 在订阅者和发布者之间起到了消息路由的功能。

简单来讲,这里面还有个channel的概念,这里就是频道的意思,比如你订阅了银行的频道,当你的资金发生变动时,银行就会通过它的频道给你发送信息,在这里,你是属于被动接收的,而不是向银行索要信息,这个例子中,你就是sub(订阅者),而银行就是pub(发布者)。

代码示例

  • Subscriber.java
import redis.clients.jedis.JedisPubSub;

public class Subscriber extends JedisPubSub {

    /**
     * 收到消息会调用
     *
     * @param channel
     * @param message
     */
    @Override
    public void onMessage(String channel, String message) {
        System.out.println(String.format("receive redis published message, channel: %s, message: %s", channel, message));
    }

    /**
     * 订阅了频道会调用
     *
     * @param channel
     * @param subscribedChannels
     */
    @Override
    public void onSubscribe(String channel, int subscribedChannels) {
        System.out.println(String.format("subscribe redis channel success, channel: %s, subscribedChannels: %d", channel, subscribedChannels));
    }

    /**
     * 取消订阅 会调用
     *
     * @param channel
     * @param subscribedChannels
     */
    @Override
    public void onUnsubscribe(String channel, int subscribedChannels) {
        System.out.println(String.format("unsubscribe redis channel, channel: %s, subscribedChannels: %d", channel, subscribedChannels));

    }
}
  • SubThread.java
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class SubThread extends Thread {

    private final JedisPool jedisPool;

    private final Subscriber subscriber = new Subscriber();

    private final String channel = "channel1";

    public SubThread(JedisPool jedisPool) {
        super("SubThread");
        this.jedisPool = jedisPool;
    }

    @Override
    public void run() {
        System.out.println(String.format("subscribe redis, channel %s, thread will be blocked", channel));
        Jedis jedis = null;
        try {
            // 取出一个连接
            jedis = jedisPool.getResource();
            // 通过subscribe 的api去订阅,入参是订阅者和频道名
            jedis.subscribe(subscriber, channel);
        } catch (Exception e) {
            System.out.println(String.format("subscribe channel error, %s", e));
        } finally {
            jedis.close();
        }
    }
}
  • Publisher.java
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Publisher extends Thread {

    private final JedisPool jedisPool;

    public Publisher(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }

    @Override
    public void run() {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        // 连接池中取出一个连接
        Jedis jedis = jedisPool.getResource();
        while (true) {
            String line;
            try {
                line = reader.readLine();
                if (!"quit".equals(line)) {
                    // 从 channel1 的频道上推送消息
                    jedis.publish("channel1", line);
                } else {
                    break;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  • RedisPubSubTest.java
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * <p>
 *
 * @author leone
 * @since 2019-01-29
 **/
public class RedisPubSubTest {


    public static void main(String[] args) {
        // 连接池配置对象 (config, host + port + timeout + password + db)
        JedisPool jedisPool = new JedisPool(new JedisPoolConfig(), "192.168.79.17", 6379, 5000, "123456", 1);

        System.out.println(String.format("redis pool is starting, redis ip %s, redis port %d", "127.0.0.1", 6379));

        // 订阅者
        SubThread subThread = new SubThread(jedisPool);
        subThread.start();

        // 发布者
        Publisher publisher = new Publisher(jedisPool);
        publisher.start();
    }
}

Redis javaAPI

import org.junit.Test;
import redis.clients.jedis.*;
import java.util.HashMap;
import java.util.Map;

/**
 * <p>
 *
 * @author leone
 * @since 2018-12-19
 **/
public class RedisClient {

    private static final String HOST = "127.0.0.1";

    private static final Integer PORT = 6379;

    private static final Integer TIME_OUT = 15000;

    private static Jedis jedis;


    private static final JedisPool jedisPool;

    static {
        JedisPoolConfig config = new JedisPoolConfig();
        // 最大连接数
        config.setMaxTotal(32);
        // 闲置最大连接数
        config.setMaxIdle(6);
        // 闲置最小连接数
        config.setMinIdle(0);
        // 到达最大连接数后,调用者阻塞时间
        config.setMaxWaitMillis(15000);
        // 连接空闲的最小时间,可能被移除
        config.setMinEvictableIdleTimeMillis(300000);
        // 连接空闲的最小时间,多余最小闲置连接的将被移除
        config.setSoftMinEvictableIdleTimeMillis(-1);
        // 设置每次检查闲置的个数
        config.setNumTestsPerEvictionRun(3);
        // 申请连接时,是否检查连接有效
        config.setTestOnBorrow(false);
        // 返回连接时,是否检查连接有效
        config.setTestOnReturn(false);
        // 空闲超时,是否执行检查有效
        config.setTestWhileIdle(false);
        // 空闲检查时间
        config.setTimeBetweenEvictionRunsMillis(60000);
        // 当连接数耗尽,是否阻塞
        config.setBlockWhenExhausted(true);
        // 连接池配置对象 (host + port + timeout + password + db)
        jedisPool = new JedisPool(config, HOST, PORT, TIME_OUT, null, 1);
        jedis = jedisPool.getResource();
    }


    /**
     * set and get string
     */
    @Test
    public void testString() {
        System.out.println("----------------redis-String-----------------");
        // set:返回操作结果
        System.out.println("name=>wsy:" + jedis.set("name", "wsy"));

        // get:value
        System.out.println("name:" + jedis.get("name"));

        // append:字符串长度
        System.out.println("append:" + jedis.append("name", "_ss"));

        // strLength:字符串长度
        System.out.println("strLength:" + jedis.strlen("name"));

        // getrange:返回不包括起始坐标的值
        System.out.println("getrange:" + jedis.getrange("name", 10, 13));

        // setrange:从起始坐标考试替换,未替换的保持
        System.out.println("setRange:" + jedis.setrange("name", 10, "#"));

        // mset:批量设置,返回批量设置结果
        System.out.println("mset:" + jedis.mset("name", "wsy", "age", "29"));

        // mget:返回数组
        System.out.println("mget:" + jedis.mget("name", "age"));

        // incr:value自增1后,返回value
        System.out.println("incr:" + jedis.incr("age"));

        // incr:value自增传参值后,返回value
        System.out.println("incrBy:" + jedis.incrBy("age", 3));

        // decr:value自减1,返回value
        System.out.println("decr:" + jedis.decr("age"));

        // decrBy:value自减入参值,返回value
        System.out.println("decrBy:" + jedis.decrBy("age", 3));

        // setex:设置key值+有效时间,如果key存在则覆盖value
        System.out.println("setex:" + jedis.setex("phone", 10, "13600000001"));

        // setnx:当key不存在时,设置才成功
        System.out.println("setnx:" + jedis.setnx("address", "china"));

        // del:删除对应key
        System.out.println("del:" + jedis.del("address1"));

        System.out.println("----------------redis-String-----------------\n");

    }


    /**
     * redis中hash类型常用操作
     */
    @Test
    public void testHash() {
        System.out.println("----------------redis-HashMap-----------------");
        // hset:返回值为key为新返回1,为旧覆盖旧值返回0
        System.out.println("hset:" + jedis.hset("user", "name", "wangshaoyi"));

        Map<String, String> map = new HashMap<>();

        map.put("name", "wsy");
        map.put("age", "29");

        // hmset:map对象
        System.out.println("hmset:" + jedis.hmset("user", map));

        // hexists:判断hashmap中key是否存在
        System.out.println("hexists:" + jedis.hexists("user", "age"));

        // hget:获取map中key对应的value
        System.out.println("hget:" + jedis.hget("user", "name"));

        // hgetAll:获取map中所有对象
        System.out.println("hgetAll:" + jedis.hgetAll("user"));

        // hkeys:获取map中所有key
        System.out.println("hkeys:" + jedis.hkeys("user"));

        // hvals:获取map中所有value
        System.out.println("hvals:" + jedis.hvals("user"));


        // hmget:批量获取keys的对象,返回List
        System.out.println("hmget:" + jedis.hmget("user", "age", "name"));

        // hlen:map的大小
        System.out.println("hlen:" + jedis.hlen("user"));

        // hdel:删除map中对应key,正确删除返回1
        System.out.println("hdel:" + jedis.hdel("user", "age0"));

        System.out.println("----------------redis-HashMap-----------------\n");

    }

    /**
     * redis中 list 类型常用操作
     */
    @Test
    public void testList() {
        System.out.println("----------------redis-List-----------------");
        jedis.del("contacts");
        jedis.del("contacts_old");

        // lpush:批量头部插入,返回List的size
        System.out.println("lpush:" + jedis.lpush("contacts", "xx", "yy", "zz"));

        // lpushx:单个头部插入,返回List的size
        System.out.println("lpushx:" + jedis.lpushx("contacts", "aa"));

        // linsert:指定对象位置(前or后)插入
        System.out.println("linsert:" + jedis.linsert("contacts", BinaryClient.LIST_POSITION.BEFORE, "zz", "bb"));

        // lset:将指定的位置设置值(替换旧值)
        System.out.println("lset:" + jedis.lset("contacts", 2, "cc"));

        // lpop:链表头的对象
        System.out.println("lpop:" + jedis.lpop("contacts"));

        // lrange:获取list指定start、end位置value
        System.out.println("lrange:" + jedis.lrange("contacts", 1, 3));

        // ltrim:只剩start\end中list值,其余删除
        System.out.println("ltrim:" + jedis.ltrim("contacts", 1, 3));

        // lrem:删除list指定值(次数指定),返回删除个数
        System.out.println("lrem:" + jedis.lrem("contacts", 2, "yy"));

        // rpoplpush:将源list尾部对象移到目标list对象头部
        System.out.println("rpoplpush:" + jedis.rpoplpush("contacts", "contacts_old"));

        // rpush:在list尾部对象添加值
        System.out.println("rpush:" + jedis.rpush("contacts", "aa", "bb"));

        // rpop:移除在list尾部值,返回移除的对象
        System.out.println("rpop:" + jedis.rpop("contacts"));

        // brpop:阻塞尾部对象抛出,指定超时时间,返回抛出值
        System.out.println("brpop:" + jedis.brpop(1, "contacts"));

        System.out.println("blpop:" + jedis.blpop(1, "contacts"));

        System.out.println("blpop(阻塞1秒返回):" + jedis.blpop(1, "contacts"));

        System.out.println("----------------redis-List-----------------\n");
    }

    /**
     * redis中 set 类型常用操作
     */
    @Test
    public void testSet() {
        System.out.println("----------------redis-Set-----------------");
        jedis.del("phones");
        jedis.del("phones_old");
        jedis.del("phones_old_1");
        jedis.del("phones_new");


        // sadd:集合添加元素,返回添加成功后数据
        System.out.println("sadd:" + jedis.sadd("phones", "13600000001", "13300000001"));
        System.out.println("sadd:" + jedis.sadd("phones", "13600000002", "13300000002"));

        // scard:返回集合中元素数
        System.out.println("scard:" + jedis.scard("phones"));

        jedis.sadd("phones_old", "13600000002");
        jedis.sadd("phones_old_1", "13300000001");

        // sdiff:首set与其他set之间的差集,返回差集值
        System.out.println("sdiff:" + jedis.sdiff("phones", "phones_old", "phones_old_1"));

        // sdiffstore:首set与其他set之间的差集保存至新set,返回差集数
        System.out.println("sdiffstore:" + jedis.sdiffstore("phones_new", "phones", "phones_old"));

        // sinter:返回集合的交集
        System.out.println("sinter:" + jedis.sinter("phones", "phones_new"));

        // sismember:判断value是否为set的值
        System.out.println("sismember:" + jedis.sismember("phones", "13600000001"));

        // smembers:返回集合中成员
        System.out.println("smembers:" + jedis.smembers("phones"));

        // smove:将首源set中元素移动目标set,返回移动数
        System.out.println("smove:" + jedis.smove("phones", "phones_new", "13600000002"));

        // spop:随机移除set的一元素,返回移除元素
        System.out.println("spop:" + jedis.spop("phones"));

        // srandmember:随机取出集合中一个元素
        System.out.println("srandmember:" + jedis.srandmember("phones_new"));

        // srem:删除集合中指定元素
        System.out.println("srem:" + jedis.srem("phones_new", "13600000002"));

        // sunion:集合中并集
        System.out.println("sunion:" + jedis.sunion("phones", "phones_new", "phones_old"));

        System.out.println("----------------redis-Set-----------------\n");
    }

    /**
     * redis中 SortedSet 类型常用操作
     */
    @Test
    public void testSortedSet() {
        System.out.println("----------------redis-SortedSet-----------------");
        jedis.del("scores");
        jedis.del("scores_1");
        jedis.del("scores_total");
        jedis.del("score_inter");
        jedis.del("score_max");

        // zadd:sortedSet添加元素
        System.out.println("zadd:" + jedis.zadd("scores", 610.5, "xx"));
        jedis.zadd("scores", 630, "yy");

        // zcard:返回sortedset中元素数
        System.out.println("zcard:" + jedis.zcard("scores"));

        // zcount:返回指定分值(包括)的元素数
        System.out.println("zcount:" + jedis.zcount("scores", 610, 620));

        // zincrby:将指定值分数加分,返回加后的分数
        System.out.println("zincrby:" + jedis.zincrby("scores", 10, "xx"));

        // zrange:返回指定坐标的值
        System.out.println("zrange:" + jedis.zrange("scores", 0, 1));

        // zrangeByScore:返回指定分数范围内的对象
        System.out.println("zrangeByScore:" + jedis.zrangeByScore("scores", 600, 700));

        // zrank:返回指定值的位置(分数低->高,0开始)
        System.out.println("zrank:" + jedis.zrank("scores", "yy"));

        // zrevrank:返回指定值的位置(分数高->低,0开始)
        System.out.println("zrevrank:" + jedis.zrevrank("scores", "yy"));


        // zrem:删除,其中还有zremrangeByRank\zremrangeByScore
        System.out.println("zrem:" + jedis.zrem("scores", "yy"));

        jedis.zadd("scores", 630, "yy");
        jedis.zadd("scores", 640, "zz");
        // zrevrange:获取指定位置数据(分数从高->低)
        System.out.println(":" + jedis.zrevrange("scores", 0, 1));

        System.out.println("zrangeByScoreWithScores:" + jedis.zrangeByScoreWithScores("scores", 600, 700));

        // zscore:获取指定分数
        System.out.println("zscore:" + jedis.zscore("scores", "xx"));
        jedis.zadd("scores_1", 630.5, "xx");
        jedis.zadd("scores_1", 610.5, "bb");
        jedis.zadd("scores_1", 622.5, "cc");

        // zunionstore:sortedset集合的并集并保存,如果集合中元素相同,则分数相加
        System.out.println("zunionstore:" + jedis.zunionstore("score_total", "scores", "scores_1"));

        ZParams zParams = new ZParams();
        zParams.aggregate(ZParams.Aggregate.MAX);// 指定分数操作:+,最小,最大
        zParams.weightsByDouble(1, 0.1);// 分数中的乘法因子
        System.out.println("zunionstore:" + jedis.zunionstore("score_max", zParams, "scores", "scores_1"));

        // zinterstore:集合元素取交集,相同元素值相加(默认)
        System.out.println("zinterstore:" + jedis.zinterstore("score_inter", "scores", "scores_1"));
        System.out.println("----------------redis-SortedSet-----------------\n");
    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值