Redis6入门学习笔记
1.NoSQL数据库简介
1.1技术发展
技术分类
1、解决功能性的问题:Java、Jsp、RDBMS、Tomcat、HTML、Linux、JDBC、SVN
以上技术能满足项目的基本功能(CRUD),等这些功能做到一定地步会产生一定的问题:
功能会随着需求变换和升级
2、解决扩展性的问题:Struts、Spring、SpringMVC、Hibernate、Mybatis
用框架来解决扩展性问题,在用框架编写程序时要遵循框架的规范。
3、解决性能的问题:NoSQL、Java线程、Hadoop、Nginx、MQ、ElasticSearch
随着用户量增加,产生性能问题。以上技术来解决性能问题。
1.1.1Web1.0时代
Web1.0的时代,数据访问量很有限,用一夫当关的高性能的单点服务器可以解决大部分问题
1.1.2Web2.0时代
随着Web2.0的时代的到来,用户访问量大幅度提升,同时产生了大量的用户数据,
加上后来的智能移动设备的普及,。IO量急剧增大,所有的互联网平台都面临了巨大的性能挑战。
1.1.3解决CPU内存压力
解决*,登录信息就被到session对象中。第二次访问时若在B服务器时,则此时B服务器没有该用户的信息,就不能证明此时是在登录状态。
再做集群或者分布式操作时,第一次用户在A服务器登录时,登录信息就被到session对象中。第二次访问时若在B服务器时,则此时B服务器没有该用户的信息,就不能证明此时是在登录状态。
为了解决以上的session问题有如下解决方案:
1.存在客户端cookie中
好处:每次请求都带session(客户端与服务器交换的信息)
缺点(致命):由于存在客户端,那么安全性很差
2.session复制
当在A服务器登录了后,则A服务器保存了用户的session,将session复制到B,C…等其他服务器
在用户登录时候匹对sessionId即可
缺点:造成数据的冗余
3.NoSQL数据库
将存储信息放在NoSQL数据库中,在每次登陆时进行比对
好处:NoSQL不需要IO操作,它的数据完全存在内存中,读的速度很快
1.1.4解决IO压力
创建缓存数据库:减少IO压力,提高访问速度
1.2NoSQL数据库
1.2.1NoSQL数据库简介
NoSQL(NoSQL = *Not Only SQL* ),不仅是SQL,泛指****非关系型的数据库****。
***关系型数据库:***按照业务逻辑存储有关系的数据(Mysql)
NoSQL 不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力。
·不遵循SQL标准。有自己的一套标准
·不支持ACID。 ACID–>事务的四个特性:原子性、一致性、隔离性、持久性
·远超于SQL的性能。 查询效率很高
1.2.2NoSQL适用场景
· 对数据高并发的读写 (高并发的秒杀功能)
· 海量数据的读写
· 对数据高可扩展性的
1.2.3NoSQL不适用场景
·需要事务支持
· 基于sql的结构化查询存储,处理复杂的关系,需要即席查询。
· *(用不着sql的和用了sql也不行的情况,请考虑用NoSql)*
1.2.4常见的数据库
1.3行式数据库
1.4图关系型数据库
好友推荐功能
2.Redis数据库概述
2.1概述
Ø Redis是一个开源的key-value存储系统。
Ø 和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。
Ø 这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。
Ø 在此基础上,Redis支持各种不同方式的排序。
Ø 与memcached一样,为了保证效率,数据都是缓存在内存中。
Ø 区别的是Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件。
Ø 并且在此基础上实现了master-slave(主从)同步。
2.2应用场景
2.2.1配合关系型数据库做缓存
高频次,热门访问的数据,降低数据库IO
分布式架构,做session共享
2.2.2多样的数据结构存储持久化数据
2.3Redis启动和关闭
2.3.1前台启动
前台启动,命令行窗口不能关闭,否则服务器停止
ctrl+c退出
2.3.2后台启动
[root@thhlinux bin]# ./redis-server /etc/redis.conf
此时把后台关闭了,redis也还在启动
查看redis是否启动:[root@thhlinux bin]# ps -ef | grep redis
redis连接端口号:
测试:
2.3.3关闭redis
直接shutdown
关闭进程
2.3Redis相关知识
Redis端口6379的来源:九键6379对应merz
选择不同的库:
串行:当有1,2,3,三个操作时,要等1完成了才做2操作,2完成了才做3操作。
Redis使用***单线程+多路IO复用(Redis)***技术:
多个用户托黄牛去火车站买票,在黄牛买票时,用户可以做自己的事情,当黄牛买到了对应用户的票时,通知来取票,其他用户继续做自己的事情。
让CPU一直工作而不休息。
3.Redis中常用的五大数据类型
redis常见数据类型操作命令
http://www.redis.cn/commands.html
3.1Redis键(Key)
增加key:
set key(需要的key) value(需要的value)
String是Redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value。
String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。
String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M
3.2.1字符串中常见命令
set 添加键值对 设置相同的key就会覆盖原理的value
get 查询对应键值
append 将给定的 追加到原值的末尾
strlen 获得值的长度
setnx 只有在 key 不存在时 设置 key 的值
incr 将 key 中储存的数字值增1 只能对数字值操作,如果为空,新增值为1
decr 将 key 中储存的数字值减1 只能对数字值操作,如果为空,新增值为-1
incrby / decrby <步长> 将 key 中储存的数字值根据自定义步长增减。步长不能为负
原子性:
所谓****原子****操作是指不会被线程调度机制打断的操作;
这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。
(1)在单线程中, 能够在单条指令中完成的操作都可以认为是"原子操作",因为中断只能发生于指令之间。
(2)在多线程中,不能被其它进程(线程)打断的操作就叫原子操作。
Redis单命令的原子性主要得益于Redis的单线程。
面试题:
java中的i++是否是原子操作 ----->不是,java是多线程的
eg:i=0,此时有两个线程分别对i进行++100次,最后i的值是多少?
由于java是多线程的会互相干扰 最后2<=i<=200
i++分为三步:i++,取值,++,赋值
mset …
同时设置一个或多个 key-value对
mget …
同时获取一个或多个 value
msetnx …
同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。
*原子性,有一个失败则都失败*
getrange <起始位置><结束位置>
获得值的范围,类似java中的substring,*前包,后包 []*
setrange <起始位置>
用 覆写所储存的字符串值,从<起始位置>开始(*索引从0开始*)。
*setex <过期时间**>*
设置键值的同时,设置过期时间,单位秒。
getset
以新换旧,设置了新值同时获得旧值。
ttl 查看过期时间
3.2.2String的数据结构
String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。
value是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.
如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。
当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。
3.3Redis中的List(列表)
单键多值
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
3.3.1常用命令
lpush/rpush … 从左边/右边插入一个或多个值。
lpop/rpop 从左边/右边吐出一个值。值在键在,值光键亡。
rpoplpush 从列表右边吐出一个值,插到列表左边。
lrange
lrange mylist 0 -1 0左边第一个,-1右边第一个,(0-1表示获取所有)
lindex 按照索引下标获得元素(从左到右)
llen 获得列表长度
linsert before 在的后面插入插入值
lrem 从左边删除n个value(从左到右)
lset 将列表key下标为index的值替换成value
3.3.2List的数据结构
List的数据结构为快速链表quickList。
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。
它将所有的元素紧挨着一起存储,分配的是一块连续的内存。
当数据量比较多的时候才会改成quicklist。
因为普通的链表需要的附加指针空间太大,会比较浪费空间。
比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。
3.4Redis中的Set集合
Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以****自动排重****的,
当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择
,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的****复杂度都是O(1)****。
一个算法,随着数据的增加,执行时间的长短,如果是O(1),数据增加,查找数据的时间不变
3.4.1常见命令
sadd …
将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略
smembers 取出该集合的所有值。
sismember 判断集合是否为含有该值,有1,没有0
scard返回该集合的元素个数。
srem … 删除集合中的某个元素。
spop *随机从该集合中吐出一个值。*
srandmember 随机从该集合中取出n个值。不会从集合中删除 。
smove value把集合中一个值从一个集合移动到另一个集合
sinter 返回两个集合的交集元素。
sunion 返回两个集合的并集元素。
sdiff 返回两个集合的****差集****元素(key1中的,不包含key2中的)
3.4.2Set数据结构
Set数据结构是dict字典,字典是用哈希表实现的。
Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。
Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。
3.5Redis中的Hash
Redis hash 是一个键值对集合。
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。
类似Java里面的Map<String,Object>
用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储
主要以下两种存储方式:
3.5.1常用命令
hset 给集合中的 键赋值
hget 从集合取出 value
hmset … 批量设置hash的值
hexists查看哈希表 key 中,给定域 field 是否存在。
hkeys 列出该hash集合的所有field
hvals 列出该hash集合的所有value
hincrby 为哈希表 key 中的域 field 的值加上增量 1 -1
hsetnx 将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在 .
3.5.2Hash数据结构
Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。
3.6Redis中有序集合Zset
Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。
不同之处是有序集合的每个成员都关联了一个****评分(score)****,这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。
因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。
访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。
适合于做一个排行榜。
3.6.1常用命令
zadd … 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。
zrange
带WITHSCORES,可以让分数一起和值返回到结果集。
zrangebyscore key minmax [withscores] [limit offset count]
返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。
zrevrangebyscore key maxmin [withscores] [limit offset count]
同上,改为从大到小排列。
zincrby 为元素的score加上增量
zrem 删除该集合下,指定值的元素
zcount 统计该集合,分数区间内的元素个数
zrank 返回该值在集合中的排名,从0开始。
3.6.2数据结构
SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map<String, Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。
zset底层使用了两个数据结构
(1)hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。
(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。
3.6.3跳跃表
1、简介
有序集合在生活中比较常见,例如根据成绩对学生排名,根据得分对玩家排名等。对于有序集合的底层实现,可以用数组、平衡树、链表等。数组不便元素的插入、删除;平衡树或红黑树虽然效率高但结构复杂;链表查询需要遍历所有效率低。Redis采用的是跳跃表。跳跃表效率堪比红黑树,实现远比红黑树简单。
2、实例
对比有序链表和跳跃表,从链表中查询出51
(1) 有序链表
要查找值为51的元素,需要从第一个元素开始依次查找、比较才能找到。共需要6次比较。
(2) 跳跃表
从第2层开始,1节点比51节点小,向后比较。
21节点比51节点小,继续向后比较,后面就是NULL了,所以从21节点向下到第1层
在第1层,41节点比51节点小,继续向后,61节点比51节点大,所以从41向下
在第0层,51节点为要查找的节点,节点被找到,共查找4次。
从此可以看出跳跃表比有序链表效率要高
4.配置文件详解
进入配置文件
4.1单位
不区分大小写。开头定义了一些基本的度量单位,只支持bytes,不支持bit
4.2INCLUDES包含
类似jsp中的include,多实例的情况可以把公用的配置文件提取出来
4.3NETWORK网络配置
4.3.1本机的访问请求
默认情况bind=127.0.0.1只能接受本机的访问请求
不写的情况下,无限制接受任何ip地址的访问
生产环境肯定要写你应用服务器的地址;服务器是需要远程访问的,所以需要将其注释掉
4.3.2 访问保护模式
将本机访问保护模式设置no
port 端口号,默认 6379
4.3.3daemonize
修改daemonize属性将no 改为yes
注:该属性是将redis后台运行
4.3.4开放6379端口
/sbin/iptables -I INPUT -p tcp --dport 6379 -j ACCEPT
一些关于防火墙的Linux命令
firewall-cmd --list-ports 查看所有开启的端口
systemctl status firewalld 查看防火墙状态
firewall-cmd --zone=public --add-port=6379/tcp --permanent 开启6379端口
systemctl restart firewalld.service 重启防火墙
systemctl stop firewalld.service #停止firewall
systemctl disable firewalld.service #禁止firewall开机启动
4.3.5timeout
timeout 一个空闲的客户端维持多少秒会关闭,0表示关闭该功能。即永不关闭。
tcp-keepalive:对访问客户端的一种心跳检测,每个n秒检测一次。单位为秒,如果设置为0,则不会进行Keepalive检测,建议设置成60
shutdown:关机
4.4GENERAL通用
4.4.1daemonize
是否为后台进程,设置为yes
守护进程,后台启动:
4.4.2pidfile
存放pid文件的位置,每个实例会产生一个不同的pid文件:
4.4.3loglevel
指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为****notice****
四个级别根据使用阶段来选择,生产环境选择notice 或者warning
4.4.4database
设定库的数量 默认16,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id
4.5SECURITY安全
4.5.1设置密码
访问密码的查看、设置和取消
在命令中设置密码,只是临时的。重启redis服务器,密码就还原了。
永久设置,需要再配置文件中进行设置。
打开901行代码 表示redis需要密码 然后在如下设置密码
4.6LIMITS限制
# 设置能连上 redis 的最大客户端连接数量
maxclients 10000
# redis 配置的最大内存容量
maxmemory <bytes>
# maxmemory-policy 内存达到上限的处理策略:
# volatile-lru:利用 LRU 算法移除设置过过期时间的 key。
# volatile-random:随机移除设置过过期时间的 key。
# volatile-ttl:移除即将过期的 key,根据最近过期时间来删除(辅以 TTL)
# allkeys-lru:利用 LRU 算法移除任何 key。
# allkeys-random:随机移除任何 key。
# noeviction:不移除任何 key,只是返回一个写错误。
maxmemory-policy noeviction
4.6.1maxclients
Ø 设置redis同时可以与多少个客户端进行连接。
Ø 默认情况下为10000个客户端。
Ø 如果达到了此限制,redis则会拒绝新的连接请求,并且向这些连接请求方发出“max number of clients reached”以作回应。
4.6.2maxmemory
Ø 建议****必须设置****,否则,将内存占满,造成服务器宕机
Ø 设置redis可以使用的内存量。一旦到达内存使用上限,redis将会试图移除内部数据,移除规则可以通过maxmemory-policy来指定。
Ø 如果redis无法根据移除规则来移除内存中的数据,或者设置了“不允许移除”,那么redis则会针对那些需要申请内存的指令返回错误信息,比如SET、LPUSH等。
Ø 但是对于无内存申请的指令,仍然会正常响应,比如GET等。如果你的redis是主redis(说明你的redis有从redis),那么在设置内存使用上限时,需要在系统中留出一些内存空间给同步队列缓存,只有在你设置的是“不移除”的情况下,才不用考虑这个因素。
5.Redis中的发布和订阅
5.1介绍发布与订阅
Redis 发布订阅 (pub/sub) 是一种消息通信模式:
发送者 (pub) 发送消息,订阅者 (sub) 接收消息。
Redis 客户端可以订阅任意数量的频道。
5.2命令实现发布与订阅
5.2.1终端A订阅channel1
5.2.2终端B发布信息
此时终端A接收到信息
注:发布的消息没有持久化,如果在订阅的客户端收不到hello,只能收到订阅后发布的消息
原理扩展
- Redis 是使用 C 实现的,通过分析 Redis 源码里的 pubsub.c 文件,可以了解发布和订阅机制的底层实现。
- Redis 通过 publish 、subscribe 和 psubscribe 等命令实现发布和订阅功能。
- 通过 subscribe 命令订阅某频道后,redis-server 里维护了一个字典,字典的键就是一个个 channel。
- 而字典的值则是一个链表,链表中保存了所有订阅这个 channel 的客户端。
- subscribe 命令的关键,就是将客户端添加到给定 channel 的订阅链表中。
- 通过 publish 命令向订阅者发送消息,redis-server 会使用给定的频道作为键,在它所维护的 channel 字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。
- pub / sub 从字面上理解就是发布(Publish)与订阅(Subscribe),在 Redis 中,你可以设定对某一个 key 值进行消息发布及消息订阅,当一个 key 值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。
- 这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。
使用场景
1.实时聊天(频道当聊天室)
2.消息实时系统
3.订阅、关注等功能
6.Redis新数据类型
6.1Bitmaps
现代计算机用二进制(位) 作为信息的基础单位, 1个字节等于8位, 例如“abc”字符串是由3个字节组成。
但实际在计算机存储时将其用二进制表示, “abc”分别对应的ASCII码分别是97、 98、 99,
对应的二进制分别是01100001、 01100010和01100011,
如下图
合理地使用操作位能够有效地提高内存使用率和开发效率。
Redis提供了Bitmaps这个“数据类型”可以实现对位的操作:
(1) Bitmaps本身不是一种数据类型, 实际上它就是字符串(key-value) , 但是它可以对字符串的位进行操作。
(2) Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。
6.1.1常用命令
6.1.1.1setbit
setbit设置Bitmaps中某个偏移量的值(0或1)
*offset:偏移量从0开始
eg:统计用户是否访问过网站,将访问的用户记做1, 没有访问的用户记做0, 用偏移量作为用户的id。
unique:users:20201106代表2020-11-06这天的独立访问用户的Bitmaps
注:
很多应用的用户id以一个指定数字(例如10000) 开头, 直接将用户id和Bitmaps的偏移量对应势必会造成一定的浪费, 通常的做法是每次做setbit操作时将用户id减去这个指定数字。
在第一次初始化Bitmaps时, 假如偏移量非常大, 那么整个初始化过程执行会比较慢, 可能会造成Redis的阻塞。
6.1.1.2getbit
getbit获取Bitmaps中某个偏移量的值
获取键的第offset位的值(从0开始算)
1访问过,5没有访问过,不存在的也返回0
6.1.1.3bitcount
统计****字符串****被设置为1的bit数。
一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start 或 end 参数,可以让计数只在特定的位上进行。
start 和 end 参数的设置,都可以使用负数值:比如 -1 表示最后一个位,而 -2 表示倒数第二个位,start、end 是指bit组的字节的下标数,二者皆包含。
举例: K1 【01000001 01000000 00000000 00100001】,对应【0,1,2,3】
bitcount K1 1 2 : 统计下标1、2字节组中bit=1的个数,即01000000 00000000
–》bitcount K1 1 2 --》1
bitcount K1 1 3 : 统计下标1、2字节组中bit=1的个数,即01000000 00000000 00100001
–》bitcount K1 1 3 --》3
bitcount K1 0 -2 : 统计下标0到下标倒数第2,字节组中bit=1的个数,即01000001 01000000 00000000
–》bitcount K1 0 -2 --》3
6.1.1.4bitop
bitop and(or/not/xor) [key…]
bitop是一个复合操作, 它可以做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果保存在destkey中。
eg:
2020-11-04 日访问网站的userid=1,2,5,9。
setbit unique:users:20201104 1 1
setbit unique:users:20201104 2 1
setbit unique:users:20201104 5 1
setbit unique:users:20201104 9 1
2020-11-03 日访问网站的userid=0,1,4,9。
setbit unique:users:20201103 0 1
setbit unique:users:20201103 1 1
setbit unique:users:20201103 4 1
setbit unique:users:20201103 9 1
计算出两天都访问过网站的用户数量
bitop and unique:users:and:20201104_03
unique:users:20201103unique:users:20201104
6.1.2bitmaps和set区别
假设网站有1亿用户, 每天独立访问的用户有5千万, 如果每天用集合类型和Bitmaps分别存储活跃用户可以得到表
set和Bitmaps存储一天活跃用户对比 | |||
---|---|---|---|
数据类型 | 每个用户id占用空间 | 需要存储的用户量 | 全部内存量 |
集合类型 | 64位 | 50000000 | 64位*50000000 = 400MB |
Bitmaps | 1位 | 100000000 | 1位*100000000 = 12.5MB |
很明显, 这种情况下使用Bitmaps能节省很多的内存空间, 尤其是随着时间推移节省的内存还是非常可观的
set和Bitmaps存储独立用户空间对比 | |||
---|---|---|---|
数据类型 | 一天 | 一个月 | 一年 |
集合类型 | 400MB | 12GB | 144GB |
Bitmaps | 12.5MB | 375MB | 4.5GB |
但Bitmaps并不是万金油, 假如该网站每天的独立访问用户很少, 例如只有10万(大量的僵尸用户) , 那么两者的对比如下表所示, 很显然, 这时候使用Bitmaps就不太合适了, 因为基本上大部分位都是0。
set和Bitmaps存储一天活跃用户对比(独立用户比较少) | |||
---|---|---|---|
数据类型 | 每个userid占用空间 | 需要存储的用户量 | 全部内存量 |
集合类型 | 64位 | 100000 | 64位*100000 = 800KB |
Bitmaps | 1位 | 100000000 | 1位*100000000 = 12.5MB |
6.2HyperLogLog
在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站PV(PageView页面访问量),可以使用Redis的incr、incrby轻松实现。
但像UV(UniqueVisitor,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题。
解决基数问题有很多种方案:
(1)数据存储在MySQL表中,使用distinct count计算不重复个数
(2)使用Redis提供的hash、set、bitmaps等数据结构来处理
以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。
能否能够降低一定的精度来平衡存储空间?Redis推出了HyperLogLog
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
什么是基数?
比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。
6.2.1pfadd
6.2.2pfcount
6.2.3pfmerge
6.3Geospatial
Redis 3.2 中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。
7.Jedis
7.1介绍
通过java来操作redis
Jedis是redis的java版本的客户端实现,使用Jedis提供的Java API对Redis进行操作,是Redis官方推崇的方式;
并且,使用Jedis提供的对Redis的支持也最为灵活、全面;不足之处,就是编码复杂度较高。
7.2连接Redis实现Jedis
7.2.1导入jedis依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
7.2.2创建jedis对象
连接测试redis
注意此时要完成本文4配置文件中以下四点才能成功----->4.3.1-4.3.4
public class JedisDemo1 {
public static void main(String[] args) {
//创建jedis对象
//host:redis中的ip地址,port:redis的端口号
Jedis jedis=new Jedis("192.168.112.100",6379);
//测试
String ping = jedis.ping();
System.out.println(ping);
}
}
测试结果pong表明与redis正常连接了
7.2.3jedis操作
方法几乎和redis命令行中一样
String的操作
@Test
public void demo1(){
Jedis jedis=new Jedis("192.168.112.100",6379);
//和redis中一样 set创建key
jedis.set("Adrian","v1");
jedis.set("Momo","v2");
//获取keyRequestPara
String adrian = jedis.get("Adrian");
System.out.println(adrian);
//和redis中一样 获取redis中所有的key
Set<String> keys = jedis.keys("*");
for (String key : keys) {
System.out.println(key);
}
//mset 创建多个key
jedis.mset("Luna","v3","SMoon","v4");
//mget获取多个key 返回list集合
List<String> mget = jedis.mget("Luna", "SMoon");
System.out.println(mget);
jedis.close();
}
List的操作
set操作
hash操作
Zet操作
7.3Jedis模拟手机验证码
7.3.1要求:
1、输入手机号,点击发送后随机生成6位数字码,2分钟有效
2、输入验证码,点击验证,返回成功或失败
3、每个手机号每天只能输入3次
7.3.2需求分析
1.随机生成6位数字验证码
利用java中的Random(随机数)类
2.验证码两分钟有效
把验证码放入redis中,并且设置过期时间为120秒
3.验证功能
判断验证码是否一致,直接用输入的验证码和redis中获取的验证码进行比较即可
4.每个手机号每天只能输入3次
redis中的incr命令 每次发送之后+1
大于2则不能发送了
7.3.3代码实现
package com.thh.shirodemo.util;
import redis.clients.jedis.Jedis;
import java.util.Random;
/**手机验证码
* @author shkstart
* @create 2022-07-25-11:10
*/
public class PhoneCode {
public static void main(String[] args) {
//测试随机的验证码
System.out.println(getCode());
}
//1 生成手机验证码
public static String getCode(){
Random random = new Random();
String code="";
for(int i=0;i<6;i++){
int rand=random.nextInt(10);
code+=rand;
}
return code;
}
//2 每个手机每天只能发送三次,验证码放到redis中,设置过期时间
public static String verifyCode(String userId){
//连接redis
Jedis jedis = new Jedis("192.168.112.100", 6379);
//拼接key v
//手机发送次数key
String countKey="VerifyCode--"+userId+":count";
//验证码key
String codeKey="VerifyCode--"+userId+":code";
// 每个手机每天只能发送三次
String count = jedis.get(countKey);
if(count==null){
//没有发送次数 第一次发送
//设置发送次数是1
jedis.setex(countKey,24*60*60,"1");
}else if (Integer.parseInt(count)<=2){
//发送次数+1
jedis.incr(codeKey);
}else{
jedis.close();
return ("发送失败!您今天的验证码发送次数超过三次了!");
}
//发送验证码到redis中
String vcode=getCode();
jedis.setex(codeKey,120,vcode);
jedis.close();
return "发送成功请查收!";
}
//3.验证码校验
public static String checkRedisCode(String userId,String code){
Jedis jedis = new Jedis("192.168.112.100", 6379);
//从redis中获取验证码
String codeKey="VerifyCode--"+userId+":code";
String redisCode=jedis.get(codeKey);
if(redisCode.equals(code)){
return "验证码验证成功";
}
return "验证码验证失败";
}
}
测试代码
package com.thh.shirodemo.controller;
import com.sun.org.apache.bcel.internal.classfile.Code;
import com.thh.shirodemo.bean.User;
import com.thh.shirodemo.util.PhoneCode;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author shkstart
* @create 2022-07-25-21:23
*/
@RestController
@RequiresPermissions("salary")
public class RedisCodeController {
/**
*生成验证码
*/
@RequestMapping("/getCode")
public String getCode(){
//获取当前对象
Subject subject = SecurityUtils.getSubject();
User currentUser = (User)subject.getSession().getAttribute("currentUser");
String msg = PhoneCode.verifyCode(String.valueOf(currentUser.getUserId()));
return msg;
}
/**
* 校验验证码
*/
@RequestMapping(value = "checkCode")
public String checkCode(@RequestParam(value = "codes") String code){
//获取当前对象
Subject subject = SecurityUtils.getSubject();
User currentUser = (User)subject.getSession().getAttribute("currentUser");
String msg = PhoneCode.checkRedisCode(String.valueOf(currentUser.getUserId()),code);
return msg;
}
}
8.springboot整合redis
SpringBoot 整合 Redis 是使用 SpringData 实现的。
在企业中pojo类都会实现可序列化 实现Serializable
8.1引入redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring2.X集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
在springboot2.x之后,原来使用的jedis被替换成了lettuce
jedis:采用的直连redis,多个线程操作的话是不安全的,如果想要避开不安全的,使用jedis pool连接池,更像bio模式
lettuce:采用netty(高性能网络框架,异步请求,很快)实例可以再多个线程中共享,不存在线程不安全的情况,可以减少线程数据,更像nio模式
8.2配置文件
spring:
redis:
#Redis服务器地址
host: 192.168.112.100
#Redis服务器连接端口
port: 6379
#Redis数据库索引(默认为0)
database: 0
#连接超时时间(毫秒)
timeout: 1800000
jedis:
pool:
#连接池最大连接数(使用负值表示没有限制)
max-active: 200
#最大阻塞等待时间(负数表示没限制)
max-wait: -1
#连接池中的最大空闲连接
max-idle: 32
#连接池中的最小空闲连接
min-idle: 0
# password: adrian123
lettuce:
pool:
enabled: true
8.3自定义redis配置类
源码分析
@AutoConfiguration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 默认的 RedisTemplate 没有过多的设置,Redis 对象都是需要序列化的
// 两个泛型都是 Object 的类型,我们使用需要强制转换,很不方便,我们仓用是 <String, Object>
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
// 由于 String 是 Redis 最常使用的类型,所以说单独提出来了一个 Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
}
通过源码可以看出,SpringBoot 自动帮我们在容器中生成了一个 RedisTemplate 和一个 StringRedisTemplate。
但是,这个 RedisTemplate 的泛型是 <Object, Object>,写代码不方便,需要写好多类型转换的代码。
我们需要一个泛型为 <String, Object> 形式的 RedisTemplate。
并且,这个 RedisTemplate 没有设置数据存在 Redis 时,key 及 value 的序列化方式。
由 @ConditionalOnMissingBean 可以看出,如果 Spring 容器中有了自定义的 RedisTemplate 对象,自动配置的 RedisTemplate 不会实例化。
因此我们可以直接自己写个配置类,配置 RedisTemplate如下。
package com.thh.shirodemo.config;
/**
* @author shkstart
* @create 2022-07-26-23:36
*/
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import javax.annotation.Resource;
import java.time.Duration;
//开启缓存注解
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
//自己定义了一个RedisTemplate
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
// 定义泛型为 <String, Object> 的 RedisTemplate
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
// 设置连接工厂
template.setConnectionFactory(factory);
// 定义 Json 序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
// Json 转换工具
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 定义 String 序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
8.4Redis工具类
直接用 RedisTemplate 操作 Redis,比较繁琐。
因此直接封装好一个 RedisUtils,这样写代码更方便点。
这个 RedisUtils 交给Spring容器实例化,使用时直接注解注入。
package cn.sail.redisspringboot.util;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* Redis 工具类
*
* @author LiaoHang
* @date 2022-06-09 22:15
*/
@Component
public class RedisUtil {
@Resource
private RedisTemplate<String, Object> redisTemplate;
// =============================Common 基础============================
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
// 如果返回值为 null,则返回 0L
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
}
}
}
// ============================String 字符串=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time,
TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ===============================List 列表=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0
* 时,-1,表尾,-2倒数第二个元素,依次类推
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return 赋值结果
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return 赋值结果
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return 赋值结果
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
return redisTemplate.opsForList().remove(key, count, value);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ============================Set 集合=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time);
}
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
return redisTemplate.opsForSet().remove(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ================================Hash 哈希=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
}
8.5测试
@RestController
@RequestMapping(redisTest)
public class RedisTestController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping
public String testRedis() {
//设置值到redis
redisTemplate.opsForValue().set("name","lucky");
//从redis获取值
String name = (String)redisTemplate.opsForValue().get("name");
return name;
}
9事务
9.1事务介绍
Redis 事务的本质是一组命令的集合,一个事务中所有的命令都会被序列化,在事务执行的过程中,会照顺序执行
队列 set set set 执行
一次性、顺序性、排他性,执行一些命令。
Redis 事务没有隔离级别的概念
所有的命令在事务中,并没有直接执行,只有发起执行命令的时候才会执行。
Redis单条命令保证原子性(要么同时成功,要么同时失败)的,但是事务不保存原子性。
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。
9.2事务基本命令
Redis事务流程
1.开启事务(multi)
开启事务后,会出现 TX 标志,此时所有的操作不会马上有结果,而是形成队列(QUEUED),待执行事务后,会将所有命令按顺序执行。
2.命令入队()
3.执行事务(exec)
127.0.0.1:6379> MULTI #开启事务
OK
#命令入队
127.0.0.1:6379(TX)> set k1 adrian
QUEUED
127.0.0.1:6379(TX)> set k2 thh
QUEUED
127.0.0.1:6379(TX)> set k3 cxy
QUEUED
127.0.0.1:6379(TX)> exec #执行事务
1) OK
2) OK
3) OK
127.0.0.1:6379>
事务执行完,该组事务(队列)就没了,需要重新开启事务
放弃事务(discard)
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379(TX)> set k1 v1 # 命令入队
QUEUED
127.0.0.1:6379(TX)> set k2 v2 # 命令入队
QUEUED
127.0.0.1:6379(TX)> set k3 33 # 命令入队
QUEUED
127.0.0.1:6379(TX)> discard # 取消事务
OK
127.0.0.1:6379> get k3 # set命令未执行
"v3"
9.3异常
事务中存在命令性错误
若在事务队列中存在命令性错误(类似于java编译性错误(代码使用出错)),则执行 exec
命令时,所有命令都不会执行。
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> getset k4 #命令出错了 事务不执行
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set k5 v5
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
事务中存在语法性错误
若在事务队列中存在语法性错误(类似于 Java 的的运行时异常(eg1/0),但是在java代码中出现1/0的异常 则是都不执行的),则执行 exec
命令时,其他正确命令会被执行,错误命令抛出异常。(所以redis事务没有原子性)
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> incr k1 #k1不是int类型 语法没错但 运行时异常 后面代码也会执行
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
2) (error) ERR value is not an integer or out of range #执行错误的命令会报错,其余命令正常执行
3) OK
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
9.4监控Watch(锁)
悲观锁,认为什么时候都会出问题(上厕所 锁门),无论什么时候都会上锁
相对效率低下
乐观锁,认为什么时候都不会出问题,所以不会上锁!更新数据的时候才去判断在此期间是否有人修改过数据
获取version
更新的时候比较version
测试多线程(终端)修改值,使用watch可以当做redis的乐观锁操作
1.正常执行 100块余额取出20块
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money #监控money对象
OK
127.0.0.1:6379> MULTI #事务正常结束,数据期间没有发生改变,代码正常执行成功
OK
127.0.0.1:6379(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)> exec #事务执行完成后 监控就取消了
1) (integer) 80
2) (integer) 20
2.两个终端
此时打开两个终端(终端),在B终端exec前A终端修改了money,此时的money是被watch监控的,money被改变所以事务执行失败。(乐观锁)
无论事务是否执行成功,都自动解锁了。
也可以手动解锁 unwatch
127.0.0.1:6379> watch money #监视money对象
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)> EXEC #执行之前,另一个线程改变了money,此时导致事务执行失败
(nil)
watch
指令类似于乐观锁,在事务提交时,如果 watch
监控的多个 key 中任何 key 的值已经被其他客户端更改。
则使用 exec
执行事务时,事务队列将不会被执行,同时返回 (nil) 应答以通知调用者事务执行失败。
9.5Jedis中的事务
@Test
public void demo2(){
Jedis jedis=new Jedis("192.168.112.100",6379);
//开启事务
Transaction multi = jedis.multi();
HashMap<String, String> map = new HashMap<>();
map.put("adrian","thh");
map.put("Smoon","cxy");
// jedis.watch("u1");
try {
multi.set("u1", String.valueOf(map));
multi.set("u2", String.valueOf(map));
int i=1/0;//此时故意代码抛出异常,执行失败!
multi.exec();//执行事务
}catch (Exception e){
multi.discard();//如果出现异常就放弃事务
e.printStackTrace();
}finally {
System.out.println(jedis.get("u1"));
System.out.println(jedis.get("u2"));
}
jedis.close();//关闭连接
}
10redis中的测试工具redis-benchmark
测试100个并发 100个请求
*11.Redis持久化
11.1概述
Redis 是内存数据库,即数据存储在内存。
如果不将内存中的数据保存到磁盘,一旦服务器进程退出,服务器中的数据也会消失。
这样会造成巨大的损失,所以 Redis 提供了持久化功能。
11.2RDB( Redis DataBase 默认的redis数据库)
RDB简介
在指定的时间间隔内将内存中的数据集快照写入磁盘。
也就是 Snapshot 快照,恢复时是将快照文件直接读到内存里。
Redis会单独创建(fork)一个子进程来进行持久化。
会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。
整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。
如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。
RDB 的缺点是最后一次持久化后的数据可能丢失。—默认情况就是RDB不需要设置
rdb保存的文件时 dump.rdb 都是在配置文件中快照进行配置的
RDB的触发
- 配置文件中默认的快照配置,建议多用一台机子作为备份,复制一份 dump.rdb。
- 保存配置:
- save:只管保存,其他不管,全部阻塞。
- bgsave:Redis 会在后台异步进行快照操作,快照同时还可以响应客户端请求。
- lastsave:获取最后一次成功执行快照的时间。
- 执行 flushall 命令,也会产生 dump.rdb 文件,但里面是空的,无意义 。
- 退出的时候也会产生 dump.rdb 文件。
修改rdb配置 save 60 5 —用于测试 只要在60秒内修改了5次key 就会触发rdb操作
一开始默认有rdb文件 我们删除后 在数据库中60S内添加5条数据 就又生成了rdb文件
恢复RDB文件
如果该目录下存在dump.rdb文件 启动就会自动恢复其中的数据
RDB优点
1.适合大规模的数据恢复
2.对数据的完整性要求不高
RDB缺点
1.需要一定的时间间隔来执行进程操作,如果redis意外宕机了,那么最后一次修改得数据就没了。
2.fork进程的时候 会占用更多的内存空间
小结
11.3AOF(Append Only File 只追加文件)
AOF简介
将我们所有的命令(写入的操作)都进行记录,恢复的时候就把这个文件全部执行一遍
只许追加文件,但不可以改写文件,Redis 启动之初会读取该文件重新构建数据。
换言之,Redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
开启AOF
默认是不开启的 在配置文件中修改 将no改为yes 来开启
修改持久化配置
# appendfsync always 每次修改都会sync 消耗性能
appendfsync everysec #每一秒钟修改(代表如果在最后一秒宕机那么最后一秒的数据会丢失)
# appendfsync no 不执行sync 此时操作系统自己同步数据 速度最快
AOF修复
若AOF文件被恶意破坏 那么下次启动redis是无法正常启动的
此时我们可以使用redis的工具 redis-check-aof --fix 来对aof文件进行修复
如果文件正常那么重启就可以正常启动了(但是可能会有容错修复了)
aof的重写
AOF默认采用文件无限追加方式,文件会越来越大,为避免出现此种情况,新增了重写机制。
当AOF文件的大小超过所设定的阈值时,Redis 就会启动 AOF 文件的内容压缩。
只保留可以恢复数据的最小指令集,可以使用命令 bgrewriteaof
。
重写原理:
AOF 文件持续增长而过大时,会 Fork 出一条新进程来将文件重写(也是先写临时文件最后再 rename)。
遍历新进程的内存中数据,每条记录有一条的 set 语句。
重写 aof 文件的操作,并没有读取旧的 aof 文件,这点和快照有点类似。
重写触发机制:
Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次 rewrite 后大小的 1 倍且文件大于 64M 时触发。
AOF缺点
1.每一次修改都同步(让文件的完整性更好)
2.每秒都同步 可能丢失最后一秒的数据
3.从不同步 效率高
AOF优点
1.相对数据文件 aof文件大于rdb 修复速度也更慢
2.aof(读写的操作)运行效率也比rdb更慢,所以默认为rdb
扩展:
- RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储。
- AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AO F命令以 Redis 协议追加保存每次写的操作到文件末尾,Redis 还能对 AOF 文件进行后台重写,使得 AOF 文件的体积不至于过大。
- 只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化。
- 同时开启两种持久化方式:
- 在这种情况下,当 redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。
- RDB 的数据不实时,同时使用两者时服务器重启也只会找 AOF 文件,那要不要只使用AOF呢?作者建议不要,因为 RDB 更适合用于备份数据库(AOF 在不断变化不好备份),快速重启,而且不会有 AOF 可能潜在的 Bug,留着作为一个万一的手段。
- 性能建议:
- 因为 RDB 文件只用作后备用途,建议只在 Slave(从节点) 上持久化 RDB 文件,而且只要 15 分钟备份一次就够了,只保留 save 900 1 这条规则。
- 如果开启 AOF ,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只 load 自己的AOF文件就可以了,代价一是带来了持续的 IO,二是AOF rewrite 的最后将 rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少 AOF rewrite 的频率,AOF重写的基础大小默认值 64M 太小了,可以设到 5G 以上,默认超过原大小 100% 大小重写可以改到适当的数值。
- 如果不开启 AOF ,仅靠 Master-Slave Repllcation(主从复制) 实现高可用性也可以,能省掉一大笔IO,也减少了 rewrite 时带来的系统波动。代价是如果 Master/Slave 同时挂掉,会丢失十几分钟的数据,启动脚本也要比较两个 Master/Slave 中的 RDB 文件,载入较新的那个(微博就是这种架构)。
*12主从复制
12.1概念
①主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器
前者称为主节点(master / leader),后者称为从节点(slave / follower)。
②主从复制读写分离 大部分情况都是在读操作(把读操作的所有压力转移到从机上,来提升主机写的效率)
数据的复制是单向的,只能由主节点到从节点。
Master 以写为主,Slave 以读为主。
③一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
④默认情况下,每台 Redis 服务器都是主节点。
⑤若要配一个redis集群,最低也是一主二从
12.2主从复制的主要作用
数据冗余
主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
故障恢复
当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复。这也是一种服务的冗余。
负载均衡
在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务,分担服务器负载。
尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
高可用(集群)
主从复制是哨兵和集群能够实施的基础,因此说主从复制是 Redis 高可用的基础。
一般来说,要将 Redis 运用于工程项目中,只使用一台 Redis 是万万不能的(redis可能会宕机),原因如下:
- 结构上:单个 Redis 服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大。
- 容量上:单个 Redis 服务器内存容量有限,一般来说,单台 Redis 最大使用内存不应该超过 20G。
应用
电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是多读少写。
对于这种场景,我们可以使用如下这种架构:
12.3环境配置
查看当前库的信息
[root@thhlinux ~]# cd /usr/local/redis/bin
[root@thhlinux bin]# ./redis-server /etc/redis.conf
[root@thhlinux bin]# ./redis-cli
127.0.0.1:6379> info replication #查看当前库的信息
# Replication
role:master #角色 master 默认情况下,每台 Redis 服务器都是主节点
connected_slaves:0 #没有从机
master_failover_state:no-failover
master_replid:0cd8474b44e37c9af255f090d1676da6d1373cae
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6379>
复制四份配置文件
修改每份配置文件的信息(以redis-80.conf为例)
①修改端口号
②修改后台运行
③修改日志文件名字
④修改dump文件的名字
分别在四个端口开启四个不同配置文件的redis
修改完毕后 启动4个redis服务器
12.4一主二从(三从)
此时还没有配置时 每个服务器都是主节点 ----默认情况下,每台 Redis 服务器都是主节点。
让每个端口连接到对应的redis端口
配置从机(命令中配置,暂时的重启后失效)
我们一般情况下只用配置从机就好了
一主(79)三从(80,81,82)
以6380为例子
127.0.0.1:6380> slaveof 127.0.0.1 6379 #配置从机 以127.0.0.1 6379为主机
OK
127.0.0.1:6380> info replication
# Replication
role:slave #角色为 从机
master_host:127.0.0.1 #主机地址(可以查看主机的信息)
master_port:6379
master_link_status:up
master_last_io_seconds_ago:5
master_sync_in_progress:0
slave_repl_offset:28
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:f71d6986ce52915f6e7d4d67bf18f254c649ae64
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:28
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:28
在主机6379中可以查看从机的信息
127.0.0.1:6379> info replication
# Replication
role:master #角色为主机
connected_slaves:0
master_failover_state:no-failover
master_replid:24ca7daea7270aaf30535acd71b7204dacd7ff0b
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:3 #以下是连接的从机的信息
slave0:ip=127.0.0.1,port=6380,state=online,offset=294,lag=0
slave1:ip=127.0.0.1,port=6381,state=online,offset=294,lag=0
slave2:ip=127.0.0.1,port=6382,state=online,offset=294,lag=1
master_failover_state:no-failover
master_replid:f71d6986ce52915f6e7d4d67bf18f254c649ae64
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:294
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:294
配置从机(从机配置文件中永久配置)
# replicaof <masterip> <masterport> #打开注解 并且加上主机的地址
# If the master is password protected (using the "requirepass" configuration
# directive below) it is possible to tell the replica to authenticate before
# starting the replication synchronization process, otherwise the master will
# refuse the replica request.
#
# masterauth <master-password> #打开注解 加上主机的密码
12.5层层链路(串串模型)
从机也可以被其他从机当作主机,可以有效减轻主机的写压力。
eg让6380作为6381的主机
此时6380依然是6379的从机
这样 6379 赋的值只需要复制到 6380,6380 再复制到 6381,这样就有效的减轻主机的写压力。
12.6slaveof no one命令(新主上任)
如果主机(6379)宕机了 需要手动选出新的主机
主机宕机后一个从机使用slaveof no one命令 该从机就变成主机了
其他从机就以该新主机为主机了
当老主机恢复后,其他从机也还是以该新主机为主机(所以要重新连接旧主机为新主机)
12.8总结
①主机可以写和读 从机只能读不能写 来提高效率
从机中如果要写,就会报错
②主机中所有信息都会热部署(同步在从机中)
③默认情况下 主机若宕机了 其他从机也还是该主机的从机但是还是没有写操作的
在主机恢复后,从机依旧可以直接获取到主机写入的信息
④从机宕机后再次启动从机是否能继续读写主机写入的信息
①若从机是通过命令行配置的从机 在宕机后重新启动默认就是主机了 就无法再读取到主机写入的信息
此时再次通过成为主机的从机后,就能再次读取到主机写入的信息
⑤复制原理
Slave启动成功连接到master后会发送一个sync同步命令
Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,maste将传送
整个数据文件到slave,并完成一次完全同步。
全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步
但是只要是重新连接master ,一次完全同步(全量复制)将被自动执行
12.7**哨兵模式(自动选择新主机)
主从切换技术的操作是:当主机宕机后,需要手动把一台从机切换为主机。
这就需要人工干预,费事费力,还会造成一段时间内服务不可用。
这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。
概述
Redis 从 2.8 开始正式提供了 Sentinel(哨兵) 架构来解决这个问题。
它是“谋朝篡位”的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从机转换为主机。
哨兵模式是一种特殊的模式,首先 Redis 提供了哨兵的命令,哨兵是**一个独立的进程,**它会独立运行。
其原理是哨兵通过发送命令,等待 Redis 服务器响应,从而监控运行的多个 Redis 实例。
哨兵的作用
- 通过发送命令,让 Redis 服务器返回监控其运行状态,包括主机和从机(若没有发回信息则说明该主/从机宕机)。
- 当哨兵监测到 master 宕机,会自动将 slave 切换成 master,然后通过发布订阅模式通知其他的从机,修改配置文件,让它们切换主机。
多哨兵模式
然而一个哨兵进程对 Redis 服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。
各个哨兵之间还会进行监控,这样就形成了多哨兵模式:
假设主机宕机,哨兵 1 先检测到这个结果,系统并不会马上进行 failover(故障转移) 过程,仅仅是哨兵 1 主观的认为主机不可用,这个现象称为主观下线。
当后面的哨兵也检测到主机不可用,并且数量达到一定值时,哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行 failover 操作。
切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从机实现切换主机,这个过程称为客观下线。
测试
目前的状态一主三从
①创建sentinel.conf(哨兵)文件
②配置哨兵监视主机redis
#sentinel monitor 被监控的名称 host port 1
sentinel monitor myredis 127.0.0.1 6379 1 #最后的1代表 当主机挂掉后salve(从机)投票来决定谁来当新主机
③启动哨兵
./redis-sentinel /etc/sentinel.conf
④此时将6379宕机模拟主机挂掉
此时6380经过哨兵监测投票将此从从机变为主机
哨兵检测到主机(6379)宕机 重新选择主机(7380)
此时将旧主机(6379)重启 6379还是不会成为新主机 只能归并到新主机成为新主机的从机
哨兵模式优点
1.哨兵集群基于主从复制模式,所有主从配置的优点都有
2.主从可以切换,故障可以转移,系统的可用性就会更好
3.哨兵模式就是主从模式的升级—手动选新主机到自动选择更加健壮
哨兵模式缺点
1.redis不好在线扩容,集群容量一旦到达一定的上限,在线扩容就是十分麻烦
2.实现哨兵模式的配置其实特别麻烦,里面有很多选择
哨兵模式完整配置
# 哨兵 sentinel 实例运行的端口 默认 26379
port 26379
# 哨兵 sentinel 的工作目录
dir /tmp
# 哨兵 sentinel 监控的 redis 主节点的 ip port
# master-name 可以自己命名的主节点名字:只能由字母 A-z、数字 0-9、".-_"这三个字符组成。
# quorum 配置多少个 sentinel 哨兵统一认为 master 主节点失联那么这时客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2
# 当在 Redis 实例中开启了 requirepass foobared 授权密码 这样所有连接 Redis 实例的客户端都要提供密码
# 设置哨兵 sentinel 连接主从的密码,注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
# 指定多少毫秒之后,主节点没有应答哨兵 sentinel,此时,哨兵主观上认为主节点下线,默认 30 秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000
# 这个配置项指定了在发生 failover 主备切换时最多可以有多少个 slave 同时对新的 master 进行同步
# 这个数字越小,完成 failover 所需的时间就越长
# 但是如果这个数字越大,就意味着越多的 slave 因为 replication 而不可用
# 可以通过将这个值设为 1 来保证每次只有一个 slave 处于不能处理命令请求的状态
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1
# 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
# 1. 同一个 sentinel 对同一个 master 两次 failover 之间的间隔时间
# 2. 当一个 slave 从一个错误的 master 那里同步数据开始计算时间。直到 slave 被纠正为向正确的 master 那里同步数据时。
# 3. 当想要取消一个正在进行的 failover 所需要的时间。
# 4. 当进行 failover 时,配置所有 slaves 指向新的 master 所需的最大时间。
# 不过,即使过了这个超时,slaves 依然会被正确配置为指向 master,但是就不按 parallel-syncs 所配置的规则来了
# 默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
# 配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
# 对于脚本的运行结果有以下规则:
# 若脚本执行后返回 1,那么该脚本稍后将会被再次执行,重复次数目前默认为 10
# 若脚本执行后返回 2,或者比 2 更高的一个返回值,脚本将不会重复执行。
# 如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为 1 时的行为相同。
# 一个脚本的最大执行时间为 60s,如果超过这个时间,脚本将会被一个 SIGKILL 信号终止,之后重新执行。
# 通知型脚本:当 sentinel 有任何警告级别的事件发生时(比如说 redis 实例的主观失效和客观失效等),将会去调用这个脚本
# 这时这个脚本应该通过邮件,SMS 等方式去通知系统管理员关于系统不正常运行的信息。
# 调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。
# 如果 sentinel.conf 配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则 sentinel 无法正常启动成功。
# 通知脚本
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
# 客户端重新配置主节点参数脚本
# 当一个 master 由于 failover 而发生改变时,这个脚本将会被调用,通知相关的客户端关于 master 地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前 <state> 总是 “failover”,<role> 是 “leader” 或者 “observer” 中的一个。
# 参数 from-ip,from-port,to-ip,to-port是用来和旧的 master 和新的 master (即旧的 slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
13Redis缓存穿透和雪崩**
Redis 缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。
但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。
如果对数据的一致性要求很高,那么就不能使用缓存。
另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。
缓存穿透
这里先介绍下日常使用缓存的逻辑:
查询一个数据,先到缓存中查询。
如果缓存中存在,则返回。
如果缓存中不存在,则到数据库查询。
如果数据库中存在,则返回数据,且存到缓存。
如果数据库中不存在,则返回空值
概念
缓存穿透出现的情况就是数据库和缓存中都没有。
这样缓存就不能拦截,数据库中查不到值也就不能存到缓存。
这样每次这样查询都会到数据库,相当于直达了,即穿透。
这样会给数据库造成很大的压力。
解决方案
布隆过滤器
缓存空对象
缓存击穿
eg微博热点事件时候,大量访问同一key,当redis中的该key失效,大量请求直接访问到mysql服务器,导致微博服务器宕机
概念
缓存击穿,是指一个 key 非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问。
当这个 key 在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
当某个 key 在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据。
由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。
解决方案
设置热点数据永不过期
从缓存层面来看,没有设置过期时间,所以不会出现热点 key 过期后产生的问题。
但是一直不过期就会导致浪费空间
加互斥锁
分布式锁:使用分布式锁(setnx),保证对于每个 key 同时只有一个线程去查询后端服务(mysql),其他线程没有获得分布式锁的权限,因此只能等待。
这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。
缓存雪崩
概念
缓存雪崩,是指在某一个时间段,缓存集中过期失效。
产生雪崩的原因之一,比如马上就要到双十一零点,很快就会迎来一波抢购。
这波商品时间比较集中的放入了缓存,假设缓存一个小时。
那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。
而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。
于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。
其实集中过期,倒不是非常致命。
比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。
因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存。
这个时候,数据库也是可以顶住压力的,无非就是对数据库产生周期性的压力而已。
而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。
解决方案
搭建集群
实现 Redis 的高可用,既然一台服务有可能挂掉,那就多增设几台服务。
这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
限流降级
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。
比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待。
停掉一些服务,以保证主服务可用
eg:双十一零点购买的商品,暂时无法退款
数据预热
数据加热的含义就是在正式部署之前,先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。
在即将发生大并发访问前手动触发加载缓存不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
笔记料来源:
尚硅谷redis入门到精通详细教程
遇见狂神说redis最新超详细版教程