Redis
第一节 Redis简介
1.1 NoSQL
NoSQL,泛指非关系型的数据库,NoSQL即Not-Only SQL,它可以作为关系型数据库的良好补充。随着互联网web2.0网站的兴起,非关系型的数据库现在成了一个极其热门的新领域,非关系数据库产品的发展非常迅速
而传统的关系数据库在应付web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,例如:
- High performance - 对数据库高并发读写的需求
web2.0网站要根据用户个性化信息来实时生成动态页面和提供动态信息,所以基本上无法使用动态页面静态化技术,因此数据库并发负载非常高,往往要达到每秒上万次读写请求。关系数据库应付上万次SQL查询还勉强顶得住,但是应付上万次SQL写数据请求,硬盘IO就已经无法承受了。其实对于普通的BBS网站,往往也存在对高并发写请求的需求,例如网站的实时统计在线用户状态,记录热门帖子的点击次数,投票计数等,因此这是一个相当普遍的需求。 - Huge Storage - 对海量数据的高效率存储和访问的需求
类似Facebook,twitter,Friendfeed这样的SNS网站,每天用户产生海量的用户动态,以Friendfeed为例,一个月就达到了2.5亿条用户动态,对于关系数据库来说,在一张2.5亿条记录的表里面进行SQL查询,效率是极其低下乃至不可忍受的。再例如大型web网站的用户登录系统,例如腾讯,盛大,动辄数以亿计的帐号,关系数据库也很难应付。 - High Scalability && High Availability- 对数据库的高可扩展性和高可用性的需求
在基于web的架构当中,数据库是最难进行横向扩展的,当一个应用系统的用户量和访问量与日俱增的时候,你的数据库却没有办法像web server和app server那样简单的通过添加更多的硬件和服务节点来扩展性能和负载能力。对于很多需要提供24小时不间断服务的网站来说,对数据库系统进行升级和扩展是非常痛苦的事情,往往需要停机维护和数据迁移,为什么数据库不能通过不断的添加服务器节点来实现扩展呢?
NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战,尤其是大数据应用难题
1.2 NoSQL的类别
1.2.1键值(Key-Value)存储数据库
相关产品: Tokyo Cabinet/Tyrant、 Redis、Voldemort、Berkeley DB
典型应用: 内容缓存,主要用于处理大量数据的高访问负载。
数据模型: 一系列键值对
优势: 快速查询
劣势: 存储的数据缺少结构化
1.2.2 列存储数据库
相关产品:Cassandra, HBase, Riak
典型应用:分布式的文件系统
数据模型:以列簇式存储,将同一列数据存在一起
优势:查找速度快,可扩展性强,更容易进行分布式扩展
劣势:功能相对局限
1.2.3 文档型数据库
相关产品:CouchDB、MongoDB
典型应用:Web应用(与Key-Value类似,Value是结构化的)
数据模型: 一系列键值对
优势:数据结构要求不严格
劣势: 查询性能不高,而且缺乏统一的查询语法
1.2.4 图形(Graph)数据库
相关数据库:Neo4J、InfoGrid、Infinite Graph
典型应用:社交网络
数据模型:图结构
优势:利用图结构相关算法。
劣势:需要对整个图做计算才能得出结果,不容易做分布式的集群方案。
1.3 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是用C语言开发的一个开源的高性能键值对(key-value)数据库。
它通过提供多种键值数据类型来适应不同场景下的存储需求,目前为止Redis支持的键值数据类型如下:
字符串类型
散列类型
列表类型
集合类型
有序集合类型
Redis 与其他 key - value 缓存产品有以下三个特点:
- Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
- Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
- Redis支持数据的备份,即master-slave模式的数据备份。
Redis 提供的API支持:C、C++、C#、Clojure、Java、JavaScript、Lua、PHP、Python、Ruby、Go、Scala、Perl等多种语言。
1.4 Redis的应用场景
目前全球最大的Redis用户是新浪微博,在新浪有200多台物理机,400多个端口正在运行Redis,有+4G的数据在Redis上来为微博用户提供服务
- 取最新的N个数据(取最新文档、排行榜等)
- 需要精确设定过期时间的应用
- 计数器应用
- 实时性要求的高并发读写
- 消息系统Pub/Sub
- 构建队列
- 缓存
1.5 Redis优缺点
1.5.1 Redis 优势
对数据高并发读写(基于内存)
对海量数据的高效率存储和访问(基于内存)
对数据的可扩展性和高可用性
垂直扩展:提升硬件
水平扩展:集群
1.5.2 Redis 缺点
redis(ACID处理非常简单)无法做到太复杂的关系数据库模型
1.6 Redis面向互联网的解决方案
- 主从:一主多从,主机可写,从机备份。类似于Mysql的读写分离,存在问题是一但主节点down掉,整个Redis不可用。
- 哨兵(2.x):启用一个哨兵程序(节点),监控其余节点的状态,根据选举策略,进行主从切换。
缺点:每个节点的数据依旧是一致的,仍无法实现分布式的数据库。- 集群(3.x):结合上述两种模式,多主多从,实现高可用、分布式数据存储
第二节 Redis的安装
2.1 下载
从官网下载,Redis官网点击下载
通过XShell将下载的文件上传到/usr/local目录
解压&编译&安装
cd /usr/local 切换到指定目录
tar -zxvf redis-3.2.11.tar.gz 解压
cd redis-3.2.11 切换到解压目录
make 编译
make PREFIX=/usr/local/redis install 指定安装目录进行安装
源码目录分析:
在/usr/local/src/redis3.2/下有一个redis.conf文件,这个文件为redis核心配置文件。
在/usr/local/src/redis3.2/src/下,有redis的常用命令,安装完成后,会将这些命令自动放入到安装路径下的bin目录下
在/usr/local/src/redis3.2/utils/下,有redis的服务启动脚本
2.2 配置和启动
redis.conf是redis的配置文件,redis.conf在redis源码目录。
注意修改port作为redis进程的端口,port默认6379。
拷贝配置文件到安装目录下
进入源码目录,里面有一份配置文件 redis.conf,修改然后将其拷贝到安装路径下
cd /usr/local/redis 切换目录
mkdir conf 创建配置文件的目录
cp /usr/local/redis-3.2.11/redis.conf /usr/local/redis/conf 复制配置文件
./redis-server 启动
redis关闭的方式
pkill redis server
kill 进程号
/usr/local/redis/bin/redis-cli shutdown
注意:
这里直接执行Redis-server 启动的Redis服务,是在前台直接运行的(效果如上图),也就是说,执行完该命令后,如果Lunix关闭当前会话,则Redis服务也随即关闭。正常情况下,启动Redis服务需要从后台启动,并且指定启动配置文件。
2.3 后端模式启动
修改redis.conf配置文件, 设置 daemonize yes
以后端模式启动。
ps aux|grep redis 查询redis是否启动
./bin/redis-server ./redis.conf 启动redis
补充1:redis desktop manager连接远程服务器redis
- 修改redis.conf配置文件
- 注释掉bind绑定配置
# bind 127.0.0.1
- 搜索并修改为
protected-mode no
。关闭保护模式,使其他主机的客户端能够连接到该Redis服务器。 - 搜索并修改为
requirepass yourpassword
。防止外部未知主机的客户端破解并进行Redis连接,设置连接密码。 - 注意:如果你的redis服务器是在华为云/阿里云服务器上自建的,默认redis端口6379是不允许外部访问的。解决办法:在华为云/阿里云控制台的安全组管理中,开启6379端口。
- 设置完成后,打开软件,如图设置即可。
补充2:redis.conf配置文件详解
4.1. Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
daemonize no
4.2. 当Redis以守护进程方式运行时,Redis默认会把pid写入/var/run/redis.pid文件,可以通过pidfile指定
pidfile /var/run/redis.pid
4.3. 指定Redis监听端口,默认端口为6379,作者在自己的一篇博文中解释了为什么选用6379作为默认端口,因为6379在手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字
port 6379
4.4. 绑定的主机地址
bind 127.0.0.1
4.5.当 客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能
timeout 300
4.6. 指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为verbose
loglevel verbose
4.7. 日志记录方式,默认为标准输出,如果配置Redis为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给/dev/null
logfile stdout
4.8. 设置数据库的数量,默认数据库为0,可以使用SELECT <dbid>命令在连接上指定数据库id
databases 16
4.9. 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
save <seconds> <changes>
Redis默认配置文件中提供了三个条件:
save 900 1
save 300 10
save 60 10000
分别表示900秒(15分钟)内有1个更改,300秒(5分钟)内有10个更改以及60秒内有10000个更改。
4.10. 指定存储至本地数据库时是否压缩数据,默认为yes,Redis采用LZF压缩,如果为了节省CPU时间,可以关闭该选项,但会导致数据库文件变的巨大
rdbcompression yes
4.11. 指定本地数据库文件名,默认值为dump.rdb
dbfilename dump.rdb
4.12. 指定本地数据库存放目录
dir ./
4.13. 设置当本机为slav服务时,设置master服务的IP地址及端口,在Redis启动时,它会自动从master进行数据同步
slaveof <masterip> <masterport>
4.14. 当master服务设置了密码保护时,slav服务连接master的密码
masterauth <master-password>
4.15. 设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过AUTH <password>命令提供密码,默认关闭
requirepass foobared
4.16. 设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回max number of clients reached错误信息
maxclients 128
4.17. 指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区
maxmemory <bytes>
4.18. 指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no
appendonly no
4.19. 指定更新日志文件名,默认为appendonly.aof
appendfilename appendonly.aof
4.20. 指定更新日志条件,共有3个可选值:
no:表示等操作系统进行数据缓存同步到磁盘(快)
always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全)
everysec:表示每秒同步一次(折衷,默认值)
appendfsync everysec
4.21. 指定是否启用虚拟内存机制,默认值为no,简单的介绍一下,VM机制将数据分页存放,由Redis将访问量较少的页即冷数据swap到磁盘上,访问多的页面由磁盘自动换出到内存中(在后面的文章我会仔细分析Redis的VM机制)
vm-enabled no
4.22. 虚拟内存文件路径,默认值为/tmp/redis.swap,不可多个Redis实例共享
vm-swap-file /tmp/redis.swap
4.23. 将所有大于vm-max-memory的数据存入虚拟内存,无论vm-max-memory设置多小,所有索引数据都是内存存储的(Redis的索引数据 就是keys),也就是说,当vm-max-memory设置为0的时候,其实是所有value都存在于磁盘。默认值为0
vm-max-memory 0
4.24. Redis swap文件分成了很多的page,一个对象可以保存在多个page上面,但一个page上不能被多个对象共享,vm-page-size是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page大小最好设置为32或者64bytes;如果存储很大大对象,则可以使用更大的page,如果不 确定,就使用默认值
vm-page-size 32
4.25. 设置swap文件中的page数量,由于页表(一种表示页面空闲或使用的bitmap)是在放在内存中的,,在磁盘上每8个pages将消耗1byte的内存。
vm-pages 134217728
4.26. 设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4
vm-max-threads 4
4.27. 设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启
glueoutputbuf yes
4.28. 指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法
hash-max-zipmap-entries 64
hash-max-zipmap-value 512
4.29. 指定是否激活重置哈希,默认为开启(后面在介绍Redis的哈希算法时具体介绍)
activerehashing yes
4.30. 指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件
include /path/to/local.conf
2.4 启动多个redis进程
2.4.1 启动时指定端口
启动时指定端口可在一台服务器启动多个redis进程
cd /usr/local/redis/bin
./redis-server ../conf/redis.conf --port 6380
2.4.2 创建多个redis目录
创建多个redis目录,以端口号命名,推荐使用此种方式
比如:创建6381、6382两个目录,将redis的安装文件bin和conf拷贝至这两个目录。
修改6381目录下的redis.conf设置端口号为6381
修改6382目录下的redis.conf设置端口号为6382
对应配置文件修改port 6381、 port 6382
启动6381和6382目录下的redis-server程序:
cd 6381
./redis-server . /redis.conf
cd 6382
./redis-server . /redis.conf
2.5 redis客户端
在redis的安装目录中有redis的客户端,即redis-cli(Redis Command Line Interface),它是Redis自带的基于命令行的Redis客户端
ps aux|grep redis 查询redis是否启动
./redis-server ../conf/redis.conf 启动redis
./redis-cli -h 127.0.0.1 -p 6379 启动redis客户端
如果conf文件设置密码,则使用./redis-cli -h 127.0.0.1 -p 6379 -a xxx 来启动客户端
ping Redis提供了PING命令来测试客户端与Redis的连接是否正常,如果连接正常会收到回复PONG
第三节 redis常用命令
3.1 基础命令
3.1.1 操作String类型
String 数据结构是简单的key-value类型,value其实不仅是String,也可以是数字,是包含很多种类型的特殊类型,并且是二进制安全的。比如序列化的对象进行存储,比如一张图片进行二进制存储,比如一个简单的字符串,数值等等。
常用命令:
设值:set name zhangsan (说明:多次设置name会覆盖)
命令:
set key value ex 有效时间n(秒):设置key-value在n秒后自动删除
setnx key1 value1: (not exist) 如果key1不存在,则设值 并返回1。如果key1存在,则不设值并返回0;
setex key1 10 lx :(expired) 设置key1的值为lx,过期时间为10秒,10秒后key1清除(key也清除)
setrange string range value 替换字符串
取值: get key
删值:del keys
批量写:mset k1 v1 k2 v2 ... 一次性写入多个值
批量读:mget k1 k2 k3
一次性设值和读取(返回旧值,写上新值):getset name lx
数值类型自增减:incr key,decr key 注意这些 key 对应的必须是数字类型字符串,否则会出错,自增或者自减1
自增或自减指定长度 incrby key increment,decrby key increment 对应的 key 自增或者自减increment值
字符串尾部拼接:append key value 向 key 对应的字符串尾部追加 value
字符串长度:strlen key
setrange pw 1 user 将key为pw的值从索引为1的开始进行替换,后面的由原来的字符补齐
命令示例:
3.1.2 Hash类型
Hash类型是String类型的field和value的映射表,或者说是一个String集合。它特别适合存储对象,相比较而言,将一个对象类型存储在Hash类型要存储在String类型里占用更少的内存空间,并方整个对象的存取。
常用命令:
设值:hset hashname field value(hset是设值命令,hashname是集合名字,field是字段名,value是值)
取值:hget hashname field
批量设置:hmset hashname field1 value1 field2 value2 ….
批量取值:hmget hashname field1 field2 ...
hsetnx key field value:和setnx大同小异
HINCRBY key field increment:指定字段增加指定值increment
hexists key field:指定 key 中是否存在指定 field,如果存在返回1,不存在返回0
hdel key field 删除指定key的hash的field
hlen key:返回hash集合里的所有的键数量(size)
hkeys key:返回hash里所有的field名称
hvals key:返回hash的所有field 对应的 value
hgetall key:返回hash里所有的field和value
命令演示
3.1.3 List类型
List类型是一个链表结构的集合,其主要功能有push、pop、获取元素等。更详细的说,List类型是一个双端链表的节后,我们可以通过相关的操作进行集合的头部或者尾部添加和删除元素,List的设计非常简单精巧,即可以作为栈,又可以作为队列,满足绝大多数的需求。
常用命令:
lpush key1 value1 value2...:从头部加入元素(栈,先进后出)
rpush key1 value1 value2 ...:从尾部加入元素(队列,先进先出)
linsert key BEFORE|AFTER pivot value
该命令首先会在列表中从左到右查找值为pivot的元素,然后根据第二个参数是BEFORE还是AFTER来决定将value插入到该元素的前面还是后面
lrange key start stop:获取指定索引内的所有元素,只可从左到右 0 -1代表所有
lset key index value:将key 集合中 index下表的元素替换掉
lrem key count value
lrem命令会删除列表中前count个值为value的元素,返回实际删除的元素个数。根据count值的不同,该命令的执行方式会有所不同:
当count>0时, LREM会从列表左边开始删除。
当count<0时, LREM会从列表后边开始删除。
当count=0时, LREM删除所有值为value的元素。
ltrim key start stop:保留制定key的值范围内的数据, 其他数据会删掉, 和 lrange 一样的参数范围
lpop key:从list的头部删除元素,并返回删除元素。
rpop key:从list的尾部删除元素,并返回删除元素
rpoplpush list1 list2:从list1尾部删除元素,并将被移除的元素添加到list2的头部,返回被移除的元素,可以实现MQ
llen key:返回元素个数
lindex key index:返回名称为key的list中index位置的元素
3.1.4 Set类型
set集合是string类型的无序集合,set是通过hashtable实现的,对集合我们可以取交集、并集、差集
常用命令
SADD key member [member ...]:向名称为key的set中添加元素,set集合不允许重复元素。
SMEMBERS key:查看set集合中的元素。
SREM key member [member ...]:删除set集合的元素
SPOP key:随机删除指定set中的一个内容并将删除的内容返回
SDIFF key [key ...]:差集运算,返回在第一个set 中存在,第二个set 中不存在的内容
sdiffstore set4 set2 set3 将set2 set3不同元素的比较结果保存到set4中
SINTER key [key ...]:取交集,集合重复的数据
sinterstore:set3 set1 set2取交集后保存到 set3
SUNION key [key ...]:取并集,因为是 set 所以相同部分只会取一次
sunionstore set3 set1 set2:取并集后保存到 set1
smove set1 set2:从一个set集合移动到另一个set集合里
SCARD key:查看集合里的元素个数
SISMEMBER key member:判断某个元素是否为集合中的元素,是,返回1。不是,返回0。
srandmember key:随机返回一个元素
3.1.5 Zset类型
有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 2^32 - 1 (4294967295, 每个集合可存储40多亿个成员)。
常用命令
ZADD key score member [score member ...]: score 是分, member 是内容, score 必须是数字,向有序集合中添加一个元素,该元素如果存在则更新顺序,如果分值相同元素不同会同时存在两个元素。
ZSCORE key member 获取指定key 中指定内容的分数
ZREM key member [member ...] :删除zset名称key中的member元素
ZRANGE key start stop [WITHSCORES] 获得排名在某个范围的元素列表,照元素分数从小到大的顺序返回索引从start到stop之间的所有元素(包含两端的元素)[WITHSCORES]为可选项,代表是否在结果中显示分数
ZREVRANGE key start stop [WITHSCORES] 照元素分数从大到小的顺序返回索引从start到stop之间的所有元素(包含两端的元素)
ZRANK key member 返回有序集合中指定成员的索引(从小到大排序)
ZREVRANK key member 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序
ZCARD key 返回集合里所有元素的个数
ZCOUNT key min max 返回集合中score在给定区间中的数量
zincrby key increment member: 有序集合中对指定成员的分数加上增量 increment
zrangebyscore key min max [WITHSCORES] [LIMIT offset count] :通过分数返回有序集合指定区间内的成员 min max 代表分数范围 ,offset 代表偏移量, count 代表获取多少个,类似于数据库
zremrangebyrank key start stop :移除有序集合中给定的排名区间的所有成员
zremrangebyscore key min max:移除有序集合中给定的分数区间的所有成员
ZINCRBY key increment member 增加memeber元素的分数increment,返回值是更改后的分数
3.1.6 HyperLogLog
Redis 在 2.8.9 版本添加了 HyperLogLog 结构。
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
什么是基数?
比如数据集 {1, 2, 1, 2} 那么这个数据集的基数集为 {1, 2}, 基数(不重复元素)为2。基数估计就是在误差可接受的范围内,快速计算基数。
常用命令:
PFADD 新增元素
PFCOUNT 获取基数的估计值
PFMERGE 将多个 HyperLogLog 合并为一个 HyperLogLog
命令演示
3.2 高级命令
3.2.1 常用命令
keys * : 返回满足的所有键 ,可以模糊匹配 比如 keys abc* 代表 abc 开头的 key
exists key :是否存在指定的key,存在返回1,不存在返回0
expire key second:设置某个key的过期时间 时间为秒
del key:删除某个key
ttl key:查看剩余时间,当key不存在时,返回 -2;存在但没有设置剩余生存时间时,返回 -1,否则,以秒为单位,返回 key 的剩余生存时间。
persist key:取消过去时间
PEXPIRE key milliseconds 修改key 的过期时间为毫秒
select : 选择数据库 数据库为0-15(默认一共16个数据库) s
设计成多个数据库实际上是为了数据库安全和备份
move key dbindex : 将当前数据中的key转移到其他数据库
randomkey:随机返回一个key
rename key key2:重命名key
echo:打印命令
dbsize:查看数据库的key数量
info:查看数据库信息
config get * 实时传储收到的请求,返回相关的配置
flushdb :清空当前数据库
flushall :清空所有数据库
命令演示
3.2.2 Redis 数据备份与恢复
数据备份
Redis SAVE 命令用于创建当前数据库的备份。
恢复数据
如果需要恢复数据,只需将备份文件 (dump.rdb) 移动到 redis 安装目录并启动服务即可。
3.2.3 Redis 安全
因为redis速度相当快,所以一台比较好的服务器下,一个外部用户在一秒内可以进行15W次密码尝试,这意味着你需要设定非常强大的密码来防止暴力破解.
可以通过 redis 的配置文件设置密码参数,这样客户端连接到 redis 服务就需要密码验证,这样可以让你的 redis 服务更安全
常用命令:
vim /usr/local/redis/conf/redis.conf 编辑配置文件,
修改:#reqirepass foobared 为 : reqirepass redis(你的密码)
pkill redis-server 关闭redis-server
./bin/redis-server ./conf/redis.conf 启动redis
./bin/redis-cli 打开客户端
第四节、Jedis
Jedis现在基本很少使用了,这里只做简单介绍。
4.1 Jedis的使用方式
4.1.1 单实例连接
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1",6379);
jedis.set("user","chengsw");
String value = jedis.get("user");
System.out.println(value);
}
4.1.1 连接池连接
package com.aicai.qa.tools.statics.redis;
import com.aicai.qa.tools.statics.config.SysConfigUtil;
import redis.clients.jedis.BinaryClient;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class RedisUtil {
private JedisPool pool = null;
private RedisUtil() {
if (pool == null) {
String ip = SysConfigUtil.getSysConfigUtil("redis.properties").getString("redis.host");
int port = SysConfigUtil.getSysConfigUtil("redis.properties").getInt("redis.port");
String password = SysConfigUtil.getSysConfigUtil("redis.properties").getString("redis.password");
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(SysConfigUtil.getSysConfigUtil("redis.properties").getInt("redis.maxTotal"));
jedisPoolConfig.setMaxIdle(SysConfigUtil.getSysConfigUtil("redis.properties").getInt("redis.maxIdle"));
jedisPoolConfig.setMaxWaitMillis(SysConfigUtil.getSysConfigUtil("redis.properties").getLong("redis.maxWaitMillis"));
jedisPoolConfig.setTestOnBorrow(SysConfigUtil.getSysConfigUtil("redis.properties").getBoolean("redis.testOnBorrow"));
if (password != null && !"".equals(password)) {
// redis 设置了密码
pool = new JedisPool(jedisPoolConfig, ip, port, 10000, password);
} else {
// redis 未设置密码
pool = new JedisPool(jedisPoolConfig, ip, port, 10000);
}
}
}
/**
* 获取指定key的值,如果key不存在返回null,如果该Key存储的不是字符串,会抛出一个错误
*
* @param key
* @return
*/
public String get(String key) {
Jedis jedis = getJedis();
String value = null;
value = jedis.get(key);
return value;
}
/**
* 设置key的值为value
*
* @param key
* @param value
* @return
*/
public String set(String key, String value) {
Jedis jedis = getJedis();
return jedis.set(key, value);
}
/**
* 删除指定的key,也可以传入一个包含key的数组
*
* @param keys
* @return
*/
public Long del(String... keys) {
Jedis jedis = getJedis();
return jedis.del(keys);
}
/**
* 通过key向指定的value值追加值
*
* @param key
* @param str
* @return
*/
public Long append(String key, String str) {
Jedis jedis = getJedis();
return jedis.append(key, str);
}
/**
* 判断key是否存在
*
* @param key
* @return
*/
public Boolean exists(String key) {
Jedis jedis = getJedis();
return jedis.exists(key);
}
/**
* 设置key value,如果key已经存在则返回0
*
* @param key
* @param value
* @return
*/
public Long setnx(String key, String value) {
Jedis jedis = getJedis();
return jedis.setnx(key, value);
}
/**
* 设置key value并指定这个键值的有效期
*
* @param key
* @param seconds
* @param value
* @return
*/
public String setex(String key, int seconds, String value) {
Jedis jedis = getJedis();
return jedis.setex(key, seconds, value);
}
/**
* 通过key 和offset 从指定的位置开始将原先value替换
*
* @param key
* @param offset
* @param str
* @return
*/
public Long setrange(String key, int offset, String str) {
Jedis jedis = getJedis();
return jedis.setrange(key, offset, str);
}
/**
* 通过批量的key获取批量的value
*
* @param keys
* @return
*/
public List<String> mget(String... keys) {
Jedis jedis = getJedis();
return jedis.mget(keys);
}
/**
* 批量的设置key:value,也可以一个
*
* @param keysValues
* @return
*/
public String mset(String... keysValues) {
Jedis jedis = getJedis();
return jedis.mset(keysValues);
}
/**
* 批量的设置key:value,可以一个,如果key已经存在则会失败,操作会回滚
*
* @param keysValues
* @return
*/
public Long msetnx(String... keysValues) {
Jedis jedis = getJedis();
return jedis.msetnx(keysValues);
}
/**
* 设置key的值,并返回一个旧值
*
* @param key
* @param value
* @return
*/
public String getSet(String key, String value) {
Jedis jedis = getJedis();
return jedis.getSet(key, value);
}
/**
* 通过下标 和key 获取指定下标位置的 value
*
* @param key
* @param startOffset
* @param endOffset
* @return
*/
public String getrange(String key, int startOffset, int endOffset) {
Jedis jedis = getJedis();
return jedis.getrange(key, startOffset, endOffset);
}
/**
* 通过key 对value进行加值+1操作,当value不是int类型时会返回错误,当key不存在是则value为1
*
* @param key
* @return
*/
public Long incr(String key) {
Jedis jedis = getJedis();
return jedis.incr(key);
}
/**
* 通过key给指定的value加值,如果key不存在,则这是value为该值
*
* @param key
* @param integer
* @return
*/
public Long incrBy(String key, long integer) {
Jedis jedis = getJedis();
return jedis.incrBy(key, integer);
}
/**
* 对key的值做减减操作,如果key不存在,则设置key为-1
*
* @param key
* @return
*/
public Long decr(String key) {
Jedis jedis = getJedis();
return jedis.decr(key);
}
/**
* 减去指定的值
*
* @param key
* @param integer
* @return
*/
public Long decrBy(String key, long integer) {
Jedis jedis = getJedis();
return jedis.decrBy(key, integer);
}
/**
* 通过key获取value值的长度
*
* @param key
* @return
*/
public Long strLen(String key) {
Jedis jedis = getJedis();
return jedis.strlen(key);
}
/**
* 通过key给field设置指定的值,如果key不存在则先创建,如果field已经存在,返回0
*
* @param key
* @param field
* @param value
* @return
*/
public Long hsetnx(String key, String field, String value) {
Jedis jedis = getJedis();
return jedis.hsetnx(key, field, value);
}
/**
* 通过key给field设置指定的值,如果key不存在,则先创建
*
* @param key
* @param field
* @param value
* @return
*/
public Long hset(String key, String field, String value) {
Jedis jedis = getJedis();
return jedis.hset(key, field, value);
}
/**
* 通过key同时设置 hash的多个field
*
* @param key
* @param hash
* @return
*/
public String hmset(String key, Map<String, String> hash) {
Jedis jedis = getJedis();
return jedis.hmset(key, hash);
}
/**
* 通过key 和 field 获取指定的 value
*
* @param key
* @param failed
* @return
*/
public String hget(String key, String failed) {
Jedis jedis = getJedis();
return jedis.hget(key, failed);
}
/**
* 设置key的超时时间为seconds
*
* @param key
* @param seconds
* @return
*/
public Long expire(String key, int seconds) {
Jedis jedis = getJedis();
return jedis.expire(key, seconds);
}
/**
* 通过key 和 fields 获取指定的value 如果没有对应的value则返回null
*
* @param key
* @param fields 可以是 一个String 也可以是 String数组
* @return
*/
public List<String> hmget(String key, String... fields) {
Jedis jedis = getJedis();
return jedis.hmget(key, fields);
}
/**
* 通过key给指定的field的value加上给定的值
*
* @param key
* @param field
* @param value
* @return
*/
public Long hincrby(String key, String field, Long value) {
Jedis jedis = getJedis();
return jedis.hincrBy(key, field, value);
}
/**
* 通过key和field判断是否有指定的value存在
*
* @param key
* @param field
* @return
*/
public Boolean hexists(String key, String field) {
Jedis jedis = getJedis();
return jedis.hexists(key, field);
}
/**
* 通过key返回field的数量
*
* @param key
* @return
*/
public Long hlen(String key) {
Jedis jedis = getJedis();
return jedis.hlen(key);
}
/**
* 通过key 删除指定的 field
*
* @param key
* @param fields 可以是 一个 field 也可以是 一个数组
* @return
*/
public Long hdel(String key, String... fields) {
Jedis jedis = getJedis();
return jedis.hdel(key, fields);
}
/**
* 通过key返回所有的field
*
* @param key
* @return
*/
public Set<String> hkeys(String key) {
Jedis jedis = getJedis();
return jedis.hkeys(key);
}
/**
* 通过key返回所有和key有关的value
*
* @param key
* @return
*/
public List<String> hvals(String key) {
Jedis jedis = getJedis();
return jedis.hvals(key);
}
/**
* 通过key获取所有的field和value
*
* @param key
* @return
*/
public Map<String, String> hgetall(String key) {
Jedis jedis = getJedis();
return jedis.hgetAll(key);
}
/**
* 通过key向list头部添加字符串
*
* @param key
* @param strs 可以是一个string 也可以是string数组
* @return 返回list的value个数
*/
public Long lpush(String key, String... strs) {
Jedis jedis = getJedis();
return jedis.lpush(key, strs);
}
/**
* 通过key向list尾部添加字符串
*
* @param key
* @param strs 可以是一个string 也可以是string数组
* @return 返回list的value个数
*/
public Long rpush(String key, String... strs) {
Jedis jedis = getJedis();
return jedis.rpush(key, strs);
}
/**
* 通过key在list指定的位置之前或者之后 添加字符串元素
*
* @param key
* @param where LIST_POSITION枚举类型
* @param pivot list里面的value
* @param value 添加的value
* @return
*/
public Long linsert(String key, BinaryClient.LIST_POSITION where,
String pivot, String value) {
Jedis jedis = getJedis();
return jedis.linsert(key, where, pivot, value);
}
/**
* 通过key设置list指定下标位置的value
* 如果下标超过list里面value的个数则报错
*
* @param key
* @param index 从0开始
* @param value
* @return 成功返回OK
*/
public String lset(String key, Long index, String value) {
Jedis jedis = getJedis();
return jedis.lset(key, index, value);
}
/**
* 通过key从对应的list中删除指定的count个 和 value相同的元素
*
* @param key
* @param count 当count为0时删除全部
* @param value
* @return 返回被删除的个数
*/
public Long lrem(String key, long count, String value) {
Jedis jedis = getJedis();
return jedis.lrem(key, count, value);
}
/**
* 通过key保留list中从strat下标开始到end下标结束的value值
*
* @param key
* @param start
* @param end
* @return 成功返回OK
*/
public String ltrim(String key, long start, long end) {
Jedis jedis = getJedis();
return jedis.ltrim(key, start, end);
}
/**
* 通过key从list的头部删除一个value,并返回该value
*
* @param key
* @return
*/
public synchronized String lpop(String key) {
Jedis jedis = getJedis();
return jedis.lpop(key);
}
/**
* 通过key从list尾部删除一个value,并返回该元素
*
* @param key
* @return
*/
synchronized public String rpop(String key) {
Jedis jedis = getJedis();
return jedis.rpop(key);
}
/**
* 通过key从一个list的尾部删除一个value并添加到另一个list的头部,并返回该value
* 如果第一个list为空或者不存在则返回null
*
* @param srckey
* @param dstkey
* @return
*/
public String rpoplpush(String srckey, String dstkey) {
Jedis jedis = getJedis();
return jedis.rpoplpush(srckey, dstkey);
}
/**
* 通过key获取list中指定下标位置的value
*
* @param key
* @param index
* @return 如果没有返回null
*/
public String lindex(String key, long index) {
Jedis jedis = getJedis();
return jedis.lindex(key, index);
}
/**
* 通过key返回list的长度
*
* @param key
* @return
*/
public Long llen(String key) {
Jedis jedis = getJedis();
return jedis.llen(key);
}
/**
* 通过key获取list指定下标位置的value
* 如果start 为 0 end 为 -1 则返回全部的list中的value
*
* @param key
* @param start
* @param end
* @return
*/
public List<String> lrange(String key, long start, long end) {
Jedis jedis = getJedis();
return jedis.lrange(key, start, end);
}
/**
* 通过key向指定的set中添加value
*
* @param key
* @param members 可以是一个String 也可以是一个String数组
* @return 添加成功的个数
*/
public Long sadd(String key, String... members) {
Jedis jedis = getJedis();
return jedis.sadd(key, members);
}
/**
* 通过key删除set中对应的value值
*
* @param key
* @param members 可以是一个String 也可以是一个String数组
* @return 删除的个数
*/
public Long srem(String key, String... members) {
Jedis jedis = getJedis();
return jedis.srem(key, members);
}
/**
* 通过key随机删除一个set中的value并返回该值
*
* @param key
* @return
*/
public String spop(String key) {
Jedis jedis = getJedis();
return jedis.spop(key);
}
/**
* 通过key获取set中的差集
* 以第一个set为标准
*
* @param keys 可以 是一个string 则返回set中所有的value 也可以是string数组
* @return
*/
public Set<String> sdiff(String... keys) {
Jedis jedis = getJedis();
return jedis.sdiff(keys);
}
/**
* 通过key获取set中的差集并存入到另一个key中
* 以第一个set为标准
*
* @param dstkey 差集存入的key
* @param keys 可以 是一个string 则返回set中所有的value 也可以是string数组
* @return
*/
public Long sdiffstore(String dstkey, String... keys) {
Jedis jedis = getJedis();
return jedis.sdiffstore(dstkey, keys);
}
/**
* 通过key获取指定set中的交集
*
* @param keys 可以 是一个string 也可以是一个string数组
* @return
*/
public Set<String> sinter(String... keys) {
Jedis jedis = getJedis();
return jedis.sinter(keys);
}
/**
* 通过key获取指定set中的交集 并将结果存入新的set中
*
* @param dstkey
* @param keys 可以 是一个string 也可以是一个string数组
* @return
*/
public Long sinterstore(String dstkey, String... keys) {
Jedis jedis = getJedis();
return jedis.sinterstore(dstkey, keys);
}
/**
* 通过key返回所有set的并集
*
* @param keys 可以 是一个string 也可以是一个string数组
* @return
*/
public Set<String> sunion(String... keys) {
Jedis jedis = getJedis();
return jedis.sunion(keys);
}
/**
* 通过key返回所有set的并集,并存入到新的set中
*
* @param dstkey
* @param keys 可以 是一个string 也可以是一个string数组
* @return
*/
public Long sunionstore(String dstkey, String... keys) {
Jedis jedis = getJedis();
return jedis.sunionstore(dstkey, keys);
}
/**
* 通过key将set中的value移除并添加到第二个set中
*
* @param srckey 需要移除的
* @param dstkey 添加的
* @param member set中的value
* @return
*/
public Long smove(String srckey, String dstkey, String member) {
Jedis jedis = getJedis();
return jedis.smove(srckey, dstkey, member);
}
/**
* 通过key获取set中value的个数
*
* @param key
* @return
*/
public Long scard(String key) {
Jedis jedis = getJedis();
return jedis.scard(key);
}
/**
* 通过key判断value是否是set中的元素
*
* @param key
* @param member
* @return
*/
public Boolean sismember(String key, String member) {
Jedis jedis = getJedis();
return jedis.sismember(key, member);
}
/**
* 通过key获取set中随机的value,不删除元素
*
* @param key
* @return
*/
public String srandmember(String key) {
Jedis jedis = getJedis();
return jedis.srandmember(key);
}
/**
* 通过key获取set中所有的value
*
* @param key
* @return
*/
public Set<String> smembers(String key) {
Jedis jedis = getJedis();
return jedis.smembers(key);
}
/**
* 通过key向zset中添加value,score,其中score就是用来排序的
* 如果该value已经存在则根据score更新元素
*
* @param key
* @param score
* @param member
* @return
*/
public Long zadd(String key, double score, String member) {
Jedis jedis = getJedis();
return jedis.zadd(key, score, member);
}
/**
* 通过key删除在zset中指定的value
*
* @param key
* @param members 可以 是一个string 也可以是一个string数组
* @return
*/
public Long zrem(String key, String... members) {
Jedis jedis = getJedis();
return jedis.zrem(key, members);
}
/**
* 通过key增加该zset中value的score的值
*
* @param key
* @param score
* @param member
* @return
*/
public Double zincrby(String key, double score, String member) {
Jedis jedis = getJedis();
return jedis.zincrby(key, score, member);
}
/**
* 通过key返回zset中value的排名
* 下标从小到大排序
*
* @param key
* @param member
* @return
*/
public Long zrank(String key, String member) {
Jedis jedis = getJedis();
return jedis.zrank(key, member);
}
/**
* 通过key返回zset中value的排名
* 下标从大到小排序
*
* @param key
* @param member
* @return
*/
public Long zrevrank(String key, String member) {
Jedis jedis = getJedis();
return jedis.zrevrank(key, member);
}
/**
* 通过key将获取score从start到end中zset的value
* socre从大到小排序
* 当start为0 end为-1时返回全部
*
* @param key
* @param start
* @param end
* @return
*/
public Set<String> zrevrange(String key, long start, long end) {
Jedis jedis = getJedis();
return jedis.zrevrange(key, start, end);
}
/**
* 通过key返回指定score内zset中的value
*
* @param key
* @param max
* @param min
* @return
*/
public Set<String> zrangebyscore(String key, String max, String min) {
Jedis jedis = getJedis();
return jedis.zrevrangeByScore(key, max, min);
}
/**
* 通过key返回指定score内zset中的value
*
* @param key
* @param max
* @param min
* @return
*/
public Set<String> zrangeByScore(String key, double max, double min) {
Jedis jedis = getJedis();
return jedis.zrevrangeByScore(key, max, min);
}
/**
* 返回指定区间内zset中value的数量
*
* @param key
* @param min
* @param max
* @return
*/
public Long zcount(String key, String min, String max) {
Jedis jedis = getJedis();
return jedis.zcount(key, min, max);
}
/**
* 通过key返回zset中的value个数
*
* @param key
* @return
*/
public Long zcard(String key) {
Jedis jedis = getJedis();
return jedis.zcard(key);
}
/**
* 通过key获取zset中value的score值
*
* @param key
* @param member
* @return
*/
public Double zscore(String key, String member) {
Jedis jedis = getJedis();
return jedis.zscore(key, member);
}
/**
* 通过key删除给定区间内的元素
*
* @param key
* @param start
* @param end
* @return
*/
public Long zremrangeByRank(String key, long start, long end) {
Jedis jedis = getJedis();
return jedis.zremrangeByRank(key, start, end);
}
/**
* 通过key删除指定score内的元素
*
* @param key
* @param start
* @param end
* @return
*/
public Long zremrangeByScore(String key, double start, double end) {
Jedis jedis = getJedis();
return jedis.zremrangeByScore(key, start, end);
}
/**
* 返回满足pattern表达式的所有key
* keys(*)
* 返回所有的key
*
* @param pattern
* @return
*/
public Set<String> keys(String pattern) {
Jedis jedis = getJedis();
return jedis.keys(pattern);
}
/**
* 通过key判断值得类型
*
* @param key
* @return
*/
public String type(String key) {
Jedis jedis = getJedis();
return jedis.type(key);
}
private void close(Jedis jedis) {
if (jedis != null) {
jedis.close();
}
}
private Jedis getJedis() {
return pool.getResource();
}
public static RedisUtil getRedisUtil() {
return new RedisUtil();
}
}
第五节 Spring Data Redis
Spring Data Redis是Spring家族的一部分,提供了在Spring应用中通过简单的配置访问redis服务,对redis底层开发包(Jedis,JRedis,RJC)进行了高度封装,RedisTemplate提供了redis各种操作、异常处理及序列化,支持发布订阅,并对spring 3.1 cache进行了实现。
Spring Data Redis针对jedis提供了如下功能:
- 连接池自动管理,提供了一个高度封装的“RedisTemplate”类;
- 针对jedis客户端中大量api进行了归类封装,将同一类型操作封装为operation接口
ValueOperations:简单K-V操作 String
SetOperations:set类型数据操作 Set
ZSetOperations:zset类型数据操作 ZSet
HashOperations:针对map类型的数据操作 Hash
ListOperations:针对list类型的数据操作 List
Spring Data Redis使用方式:
- 项目中引入想给依赖
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置文件中配置好redis信息
spring:
redis:
host: 127.0.0.1
port: 6379
password: 123456
# 连接超时时间(毫秒)
timeout: 10000
# Redis默认情况下有16个分片,这里配置具体使用的分片,默认是0
database: 0
lettuce:
pool:
# 连接池最大连接数(使用负值表示没有限制) 默认 8
max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
max-wait: -1
# 连接池中的最大空闲连接 默认 8
max-idle: 8
# 连接池中的最小空闲连接 默认 0
min-idle: 0
- 在项目中引入RedisTemplate
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/test")
public void test() {
redisTemplate.boundValueOps("user").set("chengsw",30, TimeUnit.SECONDS);
String user = (String) redisTemplate.boundValueOps("user").get();
Long size = redisTemplate.boundValueOps("user").size();
System.out.println(user+ "===" + size);
}
注意:
Spring Data Redis默认情况下是使用org.springframework.data.redis.serializer.JdkSerializationRedisSerializer来做序列化。
在使用 RedisTemplate 时会产生乱码。
不过Spring Boot整合Spring Data Redis还给我们提供了StringRedisTemplate,它继承了RedisTemplate 并更新了StringRedisSerializer的序列化方式,解决了乱码问题。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.data.redis.core;
import org.springframework.data.redis.connection.DefaultStringRedisConnection;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
public class StringRedisTemplate extends RedisTemplate<String, String> {
public StringRedisTemplate() {
RedisSerializer<String> stringSerializer = new StringRedisSerializer();
this.setKeySerializer(stringSerializer);
this.setValueSerializer(stringSerializer);
this.setHashKeySerializer(stringSerializer);
this.setHashValueSerializer(stringSerializer);
}
public StringRedisTemplate(RedisConnectionFactory connectionFactory) {
this();
this.setConnectionFactory(connectionFactory);
this.afterPropertiesSet();
}
protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) {
return new DefaultStringRedisConnection(connection);
}
}
第六节 Redis事务
Redis中的事务(transaction)是一组命令的集合,至少是两个或两个以上的命令,redis事
务保证这些命令被执行时中间不会被任何其他操作打断。
事务是一个单独的隔离操作:
事务中的所有命令都会序列化、按顺序地执行。
事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
事务是一个原子操作:
事务中的命令要么全部被执行,要么全部都不执行
一个事务从开始到执行会经历以下三个阶段:
开始事务
命令入队
执行事务
常用命令:
6.1 命令行执行事务
命令演示:
6.2 Java代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class RedisController {
@Autowired
StringRedisTemplate stringRedisTemplate;
@RequestMapping(value = "/transaction")
public String Transaction() {
//开启事务权限
stringRedisTemplate.setEnableTransactionSupport(true);
try {
//开启事务
stringRedisTemplate.multi();
//事务下的操作
stringRedisTemplate.opsForValue().set("username","chengsw");
// int i = 1 / 0;
stringRedisTemplate.opsForValue().set("password","666666");
//提交事务
stringRedisTemplate.exec();
} catch (Exception e) {
log.error("error:", e);
stringRedisTemplate.discard();
}
return "success";
}
}
6.3 Redis事务Watch机制
A、Redis的WATCH机制
WATCH机制原理:
WATCH机制:使用WATCH监视一个或多个key,跟踪key的value修改情况,如果有key的value值在事务EXEC执行之前被修改了,整个事务被取消。EXEC返回提示信息,表示事务已经失败。
WATCH机制使的事务EXEC变的有条件,事务只有在被WATCH的key没有修改的前提下才能执行。不满足条件,事务被取消。使用WATCH监视了一个带过期时间的键,那么即使这个键过期了,事务仍然可以正常执行。
大多数情况下,不同的客户端会访问不同的键,相互同时竞争同一key的情况一般都很少,乐观锁能够以很好的性能解决数据冲突的问题。
B、何时取消key的监视(WATCH)?
①WATCH命令可以被调用多次。对键的监视从WATCH执行之后开始生效,直到调用EXEC为止。不管事务是否成功执行,对所有键的监视都会被取消。
②当客户端断开连接时,该客户端对键的监视也会被取消。
③UNWATCH命令可以手动取消对所有键的监视。
C、WATCH的示例
执行步骤:
- 首先启动redis-server,再开启两个客户端连接。分别叫A客户端和B客户端。
A客户端:WATCH某个key,同时执行事务
B客户端:对A 客户端WATCH的key 修改其 value值
1)在A客户端设置key:csdn.redis为10
2)在A客户端监视key:csdn.redis
3)在A客户端开启事务multi
4)在A客户端修改csdn.redis的值为20
5)在B客户端修改csdn.redis的值为10086
6)在A客户端执行事务exec
7)在A客户端查看csdn.redis值,A客户端执行的事务没有提交,因为WATCH的csdn.redis的值已
经被修改了,所有放弃事务。
第七节 发布与订阅消息
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
Redis 客户端可以订阅任意数量的频道。
下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
7.1 命令行方式
配置订阅和发布
常用命令
subscribe [频道] 进行订阅监听
publish [频道 发布内容] 进行发布消息广播
序号 | 命令 | 描述 |
---|---|---|
1 | PSUBSCRIBE pattern [pattern …] | 订阅一个或多个符合给定模式的频道。 |
2 | PUBSUB subcommand [argument [argument …]] | 查看订阅与发布系统状态。 |
3 | PUBLISH channel message | 将信息发送到指定的频道。 |
4 | PUNSUBSCRIBE [pattern [pattern …]] | 退订所有给定模式的频道。 |
5 | SUBSCRIBE channel [channel …] | 订阅给定的一个或多个频道的信息。 |
6 | UNSUBSCRIBE [channel [channel …]] | 指退订给定的频道。 |
-
分别克隆额外的2个会话,重命名client1、client2 ,而且三个会话都运行redis的客户端
-
分别在client1和client2进行订阅
-
进行消息发布
7.2 Java代码方式
- 消息监听类
import org.springframework.stereotype.Component;
@Component
public class RedisReceiver {
public void receiveMessage(String message) {
// TODO 这里是收到通道的消息之后执行的方法
System.out.println("RedisReceiver接收到订阅消息啦!消息内容:" + message);
}
}
- Redis消息订阅配置类
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 可以添加多个 messageListener,配置不同的交换机
container.addMessageListener(listenerAdapter, new PatternTopic("channel:chengsw"));
return container;
}
/**
* 消息监听器适配器,绑定消息处理器,利用反射技术调用消息处理器的业务方法
*
* @param receiver
* @return
*/
@Bean
MessageListenerAdapter listenerAdapter(RedisReceiver receiver) {
System.out.println("消息适配器1");
return new MessageListenerAdapter(receiver, "receiveMessage");
}
@Bean
StringRedisTemplate template(RedisConnectionFactory connectionFactory) {
return new StringRedisTemplate(connectionFactory);
}
}
- 测试接口
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
@RestController
@Slf4j
public class RedisController {
@Autowired
StringRedisTemplate stringRedisTemplate;
@RequestMapping(value = "/syncmessage")
public String SyncMessage(){
for(int i = 1; i <= 5; i++){
try{
// 为了模拟消息,sleep一下。
Thread.sleep(2000);
}catch(Exception e){
log.error("error: ",e);
}
stringRedisTemplate.convertAndSend("channel:chengsw", String.format("我是消息{%d}号: %tT", i, new Date()));
}
return "success";
}
}
第八节 redis高级使用
8.1 主从复制
使用一台服务器进行模拟Redis的主从复制
8.1.1 概念
一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的,原因如下:
从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大;
从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内容容量为256G,也不能将所有内容用作Redis存储内存,一般来说,单台Redis最大使用内存不应该超过20G。
考虑如下一种场景:电子商务网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是”多读少写”。
对于这种场景,我们可以使如下这种架构:
如图所示,将一台Redis服务器作主库(Matser),其他三台作为从库(Slave),主库只负责写数据,每次有数据更新都将更新的数据同步到它所有的从库,而从库只负责读数据。
这样一来,就有了两个好处:
①读写分离,不仅可以提高服务器的负载能力并且可以根据读请求的规模自由增加或者减少从库的数量棒极了;
② 数据被复制成了好几份,就算有一台机器出现故障,也可以使用其他机器的数据快速恢复。
需要注意的是:在Redis主从模式中,一台主库可以拥有多个从库,但是一个从库只能隶属于一个主库。
8.1.2 redis主从复制特点
1、Master可以拥有多个Slave;
2、多个salve可以连接同一个Master,还可以链接到其他的slave
3、主从复制不会阻塞Master,在同步数据时,master可以继续处理client请求
4、提供系统的伸缩性。
8.1.3 redis主从复制配置
在Redis中,要实现主从复制架构非常简单,只需要在从数据库的配置文件中加上如下命令即可:
slaveof 主数据库地址 主数据库端口
主数据库不需要任何配置。
配置步骤:
- 重新安装一份redis,作为从库,拷贝redis.conf 并修改
端口号为6380
设置slaveof 主库ip地址 端口号
设置masterauth 主库密码
- 启动从库
/usr/local/redisslave1/bin/redis-server /usr/local/redisslave1/bin/redis.conf
- 分别使用2个客户端连接主库和从库
拷贝连接会话,并重命名slave,打开客户端连接从库
在主库和从库的连接客户端中输入:
info replication
- 测试数据的主从复制
如果主库宕机,那么就无法再继续写入数据
8.2 哨兵模式
8.2.1 概念
Redis-Sentinel(哨兵模式)是官方推荐的高可用解决方案,当redis在做master-slave的高可用方案时,假如master宕机了,redis本身(以及其很多客户端)都没有实现自动进行主备切换,而redis-sentinel本身也是独立运行的进程,可以部署在其他与redis集群可通讯的机器中监控redis集群。
有了主从复制的实现之后,我们如果想从服务器进行监控,那么在redis2.6以后提供了一个“哨兵”机制,并在2.8版本以后功能稳定起来。
哨兵:顾名思义,就是监控Redis系统的运行状况。
哨兵模式的特点:
1、不时地监控redis是否按照预期良好地运行;
2、如果发现某个redis节点运行出现状况,能够通知另外一个进程(例如它的客户端);
3、能够进行自动切换。当一个master节点不可用时,能够选举出master的多个slave(如果有超过一个slave的话)中的一个来作为新的master,其它的slave节点会将它所追随的master的地址改为被提升为master的slave的新地址。
4、哨兵为客户端提供服务发现,客户端链接哨兵,哨兵提供当前master的地址然后提供服务,如果出现切换,也就是master挂了,哨兵会提供客户端一个新地址。
哨兵(sentinel)本身也是支持集群的:
很显然,单个哨兵会存在自己挂掉而无法监控整个集群的问题,所以哨兵也是支持集群的,通常用三台哨兵机器来监控一组redis集群。
8.2.2 配置
配置哨兵模式主要是防止主库宕机,所以本文就在从库中进行配置哨兵模式
拷贝配置文件,并修改
命令:
cp /usr/local/redis-3.2.11/sentinel.conf /usr/local/redisslave1/bin/ 拷贝哨兵的配置
vim /usr/local/redisslave1/bin/sentinel.conf 修改哨兵模式的配置文件
配置解释
dir: 日志路径,根据自己的要求保存。
sentinel monitor mymaster 10.0.31.144 6379 1 解释:#名称 #ip #端口号 # 投票选举次数(即有多少票就被选举为主,这里节点少,就配置为1)。
sentinel down-after-millisecond mymaster 5000 解释:哨兵程序多久去监控一次集群,#默认 30s 检查一次,这里配置超时30000毫秒为宕机。
sentinel parallel-syncs mymaster 1 解释:有多少个从节点
sentinel failover-timeout mymaster 600000 解释:若哨兵在该配置值内未完成failover操作(即发生故障时,m/s切换),本次failover失败
8.2.3 启动
/usr/local/redisslave1/bin/redis-server /usr/local/redisslave1/bin/sentinel.conf --sentinel
启动哨兵模式
/usr/local/redis/bin/redis-cli -h 10.211.55.12 -p 26379 info sentinel
8.3 持久化机制
Redis持久化存储支持两种方式:RDB和AOF。RDB一定时间取存储文件,AOF默认每秒去存储历史命令,默认使用RDB,且RDB默认是启动的。
没有持久化的redis和memcache一样,相当于一个纯内存的数据库
Redis是支持持久化的内存数据库,也就是说redis需要经常将内存中的数据同步到硬盘来保证持久化。
相同数据集,AOF文件要远大于RDB文件,恢复速度慢于RDB
AOF运行效率慢于RDB,但是同步策略效率好,不同步效率和RDB相同
Redis 持久化 之 AOF 和 RDB 同时开启,Redis听谁的?
答:听AOF的,RDB与AOF同时开启,默认无脑加载AOF的配置文件
8.3.1 RDB
RDB(snapshotting快照)是将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。
- 优点:使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了redis的高性能
- 缺点:RDB是间隔一段时间进行持久化,如果持久化之间redis发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候
默认方式,将内存中以快照的方式写入到二进制文件中,默认为dump.rdb,可以通过配置设置自动做快照持久化的方式。我们可以配置redis在n秒内如果m个key修改,就自动做快照.
vim /usr/local/redis/conf/redis.conf 修改配置文件
RDB默认开启,redis.conf中的具体配置参数如下:
save 900 1 #900秒内,超过1个key被修改,则发起快照保存
save 300 10 #300秒内,超过10个key被修改,则发起快照保存
save 60 10000 #60秒内,超过10000个key被修改,则发起快照保存
dbfilename dump.rdb 持久化数据存储在本地的文件
dir ./ 持久化数据存储在本地的路径,如果是在/redis/redis-3.0.6/src下启动的redis-cli,则数据会存储在当前src目录下
持久化过程:
当满足save的条件时,比如更改了1个key,900s后会将数据写入临时文件,持久化完成后将临时文件替换旧的dump.rdb。(存储数据的节点是到触发时间时的的节点)
使用RDB恢复数据:
自动的持久化数据存储到dump.rdb后。实际只要重启redis服务即可完成(启动redis的server时会从dump.rdb中先同步数据)
使用命令进行持久化save存储:
./redis-cli -h ip -p port save
./redis-cli -h ip -p port bgsave
一个是在前台进行存储,一个是在后台进行存储。
8.3.2 AOF
AOF(append-only file)是将执行过的指令记录下来,数据恢复时按照从前到后的顺序再将指令执行一遍,实现数据恢复
- 优点:可以保持更高的数据完整性,如果设置追加file的时间是1s,如果redis发生故障,最多会丢失1s的数据;且如果日志写入不完整支持redis-check-aof来进行日志修复;AOF文件没被rewrite之前(文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的flushall)。
- 缺点:AOF文件比RDB文件大,且恢复速度慢。
类似于mysql日志,由于快照方式是在一定时间间隔做一次,所以可能发生redis意外宕机的情况就会丢失最后一次快照后的所有被修改的数据,aof比快照方式有更好的持久化型,是由于redis在使用aof时,redis会将每一个收到的写命令都通过write函数追加到命令中,在redis重新启动时会重新执行文件中保存的写命令在内存中重建这个数据库的内容,这个文件在redis/bin目录下,appendonly.aof。aof不是立即写到硬盘上,可以通过配置文件修改强制写到硬盘中。
修改配置
appendonly yes //启动aof持久化 ,持久化有三种方式:
#appendfsync always //收到写命令就立即写入到磁盘,效率最慢,但是保证完整的持久化(最常用)
#appendfsync everysec //每秒写一次硬盘,在性能和持久化方面做了很好的这种
#appendfsync no //完全依赖os,性能最好,持久化没保证。
重启redis发现bin/目录下多了一个appendonly.aof
提示: 开启aof后之前的rdb模式就失效了,且之前的数据会被清空。