初识NOSQL:
NOSQL是为了解决性能问题而产生的技术,在最初,我们都是使用单体服务器架构,如下所示:
随着用户访问量大幅度提升
,同时产生了大量的用户数据
,单体服务器架构面对着巨大的压力
NOSQL解决CPU及其内存压力:
NOSQL解决IO压力:
NOSQL(NOSQL=Not Only SQL),意即"不仅仅是SQL",泛指非关系型数据库,NOSQL不依赖业务逻辑方式存储,而以简单的key-value模式
存储,因此大大的增加了数据库的拓展能力。
不遵循SQL标准
不支持ACID,并不是不支持事务
远超于SQL的性能
NOSQL适用于对数据高并发和海量的读写,以及对数据可扩展性有需求的,它不适用于需要事务支持,或者是基于sql的结构化查询存储,处理复杂的关系的场景
常见的NOSQL数据库:
Memcache
它是很早之前就出现的NOSQL数据库,其数据都在内存中,一般不支持持久化,支持简单的key-value模式,支持类型单一,一般是作为缓存数据库辅助持久化数据库
。
Redis
几乎覆盖了Memcache的绝大部分功能,其数据也是存储在内存中,但它支持持久化,主要用作备份恢复
,除了支持简单的key-value模式,还支持多种数据结构的存储,比如,List,set,hash,zset
等,一般是作为缓存数据库辅助持久化的数据库。
MongoDB
高性能,开源,模式自由的文档型数据库,其数据也是在内存中,如果内存不足,那么会将不常用的数据保存到硬盘,虽然都是key-value模式,但是对value(尤其是json类型)提供了丰富的查询功能。它支持二进制数据以及大型对象,可以根据数据的特点代替RDBMS,称为独立的数据库,或者配合RDBMS,存储特定的数据。
行式数据库:
列式数据库:
非关系型数据库的目的都是为了打破传统关系型数据库以业务逻辑为依据的存储模式,而针对不同数据结构类型改为以性能为最优先的存储方式。
多样的数据存储持久化数据:
我们可通过redis数据库中的zset(有序集合)实现排行榜功能,使用expire实现手机验证码,也可以通过incr,decr实现计数器秒杀功能等。
redis默认有16个数据库,类似于数组下标从0开始,初识默认使用0号库
使用命令select 可以切换数据库,例如下所示:
dbsize:查看当前数据库的key的数量
flushdb:清空当前库
flushall:通杀全部库
Redis是单线程还是多线程?
Redis是单线程+多路IO复用技术,但是单线程不意味着整个Redis就一个线程,Redis其他模块还有各自的线程
。因为Redis是一个基于内存的数据库,将所有的数据放入内存,所以使用单线程的操作效率是最高的,多线程会上下文切换消耗大量资源,因此对于内存系统来说,单线程才能产生更高的效率。
怎么理解IO复用技术呢?比如我们生活中去火车站买票例子,假设现在有三个人去火车站买票,那么在这个场景中,有三个角色,火车站,顾客,以及我们生活中最常见的黄牛,顾客可以将目的地告诉黄牛,让黄牛帮顾客去买票,那么黄牛和火车站时间之间单线程
,黄牛和多个顾客之间体现的是多路复用,当黄牛买到票之后,只需要叫对应的顾客来取票就可以,没有叫到的顾客在此期间可以继续干自己的事情,而不是停止下来等票。
Redis是单线程为什么还这么快?
1、从数据存储的角度分析:
Redis基于内存,数据存在内存中,绝大部分请求是内存操作,非常快速,跟传统的磁盘文件数据存储相比,避免了通过磁盘IO读取到内存这部分的开销
2、从单线程架构的角度分析:
在redis中,命令执行是单线程操作,没有上下文切换的时间以及CPU消耗,不存在竞争条件,不用去考虑各种锁的问题,也不存在加锁释放锁操作,更也不会出现死锁而导致的性能消耗,且能够使用各种“线程不安全”命令,例如 Lpush。
3.基于线程模型的角度分析
使用基于网络 I/O多路复用机制(非阻塞IO)的线程模型,可以处理并发的链接,缓解网络 I/O 速度慢的问题,“多路”指的是多个网络连接,“复用”指的是复用同一个线程
。采用多路 I/O 复用技术可以让单个线程高效的处理多个客户端的网络IO连接请求的同时,减少了网络 IO 的时间消耗
4.从数据结构的角度分析
Redis 一共有 5 种数据类型,String、List、Hash、Set、SortedSet。不同的数据类型底层使用了一种或者多种数据结构来支撑,目的就是为了追求更快的速度。
Redis6.0 之前为什么一直不使用多线程?
Redis使用单线程的可维护性高
。多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换
、甚至加锁解锁、死锁造成的性能损耗
。
Redis6.0 为什么要引入多线程呢?
因为Redis的瓶颈不在内存
,而是在网络I/O模块带来CPU的耗时,所以Redis6.0的多线程是用来处理网络I/O这部分,充分利用CPU资源,减少网络I/O阻塞带来的性能损耗。
Redis6.0引入多线程模式,是否存在线程并发安全问题?
不存在并发安全问题,原因是Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行,一次redis请求,要建立连接,然后获取操作的命令,再执行命令,最后将响应的结果写到socket上,在redis的多线程模式下,接收,发送和解析命令可以配置成多线程执行的,因为它毕竟是主要耗时点,但是命令的执行,也就是内存操作,依然是单线程运行的。
keys *:查看当前所有库的key
exists key:判断某个key是否存在
type key:查看该key是什么类型
del key:删除指定的key数据
unlink key:非阻塞删除
del和unlink的区别
:unlink虽然执行该命令后显示给我们的是删除了,但实际上并没有真正的删除,真正的删除会在该命令执行后才慢慢执行
。
expire key 10 :为指定的key设置过期时间
超过时间后会自动删除key,但是不一定是立即删除,因为redis的过期策略是惰性删除和定期删除的策略
ttl key:查看还有多少秒过期
举例:
String:
String是Redis最基本的类型,一个 key对应一个 value,其value的最大值为512M。
String 类型是二进制安全的。意味着 Redis 的 string可以包含任何数据。比如jpg图片或者序列化的对象。
(二进制安全是指,在传输数据时,保证二进制数据的信息安全,也就是不被篡改、破译等,如果被攻击,能够及时检测出来。)
set key value:添加键值对
NX用法
:
XX用法
:
EX用法
:
PX用法
:
get key:查询该key对应的value
append key value:将该value添加到该key对应的原值的末尾
strlen key:获得该key对应的value的长度
setnx key value:只有该key不存在时,才能将其值设置为value
incr key:将key对应的值+1,该值必须为数字,且可为空值
incrby key <num>:将可以对应的值+num
decr key:将key对应的value-1,该值必须为数字,可为空值
decrby key <num>:将可以对应的值-num
mset key1 value1,key2 value2....:同时设置一个或多个key-value
mget key1,key2,key3....:同时获取一个或多个value
```sql
msetnx key1 value1 key2 value2...:同时设置一个或多个key-value对,当且仅当所有给定的key都不存在才可以
//该操作为原子性操作,有一个失败则所有都失败
getrange key <开始> <结束>:类似于Java中的substring,前后都包含
setrange key <起始位置> value:用value覆写key对应的value,从起始位置开始
setex key 过期时间:设置key的过期时间,单位为秒
getset key value:将key的值设置为value,并且返回key之前的值
数据结构:
String的数据结构为简单动态字符串(Simple Dynamic String,缩写 SDS),是可修改的字符串,内部结构实现上类似于 Java的 ArrayList
,采用预分配冗余空间的方来减少内存的频繁分配
如图中所示,内部为当前字符串实际分配的空间 capacity ,一般要高于实际字符串长度len。当字符串长度小于 1M时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M的空间
。需要注意的是字符串最大长度为 512M。
list:
单键多值:
Redis 列表是简单的字符串列表,按照插入顺序排序。添加一个元素到列表的头部(左边)或者尾部(右边)。它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差
。
lpush key value1 value2 value3:从左边插入一个或多个值
rpush key value1 value2 value3:从右边插入一个或多个值
对比如下所示:
rpoplpush key1 key2
lrange key <start> <stop>: 按照索引下标获得元素,从左到右
lindex key num:获取下标为num的元素
llen key:获取列表的长度
linsert key before value new_value:在value的后面插入new_value
lrem key n value:从左开始到右边删除n个value
lset key index value:将列表key中下标为index的元素替换成value
List 的数据结构为快速链表 quickList。首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是 ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存
。当数据量比较多的时候才会改成 quicklist
。因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是 int 类型的数据,结构上还需要两个额外的指针 prev 和 next。
Redis 将链表和 ziplist 结合起来组成了 quicklist。也就是将多个 ziplist使用双向指针串起来使用
。这样既满足了快速的插入删除性能,又不会出现太大的空间冗象。
Set:
set对外提供的功能与list类似是一个列表的功能,特殊之处在于 set 是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口
,这个也是 list所不能提供的。Redis 的 Set 是 string类型的无序集合。它底层其实是一个 value为 null的 hash表
,所以添加,删除,查找的复杂度都是 0(1)
**无论数据的规模如何增加,算法的执行时间都保持不变。这种算法的时间复杂度为O(1),也称为常数时间复杂度
。**例如,Java中的查找HashMap中的元素,HashMap使用哈希表来存储数据,通过键值对的方式进行数据的存储和查找。无论HashMap中存储了多少元素,查找某个元素的时间都是恒定的,即O(1)。
sadd:将一个或多个member元素加入到集合key中,已经存在的member元素将被忽略
sadd key value1 value2 value3....
smembers:取出该集合的所有值
smembers key
sismember:判断集合key是否含有该value值,有则返回1,否则返回0
sismember key value
scard:返回该集合的元素个数
scard key
srem:删除集合中的某个元素
srem key value1 value2....
spop:随机从该集合中吐出一个值
spop key
srandmember:随机从该集合中取出n个值,不会从集合中删除
srandmember key n
Set数据结构是dict字典,字典使用哈希表实现的
Java 中 Hashset 的内部实现使用的是 HashMap,只不过所有的 value 都指向同一个对象
。Redis 的 set 结构也是一样,它的内部也使用 hash 结构,所有的 value 都指向同一个内部值。
Hash:
Redis hash 是一个 string类型的 field 和 value 的映射表,hash 特别适合用于存储对象,类似 Java 里面的 Map<String,Object>
。
第一种存储方式:
第二种存储方式:
第三种存储方式:
hset key field value:给key集合中的field设置值为value
hget key field:取出key集合中的field的值
hmset key field1 value1 field2 value2....:批量设置hash的值
hexists key field:查看哈希表key中的field是否存在
hkeys key:列出该hash集合中的所有field
hvals key:列出该hash集合中的所有value
hincrby key field num:为key中的域field的值设置增量+num/减量-num
hsetnx key field value:将哈希表key中的域的field的值设置为value,当且仅当域field不存在
Hash 类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value 长度较短且个数较少时,使用 ziplist,否则使用 hashtable。
zset:
Redis 有序集合 zset 与普通集合 set 非常相似,是一个没有重复元素的字符串集合。
不同之处是有序集合的每个成员都关联了一个评分(score)
,这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员
。集合的成员是唯一的,但是评分可以是重复了
因为元素是有序的,所以可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。
访问有序集合的中间元素也是非常快的,因此能够使用有序集合作为一个没有重复成员的智能列表。
zadd key score1 value1 score2 value2....:将一个或多个member元素及其score值加入到有序集合key当中
zrange key start stop [withscores]:返回有序集key中,下标在start,stop之间的元素,withscores是可选项
zrangebyscore key min max:返回有序集中介于min和max之间的元素(从小到大排序)
zrevrangebyscore key max min:返回有序集中介于max和min之间的元素(从大到小排序)
zincrby key num value:为value的score增加num
zrem key value:删除key中的value
zcount key min max:统计key中范围在min~max之间的元素个数
zrank key value:返回value在key中的排名,排名从0开始
SortedSet:
SortedSet(zset)是 Redis 提供的一个非常特别的数据结构,一方面它等价于Java的数据结构 Map<String,Double>
,可以给每一个元素 value 赋予一个权重 score,另一方面它又类似于 TreeSet
,内部的元素会按照权重 score 进行排序,可以得到每个元素的名次,还可以通过 score 的范围来获取元素的列表。
zset 底层使用了两个数据结构
(1)hash,hash的作用就是关联元素
value 和权重 score ,保障元素 value 的唯一性,可以通过元素 value 找到相应的 seore 值
。
(2)跳跃表,跳跃表的目的在于给元素 value 排序
,根据 score 的范围获取元素列表。
跳跃表:
有序集合在生活中比较常见,例如根据成绩对学生排名,根据得分对玩家排名等。对于有序集合的底层实现,可以用数组、平衡树、链表等。数组不便元素的插入删除;平衡树或红黑树虽然效率高但结构复杂;链表查询需要遍历所有,效率低。Redis采用的是跳跃表。跳跃表效率堪比红黑树,但是实现远比红黑树简单。
实例:
对比有序链表和跳跃表,从链表中查询出 51
有序链表:
要查找值为51的元素,需要从第一个元素开始依次查找,比较才能找到,共需要查找6次
从第 2 层开始,1 节点比 51 节点小,向后比较,21 节点比 51 节点小,继续向后比较,后面就是 NULL 了,所以从 21 节点向下到第1层。在第1层,41 节点比 51 节点小,继续向后,61 节点比 51 节点大,所以从 41 向下在第0层,51 节点为要查找的节点,节点被找到,共查找4次。
Redis 的发布和订阅:
什么是发布和订阅?
Redis 发布订阅(pub/sub)是一种消息通信模式
:发送者(pub)发送消息,订阅者(sub)接收消息。Redis 客户端可以订阅任意数量的频道。
发布订阅命令行实现:
1.打开一个客户端channel1
subscribe channel1
2.打开另一个客户端,给channel1发布消息hello
publish channel1 hello
注:发布的消息没有持久化,只能收到订阅后发布的消息,而不能收到订阅之前的消息。
Bitmaps:Redis的新数据类型
现代计算机用二进制(位)作为信息的基础单位,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开始
setbit<key><offset><value>
每个独立用户是否访问过网站存放在 Bitmaps 中,将访问的用户记做 1,没有访问的用户记做0,用偏移量作为用户的id。
设置键的第 offset个位的值(从0算起),假设现在有 20 个用户,userid=16,11,15 ,19 的用户对网站进行了访问,那么当前 Bitmaps 初始化结果如下图所示:
unique:users:20201106 代表2020-11-06 这天的独立访间用户的Bitmaps
很多应用的用户 id 以一个指定数字(例如 10000 )开头,直接将用户 id 和Bitmaps 的偏移量对应势必会造成一定的浪费,通常的做法是每次做 setbit 操作时将用户 id 减去这个指定数字。
在第一次初始化 Bitmaps 时,假如偏移量非常大,那么整个初始化过程执行会比较慢 ,可能会造成 Redis 的阻塞。
getbit:获取 Bitmaps 中某个偏移量的值
//获取键的第offset位的值(从0开始算)
getbit key offset:获取Bitmaps中某个偏移量的值。
实例:
获取 id=offset 的用户是否在 2020-11-06 这天访问过 ,返回0说明没有访间过,返回1说明访问过
bitcount:统计字符串被设置为1的 bit 数。一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start 或 end 参数,可以让计数只在特定的位上进行。start 和 end 参数的设置,都可以使用负数值:比如 -1 表示最后一个位,而 -2 表示倒数第二个位,start、end 是指 bit 组的字节的下标数,二者皆包含
bitcount key [start,end]:统计字符串从start字节到end字节比特值为1的数量
实例:计算2022-11-06这天的独立访问用户数量,如下所示如果不指定start和end,那么则返回所有的
start和end代表起始和结束字节数:
举例:K1 【01000001 01000000 00000000 00100001】,对【0,1,2,3】
bitcount K112 :统计下标 1[ 2字节组中 bit=1的个数,即 01000000
00000000.–》bitcountK112–》1bitcount K1 13 : 统计下标 1、2 字节组中 bit=1的个数,即 01000000
0000000(00100001.–》bitcount K1 1 3-》3bitcount K10-2 :统计下标 0 到下标倒数第 2,字节组中 bit=1 的个数,即01000001
0100000000000000.–》bitcount K1 0 -2–》3
注意:redis 的 setbit 设置或清除的是 bit 位置,而 bitcount 计算的是byte的位置
bitop and/or/not/xor <destkey> [key...]
bitop是一个复合操作 ,它可以做多个 Bitmaps的and(集)、or(并集)、 not(非)、 xor(异或)操作并将结果保存在 destkey 中
Bitmaps与set对比:
假设网站有1亿用户 ,每天独立访问的用户有5 千万,如果每天用集合类型和Bitmaps 分别存储活跃用户可以得到表
很明显 ,这种情况下使用 Bitmaps 能节省很多的内存空间,尤其是随着时间推移节省的内存还是非常可观的。
但 Bitmaps 并不是万金油 ,假如该网站每天的独立访问用户很少 ,例如只有 10万(大量的僵尸用户),那么两者的对比如下表所示,很显然,这时候使用Bitmaps 就不太合适了 ,因为基本上大部分位都是 0。
HyperLogLog:
在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站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。
基数估计就是在误差可接受的范围内,快速计算基数。
pfadd key element1,element2,element3...:将一个或多个元素添加到HyperLogLog中
将所有元素添加到指定 HyperLogLog 数据结构中。如果执行命令后 HLL 估计的近似基数发生变化,则返回1,否则返回 0。
pfcount key: 计算hll的近似基数,可以计算多个hll(将多个的计算结果合并后返回)
pfmerge 目的hll 源hll1 源hll2....:将一个或多个hll合并后的结果存储在目的hll中
GEO:
Redis 3.2 中增加了对 GEO 类型的支持。GEO,Geographic,地理信息的缩写该类型,就是元素的 2维坐标,在地图上就是经纬度。redis 基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度 Hash 等常见操作。
geoadd <key> <longitude> <latitude> <member>
两极无法直接添加,一般会下载城市数据,直接通过 Java 程序一次性导入。
有效的经度从 -180 度到 180 度
。有效的纬度从 -85.05112878 度到85.05112878 度。
当坐标位置超出指定范围时,该命令将会返回一个错误,已经添加的数据,是无法再次往里面添加的
geopos key member:获得key中member的坐标值
geodist key member1 member2:获得key中member1和member2位置之间的直线距离
如果用户没有显式地指定单位参数,那么GETODIST默认使用米作为单位
//以给定的经纬度为中心找出某一半径内的元素
georadius <key> <longitude> <latitude> radius m|km|ft|mi
springboot中使用redis:
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
第一步:关闭防火墙,否则会导致连接失败
//查看防火墙的状态
systemctl status firewalld
//关闭防火墙
systemctl stop firewalld
使用Java对redis进行基本操作:
@Test
public void testString(){
Jedis jedis=new Jedis("198.168.88.129",6379);
//向redis数据库中添加单个键值对
jedis.set("name","yyqx");
//通过键获取对应的值
String name=jedis.get("name");
System.out.println(name);
//向redis数据库中添加多个键值对
jedis.mset("name","wjr","age","19","gender","1");
List<String> stringList=jedis.mget("name","age","gender")
System.out.println(stringList);
//将当前redis数据库中的所有值输出
Set<String> stringSet=jedis.keys("*");
for (String keys:stringSet) {
System.out.println(keys);
}
}
输出如下所示:
yyqx
[wjr, 19, 1]
name
gender
age
使用Java对redis中的List进行操作:
@Test
public void testList(){
Jedis jedis=new Jedis("198.168.88.129",6379);
jedis.lpush("k1","yyqx","wjk","wy");
List<String> stringList= jedis.lrange("k1",0,-1);
for (String list:stringList) {
System.out.println(list);
}
}
输出如下所示:
wy
wjk
yyqx
使用Java对redis中的Set进行操作:
@Test
public void testSet(){
Jedis jedis=new Jedis("198.168.88.129",6379);
//向set中添加元素
jedis.sadd("animal","dog","cat","panda");
Set<String> stringSet=jedis.smembers("animal");
System.out.println(stringSet);
}
输出如下所示:
[dog, panda, cat]
使用Java对redis中的Set进行操作:
@Test
public void testHash(){
Jedis jedis=new Jedis("198.168.88.129",6379);
jedis.hset("user","age","20");
String string=jedis.hget("user","age");
System.out.println(string);
}
输出如下所示:
20
使用Java对redis中的ZSet进行操作:
@Test
public void testZset() {
Jedis jedis=new Jedis("198.168.88.129",6379);
jedis.zadd("china",100d,"xian");
Set<String> stringSet= jedis.zrange("china",0,-1);
System.out.println(stringSet);
}
输出如下所示:
[xian]
1:生成6位的随机验证码
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 void verifyCode(String phone){
//连接redis
Jedis jedis=new Jedis("198.168.88.129",6379);
//拼接key
//手机发送次数key
String countKey="VerifyCode"+phone+":count";
//验证码key
String codeKey="VerifyCode"+phone+":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(countKey);
}else if (Integer.parseInt(count)>2){
//发送了三次,不能再发送
System.out.println("今天的发送次数已达上限");
jedis.close();
}//发送验证码到redis里面
String vcode=getCode();
jedis.setex(codeKey, 120,vcode);//过期时间
jedis.close();
}
3:做验证码的校验功能
public static void getRedisCode(String phone,String code){
//从redis中获取验证码
Jedis jedis=new Jedis("198.168.88.129",6379);
//验证码key
String codeKey="VerifyCode"+phone+":code";
String redisCode=jedis.get(codeKey);
//判断
if (redisCode.equals(code)){
System.out.println("成功");
}else {
System.out.println("失败");
}
jedis.close();
}
public static void main(String[] args) {
//模拟验证码的发送
verifyCode("15347896541");
//模拟校验
getRedisCode("15347896541","111");
}
springboot集成redis:
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
创建springboot启动类:
package com.wjr.redis;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RedisApplication {
public static void main(String[] args) {
SpringApplication.run(RedisApplication.class,args);
}
}
创建redis的配置类:
package com.wjr.redis.Config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurationSelector;
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 java.time.Duration;
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurationSelector {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
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);
template.setConnectionFactory(factory);
//设置“key"的序列化方式
template.setKeySerializer(new StringRedisSerializer());
//设置“值”的序列化方式
template.setValueSerializer(jackson2JsonRedisSerializer);
//设置“hash”类型数据的序列化方式
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
StringRedisSerializer 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))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
application.yaml
spring:
redis:
database: 1 # Redis数据库索引(默认为0)
host: 你的Linux主机IP地址 # Redis服务器地址
port: 6379 # Redis服务器连接端口
lettuce:
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制) 默认 8
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
max-idle: 8 # 连接池中的最大空闲连接 默认 8
min-idle: 0 # 连接池中的最小空闲连接 默认 0
connect-timeout: 30000 #连接超时时间(毫秒)
控制层:
package com.wjr.redis.Controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping
public String testRedis(){
//设置值到redis
redisTemplate.opsForValue().set("name","lucy");
//获取redis中的值
String name= (String) redisTemplate.opsForValue().get("name");
return name;
}
}
访问如下所示:
事务与锁机制:
Redis 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis 事务的主要作用就是串联多个命令防止别的命令插队
从输入 Multi 命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis 会将之前的命令队列中的命令依次执行。
组队的过程中可以通过 discard 来放弃组队
。
multi:开启事务
命令1:
命令2:
命令3:
.....
将若干条命令放入队列中,等待执行
exec:执行队列中的命令
注意:exec命令执行完毕之后就代表事务已经结束(类似于MySQL中的commit命令),那么我们如果需要重新进行事务处理,需要重新开启事务
discard:表示放弃执行队列中等待的命令
组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会被执行,而不会回滚
解决事务的冲突问题:
想想一个场景:有很多人有你的账户,同时去参加双十一抢购,
一个请求想给金额减 8000
一个请求想给金额减 5000
一个请求想给金额减 1000
悲观锁:
悲观锁顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁
,表锁
等,读锁
,写锁
等,都是在做操作之前先上锁。
乐观锁:
乐观锁,顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis 就是利用这种 check-and-set机制实现事务的。
WATCH key [key…]
在执行 multi 之前,先执行 watch key1 [key2],可以监视一个(或多个) key,如果在事务执行之前这个(或这些)key 被其他命令所改动,那么事务将被打断
我们打开两个redis窗口,这两个窗口同时对balance进行操作,窗口1:对balance进行+10操作,窗口2:对balance进行+20操作,窗口1先执行事务,而窗口2后执行,如下所示:
窗口1:
窗口2:
由于balance在此之前已经被窗口1打断了,因此此次事务操作也会被打断
unwatch:
取消 WATCH命令对所有 key 的监视。如果在执行 WATCH 命令之后,EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。
Redis事务三大特性:
单独的隔离操作:
事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
没有隔离级别的概念:
队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令者不会被实际执行。
不保证原子性:
事务中如果有一条命令执行失败,其后的命令仍然会被执,没有回滚
Redis的主从复制:
主从复制是指主机数据更新后根据配置和策略自动同步到备机的 master/slaver机制,Master以写为主
,Slave 以读为主
主从复制的功能:
读写分离
,性能扩展
,容灾快速恢复
新建 redis6379.conf,填写以下内容
include /myredis/redis.conf
pidfile /var/run/redis_6379.pid
port 6379
dbfilename dump6379.rdb
配从(库)不配主库
slaveof <ip><port>
成为某个实例的从服务器
在 6380和 6381 上执行: slaveof 127.0.0.1 6379
一主二仆:
切入点问题?slave1、slave2 是从头开始复制还是从切入点开始复制?比如从 k4 进来那之前的 k1.k2.k3 是否也可以复制 ?
从机是否可以写?set可否?
主机 shutdown 后情况如何?从机是上位还是原地待命?
将从服务器挂掉之后重启:
1:它变为主服务器
2:再次变为从服务器,那么它会将主服务器中的所有数据复制过来
将主服务器挂掉:
1:从服务器并不会改变它的角色,依然为从服务器,只是通过info replication能够查看到其主服务器的信息有所改变
2:重新连接挂掉的主机,那么从机的信息可以立马被获取到
全量复制:而 slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中
增量复制:Master 继续将新的所有收集到的修改命令依次传给 slave,完成同步
但是只要是重新连接 master,一次完全同步(全量复制)将被自动执行
我们可以将6381设置为6380的主服务器,那么6381承担的角色为6379的从服务器,6380的主服务器,那么6379的从服务器数量需要减少为1,其实这个过程就是实现了上述图示中的过程,它和一主二从的过程很相似
哨兵模式:
反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
自定义的/myredis 目录下新建sentinel.conf 文件,名字绝不能错
配置哨兵:
sentinel monitor mymaster 127.0.0.1 6379 1
其中 mymaster 为监控对象起的服务器名称,1为至少有多少个哨兵同意迁移的数量。
启动哨兵:
当6379后面的值设置为2,那么则表示必须两个都同意才能进行切换
/usr/local/bin
redis 做压测可以用自带的 redis-benchmark 工具
当主机挂掉,从机选举中产生新的主机
(大概 10 秒左右可以看到哨兵窗口日志,切换了新的主机)。哪个从机会被选举为主机呢?
根据优先级别:slave-priority,原主机重启后会变为从机。
复制延时:
由于所有的写操作都是先在 Master 上操作,然后同步更新到 slave 上,所以从 Master同步到 slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,slave机器数量的增加也会使这个问题更加严重。
优先级在 redis.conf中默认:slave-priority 100、值越小优先级越高
偏移量是指获得原主机数据最全的
每个 redis 实例启动后都会随机生成一个 40 位的 runid
Redis 集群:
容量不够,redis 如何进行扩容?并发写操作,redis 如何分摊?
另外,主从模式,薪火相传模式,主机宕机,导致ip 地址发生变化,应用程序中配置需要修改对应的主机地址、端口等信息。
之前通过代理主机来解决,但是 redis3.0 中提供了解决方案。就是无中心化集群配置。
Redis 集群实现了对 Redis 的水平扩容,即启动N个redis 节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的 1/N
。”
Redis 集群通过分区(partition)来提供一定程度的可用性(availability):即使集群中有一部分节点失效或者无法进行通讯,集群也可以继续处理命令请求.
删除持久化数据:
将 rdb,aof文件都删除掉。
制作6个实例,6379,6380,6381,6389,6390,6391
配置基本信息:
开启 daemonize yes
Pid 文件名字
指定端口
Log 文件名字
redis cluster配置修改
cluster-enabled yes打开集群模式
cluster-config-file nodes-6379.conf 设定节点配置文件名
cluster-node-timeout 15000 设定节点失联时间,超过该时间(毫秒),集群自动进cluster-node-timeout 15000行主从切换。
include /home/bigdata/redis.conf
port 6379
pidfile "/var/run/redis 6379.pid"
dbfilename "dump6379.rdb"
dir "/home/bigdata/redis_cluster"
logfile "/home/bigdata/redis cluster/redis err 6379.log"
使用查找替换修改另外5个文件:
:s/6379/6380
将六个节点合成一个集群
组合之前,请确保所有 redis 实例启动后,nodes-xxx.conf文件都生成正常。
忘记自己的redis安装在哪里的小伙伴可以通过find /-name redis*
查看
合体:
cd /opt/redis-6.2.1/src
redis-cli --cluster create -cluster-replicas 1 192.168.11.101:6379 192.168.11.101:6380 192.168.11.101:6381 192.168.11.101:6389 192.168.11.101:6390 192.168.11.101:6391
此处不要用 127.0.0.1 ,请用真实 P 地址。
–replicas1 采用最简单的方式配置集群,一台主机,一台从机,正好三组。
主从是解决并发读问题,集群是解决并发写和并发读问题
redis cluster 如何分配这六个节点?
一个集群至少要有三个主节点。
选项 --cluster-replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。
分配原则尽量保证每个主数据库运行在不同的 IP 地址,每个从库和主库不在一个ip地址上。
什么是 slots
All 16384 slots covered
一个 Redis 集群包含 16384个插槽(hashslot),数据库中的每个键都属于这 16384个插槽的其中一个。
集群使用公式 CRC16(key)% 163841
来计算键 key 属于哪个槽 ,其中 CRC16(key)语句用于计算键 key 的 CRC16 校验和 。
集群中的每个节点负责处理一部分插槽。 举个例子,如果一个集群可以有主节点,其中:
节点 A 负责处理 0 号至 5460 号插槽。
节点 B负责处理 5461 号至 10922 号插槽
将该值存放在那个里面,我们就切换到对应的数据库
查询集群中的值:
只能看自己插槽 中的值,而不能看其他端口号下的
Redis 集群提供了以下好处
实现扩容,分摊压力,无中心配置相对简单
Redis 集群的不足
多键操作是不被支持的
多键的 Redis 事务是不被支持的。lua脚本不被支持
由于集群方案出现较晚,很多公司已经采用了其他的集群方案,而代理或者客户端分片的方案想要迁移至 redis cluster,需要整体迁移而不是逐步过渡,复杂度比较大