技术分类
1、解决功能性的问题:Java、Jsp、RDBMS、Tomcat、HTML、Linux、JDBC、SVN
以上技术能满足项目的基本功能(CRUD),等这些功能做到一定地步会产生一定的问题:
功能会随着需求变换和升级
2、解决扩展性的问题:Struts、Spring、SpringMVC、Hibernate、Mybatis
用框架来解决扩展性问题,在用框架编写程序时要遵循框架的规范。
3、解决性能的问题:NoSQL、Java线程、Hadoop、Nginx、MQ、ElasticSearch
随着用户量增加,产生性能问题。以上技术来解决性能问题。
Web1.0的时代,数据访问量很有限,用一夫当关的高性能的单点服务器可以解决大部分问题
随着Web2.0的时代的到来,用户访问量大幅度提升,同时产生了大量的用户数据,
加上后来的智能移动设备的普及,。IO量急剧增大,所有的互联网平台都面临了巨大的性能挑战。
解决*,登录信息就被到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操作,它的数据完全存在内存中,读的速度很快
创建缓存数据库:减少IO压力,提高访问速度
NoSQL(NoSQL = *Not Only SQL* ),不仅是SQL,泛指****非关系型的数据库****。
***关系型数据库:***按照业务逻辑存储有关系的数据(Mysql)
NoSQL 不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力。
·不遵循SQL标准。有自己的一套标准
·不支持ACID。 ACID–>事务的四个特性:原子性、一致性、隔离性、持久性
·远超于SQL的性能。 查询效率很高
· 对数据高并发的读写 (高并发的秒杀功能)
· 海量数据的读写
· 对数据高可扩展性的
·需要事务支持
· 基于sql的结构化查询存储,处理复杂的关系,需要即席查询。
· *(用不着sql的和用了sql也不行的情况,请考虑用NoSql)*
好友推荐功能
Ø Redis是一个开源的key-value存储系统。
Ø 和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。
Ø 这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。
Ø 在此基础上,Redis支持各种不同方式的排序。
Ø 与memcached一样,为了保证效率,数据都是缓存在内存中。
Ø 区别的是Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件。
Ø 并且在此基础上实现了master-slave(主从)同步。
高频次,热门访问的数据,降低数据库IO
前台启动,命令行窗口不能关闭,否则服务器停止
ctrl+c退出
[root@thhlinux bin]# ./redis-server /etc/redis.conf
此时把后台关闭了,redis也还在启动
查看redis是否启动:[root@thhlinux bin]# ps -ef | grep redis
测试:
直接shutdown
关闭进程
Redis端口6379的来源:九键6379对应merz
串行:当有1,2,3,三个操作时,要等1完成了才做2操作,2完成了才做3操作。
Redis使用***单线程+多路IO复用(Redis)***技术:
多个用户托黄牛去火车站买票,在黄牛买票时,用户可以做自己的事情,当黄牛买到了对应用户的票时,通知来取票,其他用户继续做自己的事情。
让CPU一直工作而不休息。
redis常见数据类型操作命令
http://www.redis.cn/commands.html
增加key:
set key(需要的key) value(需要的value)
String是Redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value。
String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。
String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M
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
mset …
同时设置一个或多个 key-value对
mget …
同时获取一个或多个 value
msetnx …
同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。
*原子性,有一个失败则都失败*
getrange <起始位置><结束位置>
获得值的范围,类似java中的substring,*前包,后包 []*
setrange <起始位置>
用 覆写所储存的字符串值,从<起始位置>开始(*索引从0开始*)。
*setex <过期时间**>*
设置键值的同时,设置过期时间,单位秒。
getset
以新换旧,设置了新值同时获得旧值。
ttl 查看过期时间
String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。
value是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.
如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。
当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。
单键多值
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
lpush/rpush … 从左边/右边插入一个或多个值。
lpop/rpop 从左边/右边吐出一个值。值在键在,值光键亡。
lrange 按照索引下标获得元素(从左到右)
lrange mylist 0 -1 0左边第一个,-1右边第一个,(0-1表示获取所有)
lindex 按照索引下标获得元素(从左到右)
llen 获得列表长度
linsert before 在的后面插入插入值
lrem 从左边删除n个value(从左到右)
lset 将列表key下标为index的值替换成value
List的数据结构为快速链表quickList。
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。
它将所有的元素紧挨着一起存储,分配的是一块连续的内存。
当数据量比较多的时候才会改成quicklist。
因为普通的链表需要的附加指针空间太大,会比较浪费空间。
比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。
Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以****自动排重****的,
当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择
,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的****复杂度都是O(1)****。
一个算法,随着数据的增加,执行时间的长短,如果是O(1),数据增加,查找数据的时间不变
sadd …
将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略
smembers 取出该集合的所有值。
sismember 判断集合是否为含有该值,有1,没有0
scard返回该集合的元素个数。
srem … 删除集合中的某个元素。
spop *随机从该集合中吐出一个值。*
srandmember 随机从该集合中取出n个值。不会从集合中删除 。
smove value把集合中一个值从一个集合移动到另一个集合
sinter 返回两个集合的交集元素。
sunion 返回两个集合的并集元素。
sdiff 返回两个集合的****差集****元素(key1中的,不包含key2中的)
Set数据结构是dict字典,字典是用哈希表实现的。
Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。
Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。
Redis hash 是一个键值对集合。
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。
类似Java里面的Map
用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储
主要以下两种存储方式:
hset 给集合中的 键赋值
hget 从集合取出 value
hmset … 批量设置hash的值
hexists查看哈希表 key 中,给定域 field 是否存在。
hkeys 列出该hash集合的所有field
hvals 列出该hash集合的所有value
hincrby 为哈希表 key 中的域 field 的值加上增量 1 -1
hsetnx 将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在 .
Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。
Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。
不同之处是有序集合的每个成员都关联了一个****评分(score)****,这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。
因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。
访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。
适合于做一个排行榜。
zadd … 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。
zrange [WITHSCORES] 返回有序集 key 中,下标在 之间的元素
带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开始。
SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map
zset底层使用了两个数据结构
(1)hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。
(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。
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次。
从此可以看出跳跃表比有序链表效率要高
进入配置文件
不区分大小写。开头定义了一些基本的度量单位,只支持bytes,不支持bit
类似jsp中的include,多实例的情况可以把公用的配置文件提取出来
默认情况bind=127.0.0.1只能接受本机的访问请求
不写的情况下,无限制接受任何ip地址的访问
生产环境肯定要写你应用服务器的地址;服务器是需要远程访问的,所以需要将其注释掉
将本机访问保护模式设置no
port 端口号,默认 6379
修改daemonize属性将no 改为yes
注:该属性是将redis后台运行
/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开机启动
timeout 一个空闲的客户端维持多少秒会关闭,0表示关闭该功能。即永不关闭。
tcp-keepalive:对访问客户端的一种心跳检测,每个n秒检测一次。单位为秒,如果设置为0,则不会进行Keepalive检测,建议设置成60
shutdown:关机
是否为后台进程,设置为yes
守护进程,后台启动:
存放pid文件的位置,每个实例会产生一个不同的pid文件:
指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为****notice****
四个级别根据使用阶段来选择,生产环境选择notice 或者warning
设定库的数量 默认16,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id
访问密码的查看、设置和取消
在命令中设置密码,只是临时的。重启redis服务器,密码就还原了。
永久设置,需要再配置文件中进行设置。
打开901行代码 表示redis需要密码 然后在如下设置密码
# 设置能连上 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
Ø 设置redis同时可以与多少个客户端进行连接。
Ø 默认情况下为10000个客户端。
Ø 如果达到了此限制,redis则会拒绝新的连接请求,并且向这些连接请求方发出“max number of clients reached”以作回应。
Ø 建议****必须设置****,否则,将内存占满,造成服务器宕机
Ø 设置redis可以使用的内存量。一旦到达内存使用上限,redis将会试图移除内部数据,移除规则可以通过maxmemory-policy来指定。
Ø 如果redis无法根据移除规则来移除内存中的数据,或者设置了“不允许移除”,那么redis则会针对那些需要申请内存的指令返回错误信息,比如SET、LPUSH等。
Ø 但是对于无内存申请的指令,仍然会正常响应,比如GET等。如果你的redis是主redis(说明你的redis有从redis),那么在设置内存使用上限时,需要在系统中留出一些内存空间给同步队列缓存,只有在你设置的是“不移除”的情况下,才不用考虑这个因素。
Redis 发布订阅 (pub/sub) 是一种消息通信模式:
发送者 (pub) 发送消息,订阅者 (sub) 接收消息。
Redis 客户端可以订阅任意数量的频道。
此时终端A接收到信息
注:发布的消息没有持久化,如果在订阅的客户端收不到hello,只能收到订阅后发布的消息
1.实时聊天(频道当聊天室)
2.消息实时系统
3.订阅、关注等功能
现代计算机用二进制(位) 作为信息的基础单位, 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中叫做偏移量。
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的阻塞。
getbit获取Bitmaps中某个偏移量的值
获取键的第offset位的值(从0开始算)
1访问过,5没有访问过,不存在的也返回0
统计****字符串****被设置为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
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
假设网站有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 |
在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站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。 基数估计就是在误差可接受的范围内,快速计算基数。
Redis 3.2 中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。
通过java来操作redis
Jedis是redis的java版本的客户端实现,使用Jedis提供的Java API对Redis进行操作,是Redis官方推崇的方式;
并且,使用Jedis提供的对Redis的支持也最为灵活、全面;不足之处,就是编码复杂度较高。
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
连接测试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正常连接了
方法几乎和redis命令行中一样
@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();
}
1、输入手机号,点击发送后随机生成6位数字码,2分钟有效
2、输入验证码,点击验证,返回成功或失败
3、每个手机号每天只能输入3次
1.随机生成6位数字验证码
利用java中的Random(随机数)类
2.验证码两分钟有效
把验证码放入redis中,并且设置过期时间为120秒
3.验证功能
判断验证码是否一致,直接用输入的验证码和redis中获取的验证码进行比较即可
4.每个手机号每天只能输入3次
redis中的incr命令 每次发送之后+1
大于2则不能发送了
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;
}
}
SpringBoot 整合 Redis 是使用 SpringData 实现的。
在企业中pojo类都会实现可序列化 实现Serializable
<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模式
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
源码分析
@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 的类型,我们使用需要强制转换,很不方便,我们仓用是
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 的泛型是
我们需要一个泛型为
并且,这个 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) {
// 定义泛型为 的 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;
}
}
直接用 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);
}
}
@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;
}
Redis 事务的本质是一组命令的集合,一个事务中所有的命令都会被序列化,在事务执行的过程中,会照顺序执行
队列 set set set 执行
一次性、顺序性、排他性,执行一些命令。
Redis 事务没有隔离级别的概念
所有的命令在事务中,并没有直接执行,只有发起执行命令的时候才会执行。
Redis单条命令保证原子性(要么同时成功,要么同时失败)的,但是事务不保存原子性。
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。
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"
事务中存在命令性错误
若在事务队列中存在命令性错误(类似于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"
悲观锁,认为什么时候都会出问题(上厕所 锁门),无论什么时候都会上锁
相对效率低下
乐观锁,认为什么时候都不会出问题,所以不会上锁!更新数据的时候才去判断在此期间是否有人修改过数据
获取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) 应答以通知调用者事务执行失败。
@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();//关闭连接
}
测试100个并发 100个请求
Redis 是内存数据库,即数据存储在内存。
如果不将内存中的数据保存到磁盘,一旦服务器进程退出,服务器中的数据也会消失。
这样会造成巨大的损失,所以 Redis 提供了持久化功能。
在指定的时间间隔内将内存中的数据集快照写入磁盘。
也就是 Snapshot 快照,恢复时是将快照文件直接读到内存里。
Redis会单独创建(fork)一个子进程来进行持久化。
会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。
整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。
如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。
RDB 的缺点是最后一次持久化后的数据可能丢失。—默认情况就是RDB不需要设置
rdb保存的文件时 dump.rdb 都是在配置文件中快照进行配置的
修改rdb配置 save 60 5 —用于测试 只要在60秒内修改了5次key 就会触发rdb操作
一开始默认有rdb文件 我们删除后 在数据库中60S内添加5条数据 就又生成了rdb文件
如果该目录下存在dump.rdb文件 启动就会自动恢复其中的数据
1.适合大规模的数据恢复
2.对数据的完整性要求不高
1.需要一定的时间间隔来执行进程操作,如果redis意外宕机了,那么最后一次修改得数据就没了。
2.fork进程的时候 会占用更多的内存空间
将我们所有的命令(写入的操作)都进行记录,恢复的时候就把这个文件全部执行一遍
只许追加文件,但不可以改写文件,Redis 启动之初会读取该文件重新构建数据。
换言之,Redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
默认是不开启的 在配置文件中修改 将no改为yes 来开启
# appendfsync always 每次修改都会sync 消耗性能
appendfsync everysec #每一秒钟修改(代表如果在最后一秒宕机那么最后一秒的数据会丢失)
# appendfsync no 不执行sync 此时操作系统自己同步数据 速度最快
若AOF文件被恶意破坏 那么下次启动redis是无法正常启动的
此时我们可以使用redis的工具 redis-check-aof --fix 来对aof文件进行修复
如果文件正常那么重启就可以正常启动了(但是可能会有容错修复了)
AOF默认采用文件无限追加方式,文件会越来越大,为避免出现此种情况,新增了重写机制。
当AOF文件的大小超过所设定的阈值时,Redis 就会启动 AOF 文件的内容压缩。
只保留可以恢复数据的最小指令集,可以使用命令 bgrewriteaof
。
重写原理:
AOF 文件持续增长而过大时,会 Fork 出一条新进程来将文件重写(也是先写临时文件最后再 rename)。
遍历新进程的内存中数据,每条记录有一条的 set 语句。
重写 aof 文件的操作,并没有读取旧的 aof 文件,这点和快照有点类似。
重写触发机制:
Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次 rewrite 后大小的 1 倍且文件大于 64M 时触发。
1.每一次修改都同步(让文件的完整性更好)
2.每秒都同步 可能丢失最后一秒的数据
3.从不同步 效率高
1.相对数据文件 aof文件大于rdb 修复速度也更慢
2.aof(读写的操作)运行效率也比rdb更慢,所以默认为rdb
扩展:
①主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器
前者称为主节点(master / leader),后者称为从节点(slave / follower)。
②主从复制读写分离 大部分情况都是在读操作(把读操作的所有压力转移到从机上,来提升主机写的效率)
数据的复制是单向的,只能由主节点到从节点。
Master 以写为主,Slave 以读为主。
③一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
④默认情况下,每台 Redis 服务器都是主节点。
⑤若要配一个redis集群,最低也是一主二从
数据冗余
主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
故障恢复
当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复。这也是一种服务的冗余。
负载均衡
在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务,分担服务器负载。
尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
高可用(集群)
主从复制是哨兵和集群能够实施的基础,因此说主从复制是 Redis 高可用的基础。
一般来说,要将 Redis 运用于工程项目中,只使用一台 Redis 是万万不能的(redis可能会宕机),原因如下:
应用
电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是多读少写。
对于这种场景,我们可以使用如下这种架构:
[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>
①修改端口号
②修改后台运行
③修改日志文件名字
④修改dump文件的名字
修改完毕后 启动4个redis服务器
此时还没有配置时 每个服务器都是主节点 ----默认情况下,每台 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 #打开注解 并且加上主机的地址
# 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 #打开注解 加上主机的密码
从机也可以被其他从机当作主机,可以有效减轻主机的写压力。
eg让6380作为6381的主机
此时6380依然是6379的从机
这样 6379 赋的值只需要复制到 6380,6380 再复制到 6381,这样就有效的减轻主机的写压力。
如果主机(6379)宕机了 需要手动选出新的主机
主机宕机后一个从机使用slaveof no one命令 该从机就变成主机了
其他从机就以该新主机为主机了
当老主机恢复后,其他从机也还是以该新主机为主机(所以要重新连接旧主机为新主机)
从机中如果要写,就会报错
在主机恢复后,从机依旧可以直接获取到主机写入的信息
①若从机是通过命令行配置的从机 在宕机后重新启动默认就是主机了 就无法再读取到主机写入的信息
此时再次通过成为主机的从机后,就能再次读取到主机写入的信息
Slave启动成功连接到master后会发送一个sync同步命令
Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,maste将传送
整个数据文件到slave,并完成一次完全同步。
全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步
但是只要是重新连接master ,一次完全同步(全量复制)将被自动执行
主从切换技术的操作是:当主机宕机后,需要手动把一台从机切换为主机。
这就需要人工干预,费事费力,还会造成一段时间内服务不可用。
这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。
Redis 从 2.8 开始正式提供了 Sentinel(哨兵) 架构来解决这个问题。
它是“谋朝篡位”的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从机转换为主机。
哨兵模式是一种特殊的模式,首先 Redis 提供了哨兵的命令,哨兵是**一个独立的进程,**它会独立运行。
其原理是哨兵通过发送命令,等待 Redis 服务器响应,从而监控运行的多个 Redis 实例。
然而一个哨兵进程对 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
sentinel monitor mymaster 127.0.0.1 6379 2
# 当在 Redis 实例中开启了 requirepass foobared 授权密码 这样所有连接 Redis 实例的客户端都要提供密码
# 设置哨兵 sentinel 连接主从的密码,注意必须为主从设置一样的验证密码
# sentinel auth-pass
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
# 指定多少毫秒之后,主节点没有应答哨兵 sentinel,此时,哨兵主观上认为主节点下线,默认 30 秒
# sentinel down-after-milliseconds
sentinel down-after-milliseconds mymaster 30000
# 这个配置项指定了在发生 failover 主备切换时最多可以有多少个 slave 同时对新的 master 进行同步
# 这个数字越小,完成 failover 所需的时间就越长
# 但是如果这个数字越大,就意味着越多的 slave 因为 replication 而不可用
# 可以通过将这个值设为 1 来保证每次只有一个 slave 处于不能处理命令请求的状态
# sentinel parallel-syncs
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
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
# 配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
# 对于脚本的运行结果有以下规则:
# 若脚本执行后返回 1,那么该脚本稍后将会被再次执行,重复次数目前默认为 10
# 若脚本执行后返回 2,或者比 2 更高的一个返回值,脚本将不会重复执行。
# 如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为 1 时的行为相同。
# 一个脚本的最大执行时间为 60s,如果超过这个时间,脚本将会被一个 SIGKILL 信号终止,之后重新执行。
# 通知型脚本:当 sentinel 有任何警告级别的事件发生时(比如说 redis 实例的主观失效和客观失效等),将会去调用这个脚本
# 这时这个脚本应该通过邮件,SMS 等方式去通知系统管理员关于系统不正常运行的信息。
# 调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。
# 如果 sentinel.conf 配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则 sentinel 无法正常启动成功。
# 通知脚本
# sentinel notification-script
sentinel notification-script mymaster /var/redis/notify.sh
# 客户端重新配置主节点参数脚本
# 当一个 master 由于 failover 而发生改变时,这个脚本将会被调用,通知相关的客户端关于 master 地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
#
# 目前 总是 “failover”, 是 “leader” 或者 “observer” 中的一个。
# 参数 from-ip,from-port,to-ip,to-port是用来和旧的 master 和新的 master (即旧的 slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
Redis 缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。
但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。
如果对数据的一致性要求很高,那么就不能使用缓存。
另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。
这里先介绍下日常使用缓存的逻辑:
查询一个数据,先到缓存中查询。
如果缓存中存在,则返回。
如果缓存中不存在,则到数据库查询。
如果数据库中存在,则返回数据,且存到缓存。
如果数据库中不存在,则返回空值
缓存穿透出现的情况就是数据库和缓存中都没有。
这样缓存就不能拦截,数据库中查不到值也就不能存到缓存。
这样每次这样查询都会到数据库,相当于直达了,即穿透。
这样会给数据库造成很大的压力。
布隆过滤器
缓存空对象
eg微博热点事件时候,大量访问同一key,当redis中的该key失效,大量请求直接访问到mysql服务器,导致微博服务器宕机
缓存击穿,是指一个 key 非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问。
当这个 key 在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
当某个 key 在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据。
由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。
设置热点数据永不过期
从缓存层面来看,没有设置过期时间,所以不会出现热点 key 过期后产生的问题。
但是一直不过期就会导致浪费空间
加互斥锁
分布式锁:使用分布式锁(setnx),保证对于每个 key 同时只有一个线程去查询后端服务(mysql),其他线程没有获得分布式锁的权限,因此只能等待。
这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。
缓存雪崩,是指在某一个时间段,缓存集中过期失效。
产生雪崩的原因之一,比如马上就要到双十一零点,很快就会迎来一波抢购。
这波商品时间比较集中的放入了缓存,假设缓存一个小时。
那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。
而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。
于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。
其实集中过期,倒不是非常致命。
比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。
因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存。
这个时候,数据库也是可以顶住压力的,无非就是对数据库产生周期性的压力而已。
而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。
搭建集群
实现 Redis 的高可用,既然一台服务有可能挂掉,那就多增设几台服务。
这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
限流降级
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。
比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待。
停掉一些服务,以保证主服务可用
eg:双十一零点购买的商品,暂时无法退款
数据预热
数据加热的含义就是在正式部署之前,先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。
在即将发生大并发访问前手动触发加载缓存不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
笔记料来源:
尚硅谷redis入门到精通详细教程
遇见狂神说redis最新超详细版教程