学习笔记仅供参考,如果笔记描述的不太清晰,大家可以自行百度查阅其他的资料
该笔记是作者根据b站狂神说视频以及自己翻阅的一些资料而写
视频连接:狂神视频链接
如果有兴趣的小伙伴可以前去观看
欢迎大家一起学习
1、单击MySQL时代
90年代,一个基本的网站访问量一般不会太大,单个数据库完全足够!
那个时候,更多的使用的都是静态页面 html ~ 服务器根本没有太大的压力!!使用那个时候单击mysql完全够用
这个时候会产生一些瓶颈(弊端):
1、数据量如果太大、一个机器放不下了!
2、数据的索引 300w数据就一定要建立索引了(采用的B+Tree),访问量太大一个机器的内存也放不下
3、访问量(读写混合),一个服务器承受不了!
只要你开始出现以上的三种情况之一,那么你就必须要晋级!
2、Memcached(缓存)+ MySQL + 垂直拆分(读写分离) 时代
网站在80%的情况下都是在读数据,每次都要去查询数据库的话就十分的麻烦。所以说,我们为了减轻数据库的压力,我们可以使用缓存来保证效率!!!
发展过程:优化数据结构和索引–>(数据结构与索引并不好优化)采用文件缓存(IO操作 ) --> 文件缓存满足不了了开始使用Memcached(缓存)
3、分库分表+水平拆分+MySQL集群 时代
本质:数据库:解决读和写的问题
MySQL早些年使用的是MyISAM引擎:表锁,十分影响效率!高并发下就会出现严重的锁问题。开始转战
转战Innodb引擎:行锁,
慢慢的就开始使用分库分表来解决写的压力!MySQL在那个年代推出了表分区!但是并没有被过多使用!
MySQL的集群,很好的满足的那个年代的需求。
4、如今最近的年代
如今出现了很多新的数据:定位,音乐,等。
MySQL等关系型数据库就不够用了!因为数据量很大,而且变化很快!
于是出现了新型的数据库:
图形数据库、BSON、
如果使用MySQL来存储一些比较大的文件,博客,图片!数据库的表就会很大,效率就会很低。如果有一种数据库来专门处理这种数据,MySQL的压力就会变的十分的小(研究如何处理这些问题!)大数据的IO压力下,表几乎没法更改,–1亿条数据时,如果加列会很麻烦。
目前一个基本的互联网项目
为什么要用NoSQL?
用户的个人信息,社交网络,地理位置。用户自己产生的数据,用户日志等等爆发式增长!!
因此需要使用NoSQL。
NoSQL
NoSQL= Not Only SQL
关系型数据库:表格,行,列(POI 通过一个jar包操作Excel表)
NoSQL仅仅是一个概念,泛指非关系型的数据库,区别于关系数据库,它们不保证关系数据的ACID特性
泛指非关系型数据库,随着web2.0互联网的诞生!传统的关系型数据库很难对付web2.0时代,尤其是超大规模的高并发的社区!暴露出来很多难以克服的问题,NoSQL在当今大数据环境下发展的十分迅速,Redis是发展最快的,而且是我们当下必须掌握的技术。
由于很多的数据类型,例如用户的个人信息,社交网络,地理位置。这些数据类型的存储不需要一个固定的格式!不需要过多的操作就可以横向扩展!Map
解耦!
1、方便扩展(数据之间没有关系,很好扩展)
2、大数据量高性能(Redis 一秒可以写8w次,可以读取11w次,NoSQL的缓存记录级,是一种细粒度的缓存,性能比较高)
3、数据类型是多样型的!!(不需要实现设计数据库!随取随用!如果是数据量十分大的表,很多人就无法设计了!)
4、传统的RDBMS和 NoSQL
传统的 RDBMS
- 结构化组织(表,列)
- SQL
- 数据和关系都存在单独的表中
- 操作数据的数据操作语言,数据定义语言
- 严格的一致性
- 基础的事务操作
- ...
NoSQL
- 不仅仅是数据
- 没有固定的查询语言
- 键值对存储,列存储,文档存储,图形数据库(社交关系)
- 最终一致性,可以有误差的
- CAP定理 和 BASE 理论(异地多活!)
- 高性能,高可用,搞可扩展性
- ...
了解:3V+3高
大数据时代的3V:主要是描述问题的
大数据时代的3高:主要是对程序的要求
真正在公司中的实践:NoSQL+RDBMS 一起使用才是最强的
敏捷开发、极限编程
任何一家互联网的公司,都不可能只是简简单单让用户能用就好了
大量公司做的都是相同的业务;(竞品协议)
随着这样的竞争,业务是越来越完善,对于开发者的要求越来越高!
没有什么是加一层解决不了的
一个基本的商品页面背后的技术:
# 1、商品的基本信息
名称、价格、商家信息:
关系型数据库就可以解决!MySQL/Oracle (淘宝早年就去IOE了!-王坚:阿里云的这群疯子)
淘宝内部的 MySQL 不是大家用的 MySQL
# 2、商品的描述、评论(文字较多)
文档型数据库中,MongoDB
# 3、图片
分布式文件系统:FastDFS
- 淘宝自己的 TFS
- 谷歌的 Gooale 的 GFS
- Hadoop 的 HDFS
- 阿里云的 OSS
# 4、商品的关键字 (搜索)
- 搜索引擎 solr elasticsearch
- 淘宝使用的 ISerach 多隆
# 5、商品热门的波段信息
- 内存数据库
- Redis Tair、 Memache
# 6、商品的交易,外部的支付接口
- 三方应用
大型互联网应用存在的问题:
1、数据类型太多了
2、数据源太多了
3、经常重构
4、数据要改造,大面积改造 MySQL
MongoDB(必须掌握)
ConthDB
Redis(Remote Dictionary Server ),即远程字典服务
是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
免费、开源!是当下最热门的NoSQL技术之一!也被人们称之为结构化数据库!
1、内存存储、持久化,内存中是断电及失。使用说持久化很重要(rdb、aof)
2、效率高。开源用于告诉缓存
3、发布订阅系统
4、地图信息分析
5、计时器、技术器(浏览量!)
1、多样的数据类型
2、持久化
3、集群
4、事务
…
注意:
Redis推荐都是在Linux服务器上搭建的
1、下载安装包:https://github.com/redis/redis/tree/unstable
2、下载完毕得到压缩包解压到环境文件夹中方便开启:
3、开启Redis,双击运行服务即可!
Redis默认端口号:6379
4、使用Redis客户端来连接Redis
打开redis-cli(在打开服务端的前提)
在Windows下使用确实简单,但是Redis 推荐我们使用Linux去开发
Redis介绍:http://www.redis.cn/topics/introduction
方式一:手动安装redis(不推荐)
1、下载安装包!
https://redis.io/
我们一般把程序放在/otp目录下
4、基本的环境安装
yum install gcc-c++
执行make命令
make
make install
5、redis 的默认安装路径:usr/local/bin
6、将redis.conf放在自建文件夹下面
7、redis默认不是后台启动的,我们需要修改配置文件
8、启动redis服务
redis-server ../redis.conf
后面的是使用哪个配置文件开启服务
redis-cli -p 6379
使用客户端进行连接,-h 如果不写是默认ip,-p 端口号
输入密码
auth "[密码]"
9、如何关闭Redis服务?
shutdown
方式二:通过宝塔一键下载redis(推荐)
宝塔怎么使用可以去看看笔者的linux学习中的宝塔面板安装
https://blog.csdn.net/Ian_IMIL/article/details/118900785
宝塔面板安装的应用统一存放在主目录下的www/server/下,
配置文件redis.conf存放在redis的目录下,redis-server存放在redis/src的目录下
redis.conf已经被宝塔面板配置好了,启动就是后台启动,并且默认没有密码。可以直接启动
通过命令
redis-server ../redis.conf
redis-benchmark
是一个压力测试工具,可以使用这个工具来测试redis的性能。
redis-benchmark 命令参数!
redis-benchmark
参数:
-h :指定服务器主机名 //默认值:127.0.0.1
-p :指定服务器端口 //默认值:6379
-s :指定服务器socket
-c :指定并发连接数 //默认值:50
-n :指定请求数 //默认值:10000
-d :以字节的形式指定SET/GET值的数据大小 //默认值:2
-k :1=keep alive O=reconnect //默认值:1
-r :SET/GET/INCR使用随机 key,SADD使用随机值
-p :通过管道传输请求 //默认值:1
-q :强制退出redis。仅显示 query/sec值
--csv :以CSV格式输出
-l : 生成循环,永久执行测试
-t :仅运行以逗号分隔的测试命令列表。
-I :ldle模式。仅打开N个idle连接并等待。
如果我们要测试100个并发连接 10w个请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
但是这个性能测试需要你不设置密码才能进行直接使用
redis默认有16个数据库,默认使用的是第0个数据库
我们可以使用 select 进行切换
127.0.0.1:6379> select 3 #切换数据库
OK
127.0.0.1:6379[3]> DBSIZE #查看数据库大小
(integer) 0
查看所有的key
127.0.0.1:6379> keys * #查看数据库所有的keybash
1) "name"
2) "counter:__rand_int__"
3) "key:__rand_int__"
4) "myhash"
5) "mylist"
清空库
flushdb #清空当前库
flushall #情况所有库
redis使用的是单线程
Redis是很快的,官方表示,Redis是基于内存操作的,CPU不是Redis的性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽,既然可以使用单线程来实现,就使用单线程了。
Redis 是C 语言写的,官方提供的数据为 10w+的QPS,说明这个完全不比同样使用key-value的Memecache差!
Redis 为什么单线程还这么快?
1、误区1:高性能的服务器一定是多线程的?
2、误区2:多线程(CPU上下文会切换)一定比单线程高?
核心:redis 是将所有的数据全部放在内存中的,所以说使用单线程去操作效率就是最高的。多线程会产生CPU上下文的切换,这个是一个非常耗时的操作!!对于内存系统来说,如果没有上下文切换,效率就是最高的
如果万一CPU成为你的Redis瓶颈了,或者,你就是不想让服务器其他核闲置,那怎么办?
多起几个Redis进程就好了。Redis是keyvalue数据库,不是关系数据库,数据之间没有约束。只要客户端分清哪些key放在哪个Redis进程上就可以了。redis-cluster可以帮你做的更好。
单线程可以处理高并发请求吗?
当然可以,Redis都实现了。有一点概念需要澄清,并发并不是并行。
我们使用单线程的方式是无法发挥多核CPU 性能,有什么办法发挥多核CPU的性能嘛?
我们可以通过在单机开多个Redis 实例来完善!
set 声明一个键值对
get 通过键获取到值
expier 设置key的过期时间
ttl 查看当前key的剩余时间
exist 判断某个键是否存在
move 移除键值对
flushdb 清空当前库的数据
flushall 清空所有库中的数据
keys * 查看当前数据库中的所有的key
type 查看当前key的类型
append 在当前字符串类型的value后面追加一个字符串
set 声明一个键值对
set [key] [value]
get 通过键获取到值
get [key]
expier 设置key的过期时间
expier [key] [数字]
数字代表你要设置的过期时间
这个功能可以用来设置一些热点数据,在一定时间过期
ttl 查看当前key的剩余时间
ttl [key]
如果结果为-2,表示没有时间了或者还没有设置剩余时间
如果为正数,则正在倒计时
exist 判断某个键是否存在
exist [key]
结果为1表示存在
结果为0表示不存在
move 移除键值对
move [key] 1
参数 1 是代表的当前库
清空库
flushdb #清空当前库
flushall #情况所有库
keys * 查看当前数据库中的所有的key
keys *
type 查看当前key的类型
type [key]
append 在当前字符串类型的value后面追加一个字符串
append [key] [字符串]
如果有不知道的命令,可以直接去官网查看
http://www.redis.cn/commands.html
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。
key:字符串,m,n:数字,value:值,str:字符串
api | 效果 |
---|---|
set [key] [value] | 创建键值对,并且存储当前数据库中 |
get [key] | 通过key来查询value |
keys * | 查询当前库中的所有的key |
append [key] [str] | 在当前字符串类型的value后面追加一个字符串 |
strlen [key] | 获取当前字符串的长度 |
incr | 自动增长+1,如果当前key并不存在,则会自己创建一个 |
incrby [key] n | 设置步长并且增长指定的值,如果当前key并不存在,则会自己创建一个 |
decr | 自动减少-1,如果当前key并不存在,则会自己创建一个 |
decrby [key] n | 设置步长并且减少指定的值,如果当前key并不存在,则会自己创建一个 |
getrange [key] n m | 截取字符串 [n,m] |
getrange [key] 0 -1 | 查看全部的字符串,与get [key] 功能一样 |
setrange [key] n [str] | 从下标n处开始修改字符串, 并且可以覆盖 |
setex [key] n [str] | 创建键值对,并且设置过期时间 |
ttl [key] | 查看键剩余的时间,如果死亡或没有设置过期时间值为-2 |
setnx [key] [value] | 不存在设置,如果库中已经有这个键了,返回0,创建失败,反之返回1 |
mset [key] [value] [key] [value] | 批量输入 |
mget [key] [key] | 批量获取 |
msetnx [key] [value] [key] [value] | 批量不存在创建,这是一个原子性的操作!!! |
getset [key] [value] | 一个组合命令,先get再set,如果不存在值返回null |
string 我们对string字符串使用的方法与java类似
127.0.0.1:6379> set key1 v1
OK
127.0.0.1:6379> get key1
"v1"
127.0.0.1:6379> keys *
1) "key1"
127.0.0.1:6379> APPEND key1 11 #在当前字符串类型的value后面追加一个字符串
(integer) 4
127.0.0.1:6379> keys *
1) "key1"
127.0.0.1:6379> get key1
"v111"
127.0.0.1:6379> strlen key1 #获取当前字符串的长度
(integer) 4
通过命令进行+1,-1操作,数字的增加减少操作
127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views #incr 自动增长+1
(integer) 1
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> get views
"2"
#设置步长的方法
127.0.0.1:6379> incrby views 10
(integer) 7
127.0.0.1:6379> incrby views 10
(integer) 17
127.0.0.1:6379> decr views #decr 自动减少-1
(integer) 1
127.0.0.1:6379> decr views
(integer) 0
127.0.0.1:6379> decr views
(integer) -1
127.0.0.1:6379> decr views
(integer) -2
127.0.0.1:6379> get views
"-2"
#设置步长的方法
127.0.0.1:6379> decrby views 10
(integer) 7
127.0.0.1:6379> decrby views 10
(integer) -3
如果当前key并不存在,则会自己创建一个
字符串的范围
#查看第0个到第3个的字符串
127.0.0.1:6379> set key1 "hello"
OK
127.0.0.1:6379> getrange key1 0 3 #截取字符串 [0,3]
"hell"
127.0.0.1:6379> getrange key1 0 -1 #-1 查看全部的字符串,与get [key] 功能一样
"hello"
#替换 替换指定位置的字符串
127.0.0.1:6379> setrange key1 1 ao #修改字符串 1 并且可以覆盖
(integer) 5
127.0.0.1:6379> get key1
"haolo"
setex与setnx
setex ->(set with expire) #设置过期时间
127.0.0.1:6379> setex key2 3 hell
OK
127.0.0.1:6379> ttl key2
(integer) -2
setnx ->(set if not exist) #不存在设置
127.0.0.1:6379> setnx mykey redis #如果mykey不存在,创建成功
(integer) 1 #创建成功返回1
127.0.0.1:6379> keys *
1) "key1"
2) "mykey"
127.0.0.1:6379> setnx mykey re #如果mykey存在,创建失败
(integer) 0 #创建失败返回0
批量操作
mset 批量输入 mset [key] [value] [key] [value]
mget 批量获取 mget [key] [key]
msetnx 批量不存在创建
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379> mget k1 k2 k3
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v1 k4 v4 #这是一个原子性的操作
(integer) 0
127.0.0.1:6379> keys *
1) "k1"
2) "k3"
3) "k2"
#在实战中,我们在redis中创建对象如下:
set user:1 {name:zhangsan,age:2} #设置一个user:1对象 值为一个json字符来保存一个对象!
#我们可以这样设计,这里的key是一个巧妙的设计:user:{id}:{filed},如此设计在redis中是完全ok的
127.0.0.1:6379> mset user:1:name zhangsan user:1:age 2
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "zhangsan"
2) "2"
getset命令
getset #一个组合命令,先get再set,如果不存在值返回null
127.0.0.1:6379> getset db redis
(nil)
127.0.0.1:6379> GETSET db se
"redis"
127.0.0.1:6379> get db
"se"
String类似的使用场景:value除了是我们的字符串,还可以是我们的数字!
在redis里面,我们可以把list玩成 栈,队列,阻塞队列!
所有的list
命令都是以l
开头的
list:列表名,value:值,m,n:数字,newvalue:新的值
api | 效果 |
---|---|
lpush list [value] | 将一个或多个值插入到列表的头部(左边) |
lrange list 0 -1 | 从列表左边取出指定范围的值 |
rpush list [value] | #将一个或多个值插入到列表的尾部(右边) |
lpop list n | 移除左边元素从开头数第n个值,默认为第一个 |
rpop list n | 移除右边元素从开头数第n个值,默认为第一个 |
lrem list n [value] | 通过准确的值来删除列表中的n个值 |
lindex list n | 通过准确的下标来获取列表中的值 |
llen list | 获取列表的长度 |
ltrim list n m | 修剪 通过下标截取指定的长度,值拿到下标1到下标2中的值,list已经被改变了,只剩截取的内容了 |
rpoplpush list newlist | 移除列表尾部的元素并且添加到新的列表的首部 |
lset list | 通过下标来为数组设置值,list不能为空,下标指定的值也不能为空,否则设置失败 |
exists list | 判断list是否存在,存在返回1,不存在返回0 |
linsert list before/after [value] [newvalue] | 在指定的值的前面或后面插入指定的新值 |
lpush #将一个或多个值插入到列表的头部(左边)
lrange #从列表左边取出指定范围的值
127.0.0.1:6379> lpush list 1
(integer) 1
127.0.0.1:6379> lpush list 2
(integer) 2
127.0.0.1:6379> lpush list 3
(integer) 3
127.0.0.1:6379> lpush list 3
(integer) 4
127.0.0.1:6379> lrange list 0 -1 #通过区间获取list中的值
1) "3"
2) "3"
3) "2"
4) "1"
127.0.0.1:6379> lrange list 0 1
1) "3"
2) "3"
rpush #将一个或多个值插入到列表的尾部(右边)
127.0.0.1:6379> rpush list fore
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "fore"
移除操作
lpop #移除左边元素第一个值
rpop #移除右边元素第一个值
lrem #通过准确的值来进行删除
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "fore"
127.0.0.1:6379> lpop list #移除左边元素第一个值
"three"
127.0.0.1:6379> rpop list #移除右边元素第一个值
"fore"
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> lpop list 2 #移除左边元素前两个值
1) "two"
2) "one"
127.0.0.1:6379> lrange list 0 -1
(empty array)
############################################################################
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "two"
3) "three"
4) "fore"
5) "five"
6) "one"
127.0.0.1:6379> lrem list 1 two #删除一个值为two的元素
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "three"
3) "fore"
4) "five"
5) "one"
127.0.0.1:6379> lrem list 2 one #删除两个值为one的元素
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "fore"
3) "five"
通过下标获取值
lindex #通过下标获取列表中的某一个值
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "two"
3) "three"
4) "fore"
5) "five"
127.0.0.1:6379> lindex list 1
"two"
127.0.0.1:6379> lindex list 0
"one"
127.0.0.1:6379> lindex list 3
"fore"
获取列表的长度
llen #获取列表的长度
127.0.0.1:6379> llen list
(integer) 5
trim修剪
ltrim #修剪 通过下标截取指定的长度
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"
3) "hello3"
4) "hello4"
127.0.0.1:6379> ltrim mylist 1 2 #值拿到下标1到下标2中的值
OK
127.0.0.1:6379> lrange mylist 0 -1 #list已经被改变了,只剩截取的内容了
1) "hello2"
2) "hello3"
rpoplpush 移除列表尾部的元素并且添加到新的列表的首部
rpoplpush #移除列表尾部的元素并且添加到新的列表的首部
127.0.0.1:6379> lrange mylist 0 -1
1) "hello2"
2) "hello3"
127.0.0.1:6379> rpoplpush mylist otherlist
"hello3"
127.0.0.1:6379> lrange mylist 0 -1
1) "hello2"
127.0.0.1:6379> lrange otherlist 0 -1
1) "hello3"
lset 通过下标来为数组设置值
lset #通过下标来为数组设置值
127.0.0.1:6379> exists list
(integer) 0
127.0.0.1:6379> lpush list 1
(integer) 1
127.0.0.1:6379> lpush list 2
(integer) 2
127.0.0.1:6379> lpush list 3
(integer) 3
127.0.0.1:6379> lset list 0 hello
OK
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "2"
3) "1"
lrange 在指定位置进行插入值
lrange #在指定位置进行插入值
127.0.0.1:6379> lrange list 0 -1
1) "2"
2) "3"
3) "1"
127.0.0.1:6379> linsert list before 1 4
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "2"
2) "3"
3) "4"
4) "1"
127.0.0.1:6379> linsert list before 6 6 #如果指定的值为null,则不能进行插入
(integer) -1
总结:
他实际上是一个链表,before Node after , left , right 都可以插入值
如果key不存在,创建新的链表
如果key存在,新增内容
如果移除了所有值,空链表,也代表不存在!
在两边插入或者改动值,效率最高! 中间元素改动效率偏低
set中的值是不能重复的!set是无序不重复集合
所有的set
命令都是以s
开头的
set:集合,value:值,n:数字
api | 效果 |
---|---|
sadd set [value] | 添加一个集合 |
smembers set | 查看set集合中的成员 |
sismember set [value] | 查看值是否为集合中的成员 |
scard set | 查看集合中元素的个数 |
srem set [value] | 移除指定集合中的指定元素 |
srandmember set | 随机获取集合中的一个元素 |
srandmember set n | 随机抽取集合中的n个元素 |
smembers set | 随机从集合中移除一个元素 |
smembers set n | 随机从集合中移除n个元素 |
smove set1 set2 [value] | 将一个指定的值,移动到另一个set集合中 |
sdiff set1 set2 | set1与set2的差集 |
sinter set1 set2 | set1与set2的交集 |
sunion set1 set2 | set1与set2的并集 |
##########################################################
127.0.0.1:6379> sadd myset heeh # set集合中添加元素
(integer) 1
127.0.0.1:6379> sadd myset kun
(integer) 1
127.0.0.1:6379> sadd myset sss
(integer) 1
127.0.0.1:6379> smembers myset # 查看set集合中的成员
1) "sss"
2) "kun"
3) "heeh"
127.0.0.1:6379> sismember myset kun #查看值是否为集合中的成员
(integer) 1
127.0.0.1:6379> sismember myset kun1
(integer) 0
##########################################################
127.0.0.1:6379> scard myset #获取当前集合中元素的个数
(integer) 3
##########################################################
127.0.0.1:6379> SMEMBERS myset
1) "sss"
2) "kun"
3) "heeh"
127.0.0.1:6379> srem myset heeh #移除set集合中的指定元素
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "sss"
2) "kun"
##########################################################
127.0.0.1:6379> srandmember myset #随机获取集合中的一个值
"sss"
127.0.0.1:6379> srandmember myset
"kun"
127.0.0.1:6379> srandmember myset 2 #随机抽取集合中的n个值
1) "sss"
2) "kun"
##########################################################
127.0.0.1:6379> smembers myset #随机从集合中移除一个元素
1) "hhs"
2) "sss"
3) "kun"
4) "hhh"
127.0.0.1:6379> spop myset
"sss"
127.0.0.1:6379> spop myset
"hhs"
127.0.0.1:6379> spop myset 3 #随机从集合中移除n个元素
1) "hhs"
2) "hhh"
3) "kun"
##########################################################
将一个指定的值,移动到另一个set集合中!
127.0.0.1:6379> sadd set1 a
(integer) 1
127.0.0.1:6379> sadd set1 b
(integer) 1
127.0.0.1:6379> sadd set2 c
(integer) 1
127.0.0.1:6379> sadd set2 d
(integer) 1
127.0.0.1:6379> smove set2 set1 c
(integer) 1
127.0.0.1:6379> smove set2 set1 d
(integer) 1
127.0.0.1:6379> smembers set2
(empty array)
127.0.0.1:6379> smembers set1
1) "c"
2) "b"
3) "a"
4) "d"
##########################################################
微博,b站,共同关注(并集)
数字集合类:
-差集
-交集
-并集
127.0.0.1:6379> sdiff set1 set2 #set1与set2的差集
1) "a"
2) "b"
127.0.0.1:6379> sinter set1 set2 #set1与set2的交集 共同好友就可以这样实现
1) "c"
2) "d"
127.0.0.1:6379> sunion set1 set2 #set1与set2的并集
1) "b"
2) "d"
3) "c"
4) "a"
5) "e"
6) "f"
在微博中的应用,A用户将所有关注的人放在一个set集合中,将它的粉丝也放在一个集合中!
共同关注,共同爱好,二度好友,推荐好友(使用六度分割理论)
Map集合,key-map集合----->key-
本质和String类型没有什么太大区别,还是一个集合
所有的hash
命令都是以h
开头的
key:键,f:字段名,value:值
api | 解释 |
---|---|
hset key f [value] | set一个具体的key-value |
hget key f | 获取一个字段值 |
hmsetkey f [value] f [value] | #set多个 key-value |
hmget key f f | 获取多个字段值 |
hgetall key | 获取全部的数据 |
hdelkey key f | 删除hash指定的key的字段,只要删除了字段,对应的value值也就没有了 |
hlen key | 当前hash中含有的键值对数量 |
hexists key f | 判断hash中的字段是否存在 |
hkeys key | 只获得所有的field |
hvals key | 只获得所有的value |
hincrby key f n | 使字段的值增长n |
hsetnx | 存在判断添加 |
代码演示:
127.0.0.1:6379> hset myhash f1 kun #set一个具体的key-value
(integer) 1
127.0.0.1:6379> hget myhash f1 #获取一个字段值
"kun"
127.0.0.1:6379> hmset myhash f1 heh f2 ww #set多个 key-value
OK
127.0.0.1:6379> hmget myhash f1 f2 #获取多个字段值
1) "heh"
2) "ww"
127.0.0.1:6379> hgetall myhash #获取全部的数据
1) "f1"
2) "heh"
3) "f2"
4) "ww"
#######################################################################
#删除指定的值
127.0.0.1:6379> hdel myhash f1 #删除hash指定的key的字段
(integer) 1
127.0.0.1:6379> hgetall myhash #只要删除了字段,对应的value值也就没有了
1) "f2"
2) "ww"
#######################################################################
127.0.0.1:6379> hlen myhash #当前hash中含有的键值对数量
(integer) 1
#######################################################################
127.0.0.1:6379> hexists myhash f1 #判断hash中的字段是否存在
(integer) 0
127.0.0.1:6379> hexists myhash f2
(integer) 1
#######################################################################
127.0.0.1:6379> hkeys myhash #只获得所有的field
1) "f2"
127.0.0.1:6379> hvals myhash #只获得所有的value
1) "ww"
#######################################################################
127.0.0.1:6379> hset myhash f1 2
(integer) 1
127.0.0.1:6379> hincrby myhash f1 1 #使字段的值增长1
(integer) 3
127.0.0.1:6379> hincrby myhash f1 1
(integer) 4
#######################################################################
127.0.0.1:6379> hsetnx myhash f1 2 #存在判断添加
(integer) 0 #hash中存在,创建失败,返回0
127.0.0.1:6379> hsetnx myhash f3 2
(integer) 1 #hash中不存在,创建成功,返回1
hash变更的数据:user对象数据,key存放user对象,字段存属性
尤其是用户信息等,经常变动的信息,hash更适合与对象的存储,
String更加适合字符串的存储
在set的基础上,增加了一个值。
set k1 v1
zset k1 score1 v1
zset :有序集合,n:score,value:值
api | 效果 |
---|---|
zadd zset n [value] | 添加一个值 |
zadd zset n [value] n [value] | 添加多个值 |
zrange zset 0 -1 | 遍历 |
zrangebyscore zset -inf +inf | 从最小值到最大值对集合进行遍历 -inf负无穷 +inf正无穷 |
zrangebyscore zset -inf +inf [withscores] | 从最小值到最大值对集合进行遍历,并且显示score |
zrevrange zset 0 -1 [withscores] | 降序排序,从最大值到最小值对集合进行遍历 |
zrem [value] | 移除有序集合中指定的元素 |
zcard zset | 获取有序集合中的元素个数 |
zcount zset n m | 获取有序集合中指定区间的元素的数量 |
代码演示:
127.0.0.1:6379> zadd myset 1 one #添加一个值
(integer) 1
127.0.0.1:6379> zadd myset 2 two
(integer) 1
127.0.0.1:6379> zadd myset 2 two 3 three #添加多个值
(integer) 1
127.0.0.1:6379> zrange myset 0 -1 #遍历
1) "one"
2) "two"
3) "three"
#######################################################################
#排序如何实现
#升序排序
127.0.0.1:6379> zadd salary 200 kun
(integer) 1
127.0.0.1:6379> zadd salary 5000 zhangsan
(integer) 1
127.0.0.1:6379> zadd salary 1000 shen
(integer) 1
127.0.0.1:6379> zrangebyscore salary -inf +inf #从最小值到最大值进行遍历 -inf负无穷
1) "kun"
2) "shen"
3) "zhangsan"
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores #从最小值到最大值进行遍历,包括添加的那个值
1) "kun"
2) "200"
3) "shen"
4) "1000"
5) "zhangsan"
6) "5000"
127.0.0.1:6379> zrangebyscore salary -inf 2500 withscores #从最小值到最大值进行遍历工资小于2500的
1) "kun"
2) "200"
3) "shen"
4) "1000"
#降序排序
127.0.0.1:6379> zrevrange salary 0 -1 withscores
1) "zhangsan"
2) "5000"
3) "shen"
4) "1000"
#######################################################################
zrem #移除元素
127.0.0.1:6379> zrem salary kun
(integer) 1
127.0.0.1:6379> zrangebyscore salary -inf inf withscores
1) "shen"
2) "1000"
3) "zhangsan"
4) "5000"
127.0.0.1:6379> zcard salary #获取有序集合中的元素个数
(integer) 2
#######################################################################
127.0.0.1:6379> zadd s1 1 hehe
(integer) 1
127.0.0.1:6379> zadd s1 2 ss 3 aa
(integer) 2
127.0.0.1:6379> zcount s1 1 4 #获取指定区间的元素的数量
(integer) 3
127.0.0.1:6379> zcount s1 1 3
(integer) 3
127.0.0.1:6379> zcount s1 1 2
(integer) 2
案例思路:set 排序,存储班级成绩表,工资表排序
普通消息,1;重要消息,2
朋友的定位,附近的人,打车距离计算
使用Redis的Geo 在Redis3.2版本就推出了!!这个功能可以推算地理位置的信息,两地之间的距离,方圆几里的人!
http://www.jsons.cn/lngcode/
只有6个命令
api | 效果 |
---|---|
geoadd | 添加地理位置 |
geopos | 获取指定的经度和维度 |
geohash | 返回一个或多个位置元素的Geohash表示 |
geodist | 获取两地的直线距离 |
georadius | 以给定的经纬度为中心,找出某一半径内的元素 |
georadiusbymember | 位于指定元素周围的其他元素 |
geoadd 添加地理位置
#geoadd 添加地理位置
#规则:两级无法直接添加,我们一般会去下载城市数据,直接通过java程序一次性导入!!!
#将指定的地理空间位置(纬度、经度、名称)添加到指定的key中
127.0.0.1:6379> geoadd china:city 116.405285 39.904989 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 121.472644 31.231706 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 106.504962 29.533155 chongqing
(integer) 1
geopos 获取地理位置
#获取指定的经度和维度
127.0.0.1:6379> geopos china:city beijing
1) 1) "116.40528291463851929"
2) "39.9049884229125027"
127.0.0.1:6379> geopos china:city shanghai
1) 1) "121.47264629602432251"
2) "31.23170490709807012"
geodist 获取两地的距离
单位:
#geodist 获取两地的直线距离
127.0.0.1:6379> geodist china:city beijing shanghai
"1067597.9668"
127.0.0.1:6379> geodist china:city beijing shanghai km
"1067.5980"
georadius 以给定的经纬度为中心,找出某一半径内的元素
我附近的人,获取所有附近的人的地址,定位,通过半径来查询!
127.0.0.1:6379> georadius china:city 116 39 1000 km #提供一个点,然后从集合中查找以这个点为中心半径内的所有的城市
1) "beijing"
2) "shanghai"
127.0.0.1:6379> georadius china:city 116 39 500 km
1) "beijing"
127.0.0.1:6379> georadius china:city 116 39 500 km withdist
1) 1) "beijing"
2) "106.5063"
127.0.0.1:6379> georadius china:city 116 39 500 km withcoord
1) 1) "beijing"
2) 1) "116.40528291463851929"
2) "39.9049884229125027"
##附近的人,获取指定的人
127.0.0.1:6379> georadius china:city 116 39 5000 km withcoord count 1
1) 1) "beijing"
2) 1) "116.40528291463851929"
2) "39.9049884229125027"
127.0.0.1:6379> georadius china:city 116 39 5000 km withcoord count 2
1) 1) "beijing"
2) 1) "116.40528291463851929"
2) "39.9049884229125027"
2) 1) "shanghai"
2) 1) "121.47264629602432251"
2) "31.23170490709807012"
georadiusbymember 位于指定元素周围的其他元素
#用来找城市刚刚好,城市之间的定位
127.0.0.1:6379> georadiusbymember china:city beijing 1000 km
1) "beijing"
127.0.0.1:6379> georadiusbymember china:city beijing 3000 km
1) "chongqing"
2) "shanghai"
3) "beijing"
geohash 返回一个或多个位置元素的geohash表示
#geohash 返回一个或多个位置元素的geohash表示
127.0.0.1:6379> geohash china:city beijing shanghai #把经纬度转换为字符串
1) "wx4g0b7xrt0" #将二维的经纬度转换为一维的字符串
2) "wtw3sjt9vg0"
geo底层的实现原理确实就是zset,我们可以使用zset元素中的命令来操作geo
127.0.0.1:6379> zrange china:city 0 -1
1) "chongqing"
2) "shanghai"
3) "beijing"
127.0.0.1:6379> zrem china:city beijing
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1
1) "chongqing"
2) "shanghai"
所以说基本的数据类型才是最重要的!
什么是基数?
A{1,3,5,7,9}
B{1,3,5,7,8}
基数(不重复的元素),可以接受误差的!
简介
Redis 2.8.9 版本就更新了Hyperloglog 数据结构!
Hyperloglog 基数统计的算法!
网页的UV (一个人访问一个网站多次,但是还是算作一个人)
传统的方式,set保存用户的id,然后就可以统计set中的元素数量作为标准判断!
这个方式如果保存大量的用户id,就会比较麻烦!我们的目的并不是保存用户id,而是用来计数的。
Hyperloglog优点:
占用的内存是固定的,2^64不同的元素的技术,只需要12kb的内存!
0.81%错误率!统计UV任务,可以忽略不计!
测试使用
127.0.0.1:6379> pfadd mykey a b c d e f g #添加的第一组元素
(integer) 1
127.0.0.1:6379> pfcount mykey #查看元素基数
(integer) 7
127.0.0.1:6379> pfadd mykey2 a a h i #添加的第二组元素
(integer) 1
127.0.0.1:6379> pfcount mykey2
(integer) 3
127.0.0.1:6379> pfmerge mykey3 mykey mykey2 #合并两个集合
OK
127.0.0.1:6379> pfcount mykey3
(integer) 9
如果允许容错,那么一定可以使用Hyperloglog!
如果不允许容错,就使用set或者自己的数据类型即可
位存储
统计用户信息,活跃,不活跃!登录、未登录!打卡,365打卡!两个状态的,都可以使用Bitmaps !
Bitmaps位图,数据结构!都是操作二进制位来进行记录,就只有0和1两个状态!
使用bitmap来记录周一到周日的打卡!
127.0.0.1:6379> setbit sign 0 0 #周一 未打卡
(integer) 0
127.0.0.1:6379> setbit sign 1 0 #周二 未打卡
(integer) 0
127.0.0.1:6379> setbit sign 2 1 #周三 打卡
(integer) 0
127.0.0.1:6379> setbit sign 4 0
(integer) 0
127.0.0.1:6379> setbit sign 5 0
(integer) 0
127.0.0.1:6379> setbit sign 6 0
(integer) 0
############################################################
#查看某一天是否打卡
127.0.0.1:6379> getbit sign 1
(integer) 0 #结果为0 未打卡
127.0.0.1:6379> getbit sign 2
(integer) 1 #结果为1 打卡
############################################################
#统计的操作,统计打卡的天数
127.0.0.1:6379> bitcount sign #默认为全部
(integer) 2 #当前两个打卡
要么同时成功,要么同时失败,原子性!
Redis单条命令是保证原子性的,但是redis事务是不保证原子性的
Redis事务没有隔离级别的概念!
所有的命令在事务中,并没有直接被执行!只有发起执行命令的时候才会执行!Exec
Redis 事务本质:一组指令的集合!一个事务中的所有的命令都会被序列化,在事务执行过程中,会按照顺序执行!
一次性、顺序性、排他性!执行一些列的命令
----- 队列 set set set 执行 -----
Redis的事务:
正常执行事务!
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)> get k2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec #执行事务
1) OK
2) OK
3) "v2"
4) OK
放弃事务
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)> discard #放弃事务
OK
127.0.0.1:6379> get k2 #事务中的队列命令都没有被执行
(nil)
事务出现异常
编译型异常(代码有问题!命令有错! ),事务中所有的命令都不会被执行!
运行时异常(1/0),如果事务队列中存在语法性,那么执行命令的时候,其他命令是可以正常执行的,错误命令抛出异常!
#编译时错误
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)> get k2
QUEUED
127.0.0.1:6379(TX)> set k3 #错误的命令
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors. #执行事务报错
#结果:所有的命令都没有被执行
##################################################################
#运行时错误
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incr k1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> get k1
QUEUED
127.0.0.1:6379(TX)> exec
1) (error) ERR value is not an integer or out of range #虽然第一条命令报错了,但是依旧正常执行成功了!
2) OK
3) "v2"
4) "v1"
悲观锁
很悲观,认为什么时候都会出问题,无论做什么都会加锁!非常影响性能
乐观锁
很乐观,认为什么时候都不会出现问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人改动过这个数据。
获取version
更新的时候比较version
Redis的事务机制以及watch指令(CAS)实现乐观锁。
所谓乐观锁,就是利用版本号比较机制,只是在读数据的时候,将读到的数据的版本号一起读出来,当对数据的操作结束后,准备写数据的时候,再进行一次数据版本号的比较,若版本号没有变化,即认为数据是一致的,没有更改,可以直接写入,若版本号有变化,则认为数据被更新,不能写入,防止脏写。
Redis监视测试
127.0.0.1:6379> set money 1000
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) 980
2) (integer) 20
测试多线程修改值,使用watch 可以当做redis的乐观锁操作
127.0.0.1:6379> watch money
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 #执行之前,另一个线程修改了我们的值,这个时候,就会导致事务执行失败
(nil)
127.0.0.1:6379> unwatch #放弃监视
OK
127.0.0.1:6379> watch money
OK
如果执行失败,获取最新值即可
Redis实现乐观锁比较简单,主要思路就是watch一个key的变动,并在watch和unwatch之间做一个类似事务操作,只有当事务操作成功,整体循环才会跳出,当然,当操作期间watch的key变动时候,提交事务操作时候,事务操作将会被取消。
public void testRedisSyn(int clientName,String clientList) {
//redis中存储商品数量为(goodsNum:100)
String key = "goodsNum";
Jedis jedis = new Jedis("192.168.140.98", 6379);
jedis.auth("redis密码");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (true) {
try {
jedis.watch(key);
System.out.println("顾客:" + clientName + "开始抢商品");
System.out.println("当前商品的个数:" + jedis.get(key));
//当前商品个数
int prdNum = Integer.parseInt(jedis.get(key));
if (prdNum > 0) {
//开启事务,返回一个事务控制对象
Transaction transaction = jedis.multi();
//预先在事务对象中装入要执行的操作
transaction.set(key, String.valueOf(prdNum - 1));
List<Object> exec = transaction.exec();
if (exec == null || exec.isEmpty()) {
//可能是watch-key被外部修改,或者是数据操作被驳回
System.out.println("悲剧了,顾客:" + clientName + "没有抢到商品");
} else {
//这个命令是做啥的。//抢到商品记录一下
jedis.sadd(clientList, clientName+"");
System.out.println("好高兴,顾客:" + clientName + "抢到商品");
break;
}
}
} catch (NumberFormatException e) {
e.printStackTrace();
}finally {
jedis.unwatch();
}
}
}
1.在不成功的情况下,一般需要重试几次,在重试的过程中每次循环都需要重新watch操作,因为每次事务提交之后,watch操作都会失效。
2.在事务提交之后返回的结果对象分为几种情况
1)事务提交前,watch的key发生改变,返回的List对象并不是null,而是一个初始化后的空对象(size==0)
2)事务提交前,watch的key没有改变,事务提交成功,返回的List对象中有一个"OK"的String对象。
如果是高并发场景,就使用乐观锁,因为乐观锁性能比悲观锁好;悲观锁不适合高并发场景。
乐观锁的实现方式:数据库,redis;版本号。
乐观锁的使用场景,悲观锁的使用场景。
synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。
CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
1、查询时 获取乐观锁的标记
2、只有更新的时候,才会更新锁
3、重试,重新获取锁,然后去更新。
锁机制:乐观锁(版本号,CAS),悲观锁(同步锁)
乐观锁的实现,以及乐观锁的使用场景。 使用DB实现乐观锁。
使用乐观锁进行下单减库存的操作。
4、watch,监视键值对,作用时如果事务提交exec时发现监视的监视对发生变化,事务将被取消。
我们要使用java来操作Redis
什么是Jedis,是redis官方推荐的java连接开发工具!
使用java操作Redis的中间件,如果要使用java操作redis,那么需要学习jedis
在进行连接之前,我们需要开启Linxu服务器中防火墙的6379端口号,这样我们才能成功进行远程连接
并且需要修改redis-conf中的配置
先关闭: redis-cli shutdown
再启动:redis-server redis.conf
1、导入对应的依赖
<dependencies>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>3.6.3version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.76version>
dependency>
dependencies>
2、编码测试:
连接Redis
public static void main(String[] args) {
//1、new Jedis 对象即可
Jedis jedis = new Jedis("127.0.0.1",6379);
//输入redis-cli端设置的密码
//jedis.auth("你设置的密码"); 没有密码就不填写
//所有的命令就是我们之前学习的所有指令!!!
System.out.println(jedis.ping());
jedis.close();//关闭连接
}
运行结果:
String
List
Set
Hash
Zset
jedis开启事务
public static void main(String[] args) {
Jedis jedis = new Jedis("120.79.184.67",6379);
JSONObject jsonObject = new JSONObject();
jsonObject.put("hello","world");
jsonObject.put("name","kun");
String s = jsonObject.toJSONString();
//开启事务
Transaction multi = jedis.multi();
jedis.watch(s); //开启监控
try {
multi.set("user1",s);
multi.set("user2",s);
multi.exec(); //成功执行事务
}catch (Exception e){
e.getMessage();
multi.discard();//失败抛弃事务
}finally {
jedis.close();//关闭连接
}
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
}
正常输出结果:
如果出现异常:
SpringBoot操作数据: spring-data jpa jdbc mongodb redis !
SpringData 也是和SpringBoot齐名的项目!
说明︰在SpringBoot2.x之后,原来使用的jedis被替换为了lettuce?
jedis : 采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用jedis pool连接池!更像BIO模式
lettuce:采用netty,实例可以在多个线程中进行共享,不存在线程不安全的情况!可以减少线程数量,更像NIO模式
源码分析:
@Bean
@ConditionalOnMissingBean(name = {"redisTemplate"}) //我们可以自己定义一个redisTemplate来替换这个默认的
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
//默认的 RedisTemplate 没有过多的设置,redis 对象都是需要序列化!
// 两个泛型都是 Object, Object 的类型,我们后面使用需要强制转换
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean //由于string 是redis中最常使用的类型,我也单独提出了一个bean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
整合:
1、导入依赖
2、配置连接
# SpringBoot 所有的配置类,都有一个自动配置类 RedisAutoConfiguration
# 自动配置类都会绑定一个properties 配置文件 RedisProperties
#配置redis
spring.redis.port=6379
spring.redis.host=120.79.184.67
3、测试!
@SpringBootTest
class RedisTeApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
//redisTemplate 操作不同的数据类型,api和我们的指令是一样的
//opsForValue 操作字符串的 类似于string
//opsForList 操作集合的
//除了基本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务,和基本的CRUn
//获取我们redis的连接对象
// RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
// connection.ping();
redisTemplate.opsForValue().set("k1","hello");
System.out.println(redisTemplate.opsForValue().get("k1"));
}
}
对象中的方法
测试结果:
但是当我们进入xshell查看时,却发现存入的值是一堆16进制
为了解决序列化问题,我们需要自定义模板
@Configuration
public class RedisConfig {
//自己定义一个RedisTemplate
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
//我们为了方便自己开发方便,一般直接使用String, Object
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//序列化配置
//jackson的序列化
Jackson2JsonRedisSerializer<Object> objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper om =new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
objectJackson2JsonRedisSerializer.setObjectMapper(om);
//string的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//string采用string的序列化方式
template.setStringSerializer(stringRedisSerializer);
//hash的key采用string的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
//key采用string的序列化方式
template.setKeySerializer(stringRedisSerializer);
//hash的value采用jackson的序列化方式
template.setHashValueSerializer(objectJackson2JsonRedisSerializer);
//value采用jackson的序列化方式
template.setValueSerializer(objectJackson2JsonRedisSerializer);
return template;
}
}
在企业开发中,我们80%都不会使用这个原生的方式去编写代码!
自己写一些RedisUtils工具类
百度搜索即可
所有的redis操作,其实对于java开发人员来说,十分的简单,更重要是要去理解redis的思想和每一种数据结构的用处和作用场景!
Springboot方式连接到远程服务器
众所周知,Springboot已经不默认集成Jedis了,是使用的Lettuce。要连接也比较简单,在你的spring配置文件中添加即可:
spring:
redis:
host: x.xxx.xxx.xx
port: 6379
#设置密码
password: "你设置的密码"
启动的时候,就是通过配置文件启动的
单位
1、配置文件unit单位 对大小写不敏感
包含
就跟Spring、Import 可以把多个配置文件配置过来
网络
如果bing 127.0.0.1 没有被注释,只能本机访问
bind 127.0.0.1 #绑定的ip
protected-mode yes #保护模式
port 6379 #端口设置
GENERAL 通用配置
daemonize yes #默认是no,需要我们自己打开。守护进程,后台运行。
pidfile /www/server/redis/redis.pid #配置文件的pid文件 如果以后台运行,我们就需要指定一个pid文件
loglevel notice #日志级别
logfile "/www/server/redis/redis.log" #日志文件生成的文件名
databases 16 #数据库数量
always-show-logo yes #是否显示logo,默认是开启的
快照
持久化,在规定的时间内,执行多少操作,则会持久化到文件.rdb .aof
redis是内存数据库,如果没有持久化,那么数据断电及失!
#持久化规则
save 900 1 #如果900秒内,如果至少有一个key进行了修改,我们就进行持久化操作
save 300 10 #如果300秒内,如果至少有10个key进行了修改,我们就进行持久化操作
save 60 10000 #如果60秒内,如果至少有1w个key进行了修改,我们就进行持久化操作
#我们之后学习持久化,会自己定义这个规则
stop-writes-on-bgsave-error yes #持久化如果出错,是否还继续工作。默认是继续
rdbcompression yes #是否压缩我们的rdb文件,默认是yes,需要消耗一些cpu的资源
rdbchecksum yes #保存rdb的时候,是否检查校验rdb文件
dbfilename dump.rdb #rdb保存的文件名字
dir /www/server/redis/ #rdb文件储存的目录
主从复制 REPLICATION
安全 SECURITY
requirepass 111222 #设置密码
我们也可以在这里设置面
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) ""
127.0.0.1:6379> config set requirepass "111222"
ok
127.0.0.1:6379> auto 111222
ok
限制客户端 CLIENTS
maxclients 10000 #设置能连上redis的最大客户端数量
MEMORY MANAGEMENT 内存限制
maxmemory <bytes> #redis配置 最大内存限制
maxmemory-policy noeviction #内存达到上限的处理策略
redis 6种过期策略的具体方式:
1、volatile-lru:只对设置了过期时间的key进行LRU(默认值)
2、allkeys-lru : 删除lru算法的key
3、volatile-random:随机删除即将过期key
4、allkeys-random:随机删除
5、volatile-ttl : 删除即将过期的
6、noeviction : 永不过期,返回错误
APPEND ONLY MODE aof配置
appendonly no #默认是不开启的,因为默认是使用rdb方式持久化的
#因为在大部分情况下,rdb就够用了
appendfilename "appendonly.aof" #持久化文件的名字
同步策略
# appendfsync always #每次修改都会执行同步。消耗性能
appendfsync everysec #每秒执行一次同步 可能会丢失这1s的数据
# appendfsync no #不执行同步,这个时候操作系统自己同步数据,速度最快
Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以Redis 提供了持久化功能!
什么是RDB
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
Redis会单独创建 ( fork )一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
我们默认的就是rdb,一般不需要修改配置
rdb保存的文件是:dump.rdb 都是在我们的配置文件中快照中进行配置的!
触发机制
1、 save的规则满足的情况下,会自动触发rdb规则
2、执行flushall命令,也会触发我们的rdb规则!
3、退出redis,也会产生rdb文件!
备份就自动生成一个dump.rdb
如何恢复rdb文件
1、只需要将rdb文件放在我们redis启动目录就可以,redis启动的时候会自动检查dump.rdb恢复其中的数据!
2、查看需要存在的位置
127.0.0.1:6379> config get dir
1) "dir"
2) "/etc" #如果在这个目录下存在 dump.rdb文件,启动就会自动恢复其中的数据
优点:
1、适合大规模的数据恢复!dump.rdb
2、对数据的完整性要求不高!
缺点:
1、需要一定的时间间隔进程操作!如果redis意外宕机了,这个最后一次修改数据就没有了!
2、fork进程的时候,会占用一定的内容空间!
将我们的所有命令都记录下来,history,恢复的时候就把这个文件全部在执行一遍
以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
aof保存的是 appendonly.aof 文件
默认是不开启的,我们需要手动进行配置。
我们只需将appendonly 改为yes即可使用 aof
重启,redis就可以生效了
配置:
# appendfsync always #每次修改都会执行同步。消耗性能
appendfsync everysec #每秒执行一次同步 可能会丢失这1s的数据
# appendfsync no #不执行同步,这个时候操作系统自己同步数据,速度最快
如果这个aof文件有错误,这时候redis是启动不起来的,我们需要修改这个aof文件
这个aof文件相当于日志文件
redis提供了一个工具
redis-check-aof
appendonly no #默认是不开启的,我们需要手动进行配置。绝大多数rdb完全够用!
appendfilename "appendonly.aof" #持久化的文件的名字
# appendfsync always #每次修改都会执行同步。消耗性能
appendfsync everysec #每秒执行一次同步 可能会丢失这1s的数据
# appendfsync no #不执行同步,这个时候操作系统自己同步数据,速度最快
重写规则
aof默认的就是文件的无限追加,文件会越来越大!
如果aof文件大于64mb,这个时候因为文件太大了,就fock一个新的进程来讲我们的文件进行重写!
优点和缺点
优点:
1、每一次修改都同步,文件的完整性就更加好!
2、每秒同步一次,可能会丢失一秒的数据
3、从不同步,效率最高的!
缺点:
1、相对于数据文件来说,aof远远大于rdb,修复的速度也比rdb慢
2、aof运行效率也要比rdb慢,所以我们redis默认的配置就是rdb持久化!
扩展:
1、RDB持久化方式能够在指定的时间间隔内对你的数据进行快照存储
2、AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以Redis 协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。
3、只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化
4、同时开启两种持久化方式
5、性能建议
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接受消息。
Redis 客户端可以订阅任意数量的频道
订阅/发布消息图:
下图展示了频道 channel1,以及订阅了这个频道的三个客户端----client2、client5和client1之间的关系:
当有新消息通过PUBLISH命令发送给频道channel1时,这个消息就会被发送给订阅它的三个客户端︰
命令:
这些命令被广泛用于构建即时通信应用,比如网络聊天室(chatroom)和实时广播、实时提醒等。
测试:
#订阅者
127.0.0.1:6379> SUBSCRIBE kundishuo
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "kundishuo"
3) (integer) 1
#实时更新
1) "message" #消息
2) "kundishuo" #哪个频道的消息
3) "hello" #消息内容
1) "message"
2) "kundishuo"
3) "helloredis"
#发布者
127.0.0.1:6379> PUBLISH kundishuo "hello"
(integer) 1
127.0.0.1:6379> PUBLISH kundishuo "helloredis"
(integer) 1
#退订
原理
Redis是使用C实现的,通过分析Redis源码里的pubsub.c文件,了解发布和订阅机制的底层实现,籍此加深对Redis的理解。
Redis 通过PUBLISH、SUBSCRIBE 和PSUBSCRIBE等命令实现发布和订阅功能。
通过SUBSCRIBE命令订阅某频道后,redis-server里维护了一个字典,字典的键就是一个个频道,而字典的值则是一个链表,链表中保存了所有订阅这个channel的客户端。SUBSCRIBE 命令的关键,就是将客户端添加到给定channel的订阅链表中。
通过PUBLISH命令向订阅者发送消息,redis-server会使用给定的频道作为键,在它所维护的channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。
Pub/Sub从字面上理解就是发布( Publish )与订阅(Subscribe ),在Redis中,你可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。
使用场景:
1、实时消息系统!
2、实时聊天!
3、订阅,关注系统都是可以的!
稍微复杂的场景我们就会使用 消息中间件MQ
概念
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master/leader),后者称为从节点(slave/follower);数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave 以读为主。
默认情况下,每台Redis服务器都是主节点;
且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。主从复制的作用主要包括:
1、数据冗余︰主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
2、故障恢复∶当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
3、负载均衡∶在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载﹔尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
4、高可用基石︰除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的,原因如下︰
1、从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大;
2、从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有内存用作Redis存储内存,一般来说,单台Redis最大使用内存不应该超过20G。
电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是"多读少写"。
对于这种场景,我们可以使如下这种架构︰
主从复制,读写分离!80%的情况下都是在进行读操作!减缓服务器的压力!架构中经常使用!一主二从最常用!
只要在公司中,主从复制就是必须要使用的,因为在真实的项目中不可能单机使用Redis !
只配置从库,不用配置主库!!redis服务默认是主库
127.0.0.1:6379> info replication #查看当前库的信息
# Replication
role:master #角色 master
connected_slaves:0 #连接的从机数量
master_failover_state:no-failover
master_replid:aa43875e3348b23afff940d6ebc9f14f75158d05
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
复制3个配置文件,然后修改对应的信息
1、端口号
2、pid 名字
3、log文件名字
4、dump.rdb 的名字
修改完毕之后,启动三个redis服务
**默认情况下,每台Redis服务器都是主节点;**我们只需配置从机
slaveof host port
#从机配置认主
127.0.0.1:6380> slaveof 127.0.0.1 6379
OK
127.0.0.1:6380> info replication
# Replication
role:slave #角色 从机
master_host:127.0.0.1 #主机的host
master_port:6379 #主机的端口号
master_link_status:up
master_last_io_seconds_ago:6
master_sync_in_progress:0
slave_repl_offset:14
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:884d5696f699e4ff0773e2703bf6ebc1cda5668c
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:14
#主机状态
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1 #从机个数为1
slave0:ip=127.0.0.1,port=6380,state=online,offset=84,lag=1
master_failover_state:no-failover
master_replid:884d5696f699e4ff0773e2703bf6ebc1cda5668c
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:84
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:84
如果两个都配置完了,就是有两个从机了。
真实的从主配置应该在配置文件中配置,这样的话才是永久的,通过命令配置的话只是暂时的
修改配置:
replicaof <masterip> <masterport>#在这里修改
细节
主机可以进行写操作,从机不能写只能读!主机中的所有信息和数据,都会被从机保存!
#主机
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set k1 v1
OK
#从机
127.0.0.1:6380> keys *
(empty array)
127.0.0.1:6380> keys *
1) "k1"
127.0.0.1:6380> set k2 v2
(error) READONLY You can't write against a read only replica.
测试:
主机断开连接之后,从机依旧能使用之前主机存入的数据,不能进行写操作;主机重新连接,写入的数据,从机依然可以获取到。
如果是使用我们的命令行来配合的主从,这个时候如果重启从机,从机就变成了主机,如果再配置为从机,我们又可以拿到主机中的数据。
复制原理
Slave启动成功连接到master后会发送一个sync同步命令
Master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。
全量复制︰而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步
但是只要是重新连接master,一次完全同步(全量复制)将被自动执行
我们还可以这样配置
这个时候也可以完成我们的主从复制
如果没有老大了,能不能自己找一个老大?
如果主机断开了,我们可以执行下列命令,使自己变成主机(手动)
执行命令
slaveof no one #自己为主节点
概述
主从切换技术的方法是︰当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel (哨兵)架构来解决这个问题。
谋权篡位的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
这里的哨兵有两个作用
然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
假设主服务器宕机;哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。
测试
目前状态是1主2从
哨兵配置文件
#sentinel monitor 被监控的名称 host port 2
sentinel monitor mymaster 127.0.0.1 6379 2
#后面这个2,表示在执行故障恢复操作前需要多少哨兵节点同意。如果只开启一个哨兵的话,我们需要改为1
启动哨兵
redis-sentinel ../sentinel.conf
如果master节点断开了,这个时候就会从从机中随机选择一个服务器!(这里面有一个投票算法!)
如果主机此时回来了,只能归并到新的主机下,当做从机,这就是哨兵模式的规则!
优点:
1、哨兵集群,基于主从复制模式,所有的主从配置优点,都有
2、主从可以切换,故障可以转移,系统的可用性就会更好
3、哨兵模式就是主从模式的升级,手动到自动,更加健壮!
缺点:
1、Redis不好在线扩容,集群容量一旦达到上限,在线扩容就十分麻烦!
2、实现哨兵模式的配置其实是很麻烦的,里面有很多选择!
Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。
另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。
概念
缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
解决方案
布隆过滤器
布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力;
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IKONRw84-1627294070328)(Redis学习.assets/image-20210726171442566.png)](https://img-blog.csdnimg.cn/287df45f635948eaaa9a5abe5dfb300e.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0lhbl9JTUlM,size_16,color_FFFFFF,t_70)
缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;
但是这种方法会存在两个问题:
1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键
2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
概述
这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。
解决方案
设置热点数据永不过期
从缓存层面来看,没有设置过期时间,所以不会出现热点key过期后产生的问题。
加互斥锁
分布式锁︰使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。
概念
缓存雪崩,是指在某一个时间段,缓存集中过期失效。Redis 宕机 !
产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。
其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。
解决方案
redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。