简介
1、数据库因为数据量的发展历程
优化历程:优化数据结构和索引 -> 文件缓存(IO) -> 添加缓存
大数据时代,一个机器放不下,一个服务器承受不了,数据量、数据的索引、访问量都太大。所以使用了 缓存 + 垂直拆分(读写分离),减轻数据库压力,保证效率
但当数据越来越多,使用了 分库分表 + 水平拆分 + MySQL集群。
但是MySQL这些关系型数据库已经不够用了,数据量大、数据变化快,并且需要在数据库中放图片、文章、视频什么的,传统的关系型数据已经无法满足了,所以需要非关系型数据库来专门处理这些数据。于是关系型数据库需要配合非关系型数据库,使用NoSQL数据库。
在关系型数据库中取数据,就相当于在磁盘中取数据,很慢;而非关系型数据库的数据一般存在缓存中,缓存的读取速度远高于磁盘。
2、关系型数据库与非关系型数据库
2.1、不同的数据使用不同的存储方式
-
关系型数据库:商品的基本信息,比如名称、价格、商家信息这种
-
文档型数据库(MongoDB):商品的描述,比如评论,文字比较多的
-
分布式文件系统(FastDFS、淘宝的TFS、Google的GFS、Hadoop的HDFS):图片
-
搜索引擎(solr、ES、ISearch):商品关键字、搜索关键字
-
内存数据库(Redis、Tair、Memcache)商品热门的波段信息
-
三方应用:商品的交易、外部的支付接口
2.2、NoSQL的特点、与RDBMS的区别
-
方便扩展(数据之间没有关系,很好扩展)
-
大数据量高性能(Redis 一秒写8万次,读 11万次,NoSQL 的缓存是 记录级的,是一种细粒度的缓存,性能比较高)
-
数据类型多样性,并且不需要事先设计数据库,随取随用
2.3、传统的关系型数据库(RDBMS)与NoSQL的区别
RDBMS:Relational Database Management System)
NoSQL + RDBMS 一起使用才是最好的
-
RDBMS: 结构化组织、SQL、数据和关系都存在单独的表中、数据操作语言、数据定义语言、严格的一致性、事务等等
-
NoSQL: 不仅仅是数据、没有固定的查询语言、键值对、列存储、文档存储、图形数据库(社交关系)、最终一致性、CAP定理、 BASE理论 (异地多活,保证服务器不会宕机)、高性能、高可用、易扩展等等
2.4、3V和3高
海量 Volume、多样 Variety、实时 Velocity
高并发、高可拓、高性能
3、什么是NOSQL(Not Only SQL)
-
不遵循 SQL标准
-
不保证关系数据的 ACID特性
-
消除了数据间的关联性
-
高性能、高可扩、高可用,更灵活
3.1、NoSQL的四大分类
KV键值对、列存储数据库、文档型数据库、图关系数据库
3.1.1、KV键值对
Redis、Tair、MemCache 等等
3.1.2、列存储数据库
HBase、分布式文件系统 等等
3.1.3、文档型数据库(BSON数据格式)
BSON格式和 JSON 一样,只不过是二进制的
ConchDB(国外的)、MongoDB 等等
MongoDB 是一个介于关系型数据库和非关系型数据库中间的产品,基于分布式文件存储的数据库,一般存大量的文档,C++编写,是非关系型数据库中最像关系型数据库的。
3.1.4、图关系数据库(存关系的)
放的是关系,比如社交网络、广告推荐
Neo4J、InfoGrid、Infinite Graph
4、Redis简介
Redis 远程字典服务(Remote Dictionary Server)
Redis 是内存数据库,如果没有持久化,那么数据断电即失
关系型数据库:表格,只有行和列,但是像:用户的个人信息、社交网络、地理位置这些,没有固定格式,所以关系型数据库不适合存储这些数据
UDSL(统一数据服务平台):应用层与数据层之间的一层
Redis 是 C语言写的
Redis 可以用作:数据库、缓存、消息中间件
Redis优势:原子性、性能高、数据类型丰富、特性丰富
4.1、Redis 的作用
-
内存存储,用于持久化(RDB、AOF)
-
效率高,可以高速缓存
-
地图信息分析
-
发布订阅信息
-
计数器、计时器 等等
4.2、Redis 的
4.3、下载安装 Redis
4.3.1、Windows下
https://github.com/tporadowski/redis/releases
4.3.2、Linux下
4.3.2.1、虚拟机中安装
# 系统上需要安装有 C语言环境,如果没有则用下面的这个命令进行安装 # Ubuntu系统中,默认并没有提供 C/C++的编译环境,因此还需要手动安装。 # 但是如果单独安装 gcc 以及 g++ 比较麻烦,所以 Ubuntu 提供了 build-essential软件包, # 查看该软件包的依赖关系:apt-cache depends build-essential # apt 与 apt-get 有一些类似的命令选项,但它并不能完全向下兼容 apt-get 命令。 # 也就是说,可以用 apt 替换部分的 apt-get 系列的命令,但不是全部 sudo apt install build-essential # 检测 gcc版本 gcc --version # 下载 redis压缩包 # 可以从 windows 中放进去,也可以自己选一个进行下载。http 和 https 无所谓 wget http://download.redis.io/releases/redis-3.0.7.tar.gz wget https://download.redis.io/releases/redis-6.2.6.tar.gz # 解压 tar -xvf redis-3.0.7.tar.gz # 为了方便升级和管理,这里给 redis 的解压目录新建一个软链接 ln -s redis-3.0.7 redis # 通过软链接进入解压目录下 cd redis # 编译,然后安装 # 如果 make 报错,可以加上参数 # make CFLAGS="-march=x86-64" make sudo make install # 安装成功后,这些可执行文件,会被放到 /usr/local/bin 目录中 # 最好复制一份 /opt/redis-5.0.8/redis.conf 到 同文件夹下自己建的文件夹, # 以后就可以改这个复制品,保证安全。 # 并且以后可以通过这个复制文件启动服务器 # redis 默认不是后台自启的,要修改 vim redis.conf,把 daemonize 的 no 改成 yes。 # 记住修改的是复制品
4.3.2.2、docker 中安装
# 1、拉取镜像 docker pull redis # 2、run 运行镜像产生容器 docker run -di --name briup-redis -p 6379:6379 redis 或 docker run -di --name briup-redis -P redis
4.4、开启 Redis
4.4.1、Windows 中
解压完之后就 ok 了,不需要其他配置
4.4.1.1、打开服务器
-
双击 redis-server.exe 即可
-
Redis 的默认端口号是 6379
4.4.1.2、使用 Redis客户端来连接 Redis服务器
-
双击 redis-cli.exe 即可
-
连上后输入 ping,有 pong 就是连接成功
4.4.2、Linux 中
4.4.2.1、开启服务器
-
直接开启服务器:redis-server
-
指定服务器端口号:redis-server --port 6380
-
通过配置文件启动:redis-server redis/config/redis-6379.conf
-
在 redis目录下 mkdir config,创建 config文件夹
-
将 redis.conf 复制一份到 conf文件夹中 cp redis.conf ./config/
-
可以去掉空格和注释,然后创建一个新的指定端口号的 conf文件,方便区分 cat redis.conf | grep -v "#" | grep -v "^$" > redis-6379.conf
-
可以在 redis文件夹下创建一个 data文件夹,修改 log 的地方,然后修改 conf 的 dir,这样的话,通过这个配置文件启动的服务器的 log日志会放在这个文件夹的 log 指定文件,
-
4.4.2.2、客户端连接服务器
-
连接指定 IP 和端口的 redis服务器:redis-cli [-h 127.0.0.1] -p 6379,默认就是localhost
-
也可以远程连接 Redis:redis-cli.exe -h 192.168.93.132 -p 6379
4.4.2.3、关闭 Redis
在客户端中 shutdown,就可以关闭了。exit 也一样
4.4.3、docker 中
# 创建服务器 docker run -di --name briup-redis -p 6379:6379 redis # 开启服务器 docker start redis # 进入 Redis 并使用 docker exec -it redis /bin/bash # 开启 Redis客户端 redis-cli
5、Redis基本知识
Redis 不区分大小写
5.1、Redis基本命令
可以去 Redis 的中文官网查看命令目录
ps -ef | grep redis //查看关于redis的进程 select 数字 //切换数据库 dbsize //查看当前数据库的 KV 数据个数 keys * //查看当前数据库所有的 key flushall //清空所有数据库的数据 flushdb //清空当前数据库的数据 exists key //判断该 key 是否存在,1 是 true,0 是 false move key 1 //移动指定 key 到指定数据库 expire key 秒数 //设置指定key的过期时间,单位是秒 ttl key //查看指定 key 的过期时间 type key //查看指定 key 的数据类型 randomkey // 随机得到一个 key rename k1 k2 // 给 key 改名, del key // 删除 key save //保存 shutdown //关闭服务器 exit //退出
5.2、redis-benchmark(压力测试工具)
使用方法:redis-benchmark 命令参数
比如:redis-benchmark -h localhsot -p 6379 -c 100 -n 10000
5.1.1、查看分析结果
-
查看 SET 和 GET,查看多少个 parallel clients,代表有多少个用户连接,
-
payload 是每次写入的字节数,
-
completed in 代表每条连接的这么多请求在多少时间内完成的,
-
keep alive 代表有几个服务器
5.3、Redis 默认有 16个数据库
可通过配置文件查看,databases个数
默认使用的是第 0 个数据库,可通过 select 切换数据库,如:select 6
DBSIZE 可以看当前数据库的数据大小
5.4、Redis 是单线程的
redis 是很快的,是基于内存操作的,Redis 将所有数据都放在内存中,CPU不是Redis的性能瓶颈,
Redis 的瓶颈是机器的内存和网络带宽,所有和 CPU 没有关系,所以运行在单线程下。
5.4.1、Redis 为什么单线程还这么快
-
误区1:高性能的服务器不一定都是多线程的!
-
误区2:多线程的效率也不一定比单线程高!因为多线程有 CPU 的上下文切换,会比单线程慢
5.5、Redis.conf
-
配置文件对大小写不敏感,1GB = 1Gb = 1gB = 1gb
-
如果想要本地 redis 可以进行外网连接,先注释掉 bind 127.0.0.1,然后修改模式 protected-mode no
-
包含,即 include /path/to/local.conf 和 /path/to/other.conf
-
网络相关:
-
绑定的IP:bind 127.0.0.1
-
保护模式:protected-mode yes
-
port 6379
-
-
GENERAL通用配置:
-
是否已守护进程方式运行(我们配置成yes):deamonize no
-
如果以后台即守护进程方式运行,需要指定一个pid文件:pidfile /var/run/redis_6379.pid
-
日志级别:loglevel notice 还有:debug、verbose、notice、warning
-
日志的存储文件名:logfile “” 如果是"",就是直接输出
-
数据库个数:database 16
-
是否总是显示logo:always-show-logo yes
-
-
SNAPSHOTTING快照:做持久化用的,在规定时间内,执行了多少次操作,则会持久化到文件 .rdb文件 和 .aof文件
-
save 900 1:若900秒内,若至少有1个key被修改,就进行持久化
-
save 300 10:同上,变数字
-
save 60 10000:同上,变数字
-
持久化若出错,是否继续工作stop-writes-on-bgsave-error yes
-
是否压缩rdb文件,即持久化文件,有时候会浪费CPU资源:rdbcompression yes
-
保存rdb文件的时候进行错误的校验检查:rdbchecksum yes
-
rdb文件保存目录:dir ./
-
-
REPLICATION主从复制
-
SECURITY安全:
-
设置的密码:requirepass 密码
-
可通过config get requirepass 查看密码
-
可通过config set requirepass 密码 查看密码
-
auth 密码 才可登录
-
-
-
CLIENTS客户端:
-
设置客户端最大连接数:maxclients 10000
-
redis配置最大的内存容量:maxmemory 字节大小
-
内存达到上限后的处理策略:maxmemory-policy noeviction
-
-
APPEND ONLY MODE:aof的配置处
-
默认不开启aof模式,即默认rdb方式:appendonly no
-
持久化文件的名字:appendfilename "appendonly.aof"
-
默认是每秒执行一次同步sync,但是可能丢失这1秒的数据
-
6、五大基本数据类型
数据类型是相对于 value 而言的,key 都是名字,
第一次 set k1 v1 是存储,第二次则是覆盖替换了,
命令的开头就代表了是什么数据类型,比如 l,s,h
6.1、字符串 string
默认set key value,就是字符串类型的
value 的类型是 var,即自动的,如果你存的时候是 1,那么又可以当作字符串,又可以当作数字,但是如果你存 "myname" 那么只能是当作字符串了,
可以一次设置多个,也可以一次获取多个,
可以存对象,使用 : 进行板块分隔,比如 set user:info:1:name zs,相当于是在存对象
6.1.1、基础命令
/* 对于 String 的操作 */ // 存值并设定过期时间,ex 是秒,px 是毫秒 set k1 v1 ex 10 // 给一个 k 设定过期时间,单位是秒 expire k 10 // 查看距离过期的时间 ttl key // 将 k 设为不过期 persist key incr key //对指定key的value做 自加1 的操作 incrby key 步长数 //指定步长增长 set keyName value //存入KV键值对数据 get key //通过key获取对应的value append key value2 //对指定key的value进行追加 strlen key //获取指定key的value的字符串长度 // 截取字符串,左闭右闭。如果endIndex是 -1,那就是查看所有。 getrange key startIndex endIndex // 从指定下标开始,替换 replaceValue长度的字符串。 // 比如:abcdef 你 1 xxx 操作,变成 axxxef setrange key startIndex replaceValue decr key //对指定key的value做 自减1 的操作 decrby key 步长数 //指定步长减少 setex key 秒数 value //存储KV值,并且设置其过期时间。即set with expire setnx key value //如果这个key不存在,存储KV值,否则不会替换,就是不存储。即set if not exist mset [k1 v1 k2 v2...] //存储多个KV。 msetnx [k1 v1 k2 v2...] //如果这里面有key已存在,那么所有的 KV 都会设置失败。原子性操作 mget [k1 k2...] //获取多个key的value getset key value2 //先获取k的v,然后替换成新的value。如果key不存在,就创建KV对
6.2、散列 hash
其实就 Map<String, Map<String, String>> 这样一个 HashMap,
不允许嵌套 map,即只能存 String,
6.2.1、基础命令
/* 对于 hash 的操作 */ // 给 hash1 存两个 kv,hashmap 的名称是 hash1, hset hash1 key1 value1 hset hash1 field1 value1 field2 value2 hget hash1 field1 //取 hash1 的 field1字段的value hgetall hash1 //获取所有,field 与 value 都自成一行 hdel hash1 field1 //删除指定的 field hlen hash1 //统计 map 中的 kv元素的个数 hexists hash2 field1 //判断指定 field 是否存在 hkeys hash2 //获取 map 中的所有 key hvals hash2 //获取 map 中的所有值 incr hash1 field1 2 //自增 decr hash1 field1 1 //自减 hsetnx hash1 field3 value //若不存在就创建,存在就不创建,也不会替换 hset user:1 name zs //1就相当于 id,以后可以通配 // 查询该 hash 下所有的key hkeys cart:423808 // 看这个属性具体的值 hget cart:423808 364615
6.3、列表 list
列表实际就是 链表,可以用来做消息队列,一边放一边拿。,存一个值
列表不一定是只能存一个数字或字符串,而是能存一个很大的东西,比如对象,红黑树这些,
列表有两个开口,即既可从左边放入,又可以从右边放入。计数则是从左往右,从 0 开始计数
6.3.1、使用命令
/* 对于 lists 的操作 */ // 阻塞 pop,当有没有数据的时候就会等待有数据了再进行 pop,有指定时间。类似生产者消费者 blpop list1 list2 10 lpush listName value1 value2 //Left Push,从左放入。所以后进入的下标为 0 lrange list1 stratIndex endIndex //获取 key 为 list1 的列表的所有值,-1 是倒数第一个的意思 rpush list1 value3 //Right Push,从右放入。都是对这个列表进行放入 lpop list1 //从左移除,即移除第一个元素 rpop list1 //从右移除,即移除最后一个元素 lindex list1 index //通过下标获取值。l是list的意思,不是left的意思了 llen list1 //获取指定列表的长度 lrem list1 count value //移除这个列表中,count个,值为value的值 ltrim list1 startIndex endIndex //截取,左闭右闭,列表只剩下要的 rpoplpush list1 list2 //右移除list1,然后左放入到list2,list2若不存在,会自动创建 lset list index value //给list的指定下标设置值。若list不存在,会报错 linsert list1 before|after value2 value5 //对目标值的前或后进行插入,before和after你要选一个
6.4、集合 set
set 中的值不能重复,就相当于 HashSet,存一个值
6.4.1、使用命令
/* 对于 set 的操作 */ sadd setName value //给指定的set名字的set集合添加元素 smembers set1 //查看指定 set 的全部元素 sismember set1 member //判断 member 是不是 set集合中的成员 scard set1 //获取 set集合中的个数 srem set1 member1 member2 //移除 set 中的指定元素 srandmember set1 count //随机筛选 count个元素 spop set1 //随机移除元素 smove set1 set2 member //将 set1 中的指定成员移动到 set2 中 sdiff set1 set2 //显示 set1集合与 set2集合的差集,即 set1 - set2 sinter set1 set2 //显示 set1 与 set2 的交集 sunion set1 set2 //显示 set1 与 set2 的并集,set 不可重复,所以会去重 sdiffstore set3 set1 set2 // 将差集的结果存到指定 set
6.5、有序集合 sorted sets(Zset)
在 set 的基础上增加了一个值,score 这个增加的值是为了方便排序,会根据分数自动排序。
6.5.1、使用命令
/* 对于 Zset 的操作 */ zadd set1 score1 member1 score2 member2 //增加 member,score 是其排序标准 zrange set1 start end [withscores] //左闭右闭,还可以展示分数 //根据 score 进行排序,并选择分数在区间内的成员,可以选择展示 score,可以选择从那个下标开始,要几个值 zrangebyscore set1 -inf +inf [withscores] [limit 0 2] zrevrange set 0 -1 //降序 zrem set1 member1 //移除指定元素 zcard set1 //获取集合中的个数 zrank set1 member1 // 展示成员的排名,即下标 zcount set1 start end //计算 score在区间内的个数,左闭右闭
7、三种特殊数据类型
7.1、geospatial地址位置
geo的底层其实就是 Zset,这个功能可以推算地理位置的信息,两地间距离什么的
相关命令:GEOADD、GEODIST、GEOHASH、GEOPOS、GEORADIUS、GEORADIUSBYMEMBER
经度:-180—180度,维度:-85—85
geohash 返回 11个字符的字符串
7.1.1、使用命令
/* 对于 geospatial 的操作 */ geoadd key 经度1 维度1 member1 经度2 维度2 member2 //添加地理位置, //比如:geoadd china:city 精度值 维度值 BeiJing geopos key member1 member2 //获取指定的成员的经纬度 geodist key member1 member2 距离单位 //获取两地间距离,单位可以指定,km georadius key 经度 维度 radius 距离单位 //找出集合中,在指定位置的半径内的地理位置 georadius key 经度 维度 radius km withdist withcoord count num //withdist是直线距离,withcoord是经纬度,统计num个 georadiusbymember key member1 radius 距离单位 //从指定点变成了集合中的成员 geohash key member1 member2 //将二维的经纬度变成11位的hash字符串 zrange key 0 -1 //因为geo的底层是Zset,所有可通过这个命令查看所有的地理位置 zrem key member1 //移除指定地理位置
7.2、hyperloglog
基数:不重复的元素的个数。可以接受误差
hyperloglog 是一种数据结构
优点:占用的内存是固定的。只需要 12KB的内存,但有 0.81%的错误率
hyperloglog 是基数统计的算法,比如网页统计访问人数,传统使用 set集合保存用户id,但是占内存,因为我们并不是为了保存用户id,我们是想要的是基数,即有多少不同用户访问过网页。
使用 hyperloglog 的话必须得是允许容错的,因为有 0.81%的错误率
7.2.1、使用命令
/* 对于 hyperloglog 的操作 */ pfadd key element1 element2... //添加元素 pfcount key //统计基数 pfmerge targetKey key1 key2... //将多个key的 并集 给targetKey,会自动创建targetKey
7.3、bitmap
位存储,bitmap 位图,是一种数据结构,使用二进制来记录,多用在只有两个状态的统计情况,bitmap 的 value 只有 0、1 两种状态
7.3.1、使用命令
/* 对于 bitmap 的操作 */ setbit key index value //设置指定下标的value getbit key index //获取指定下标的value bitcount key [start end] //统计这个key的bitmap中,value为 1 的个数
8、事务
注意:Redis 的单条命令就是保证原子性的,但是 Redis 中的事务是不保证原子性的,并且没有隔离级别的概念,也不能进行回滚。
数据库中的事务是先执行,然后再把结果提交或做回滚;但是 Redis 中的事务是存入队列,等到执行事务时一起执行掉。
Redis 可以实现乐观锁,通过 watch,有版本号,不会发生 ABA问题。
事务是一组命令的集合,事务运行时,里面的所有命令都会被序列化。
Redis事务在运行过程中,顺序执行,排他执行
-
Redis 的事务步骤:开启事务(multi)、命令入队(...)、执行事务(exec)。
8.1、使用事务
discard:取消事务
multi //开启事务 set k1 v1 //下面就是命令队列,让这些命令入队,并没有让它执行,会得到执行命令开始 get k1 //如果数据库里面,并且队列里面都没有 k1 ....... [discard] //废弃事务。此命令和exec只能出现一个,即把事务队列取消掉 exec //执行事务,按顺序执行完所有命令队列
8.2、事务中的异常
编译型异常:如果代码有问题,事务中所有的命令都不会被执行
运行时异常:如果代码存在语法性错误,那么其他命令正常执行,出现异常的命令抛出异常
//编译型异常 multi set k1 v1 getset k1 //这个命令是错误的,会报错 set k2 v2 exec //因为命令队列中有编译型错误,所以执行报错,所有的命令都不会被执行 //运行时异常 multi incr k1 //当前数据库中的k1的值是 tom1,不可能是个数字,所以自加操作是语法性错误 set k2 v3 exec //其他的命令正常执行,发生错误的语句报错
8.3、Redis 中的锁
-
悲观锁:悲观,认定执行过程中一定会出现问题,一开始就上锁
-
乐观锁:乐观,认为执行过程中不一定会有问题,不会一开始就加锁,但会在更新数据的时候进行判断,判断执行期间是否有人改动过。
8.3.1、watch—Redis中的乐观锁
-
watch key1:监视key1。
-
watch 是一次性的,即监视过程中,一旦发现监视的对象被另一个线程修改了,那么就相当于这个监视器的"版本号"发生变化,则线程1 此后所有对监视对象的操作都会失败。所以要unwatch 解除监视后重新监视.
//线程1 //线程2 set money 100 set withdraw 0 wtach money multi decrby money 10 incrby withdraw 10 exec //可以正常执行 multi decrby money 10 set money 1000 incrby withdraw 10 exec //执行失败,watch 就是乐观锁 unwatch watch money //重新监视,将阈值重置,获取最新的"版本号"
9、Jedis
Jedis 是 Redis官方的 Java连接开发工具。类似于 JDBC 一样。即通过 Jedis,使用 Java,操作 Redis,
Redis 都需要序列化,不然会乱码,
在存取对象的时候,可以变成 json,或者序列化变成字节。即在实体类外变成 json,或者让实体类实现序列化类
9.1、准备工作
9.1.1、非 SpringBoot项目引入 jedis、json 的依赖
使用 jackson 和 fastjson 中哪个都可以,虽然 fastjson 更快些,更简洁一些,但是确实相比 jackson,并没有快多少,但是功能没有 jackson 强大,
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.7.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.75</version> </dependency> <!-- 可能会需要 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-jdk14</artifactId> <version>1.7.32</version> </dependency>
9.1.2、SpringBoot项目引入 Redis依赖
starter 已经给我们提供了两个默认的模板类,在 RedisAutoConfiguration类中,一个是 RedisTemplate,另一个是 StringRedisTemplate。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
9.1.3、配置文件
// 在 application.properties 文件下加入以下配置 # Redis数据库索引(默认为0) spring.redis.database=0 # Redis服务器地址 spring.redis.host=127.0.0.1 # Redis服务器连接端口 spring.redis.port=6379 # Redis服务器连接密码(默认为空) spring.redis.password= # 连接池最大连接数(使用负值表示没有限制) spring.redis.jedis.pool.max-active=8 # 连接池最大阻塞等待时间(使用负值表示没有限制) spring.redis.jedis.pool.max-wait=-1 # 连接池中的最大空闲连接 spring.redis.jedis.pool.max-idle=8 # 连接池中的最小空闲连接 spring.redis.jedis.pool.min-idle=0 # 连接超时时间(毫秒) spring.redis.timeout=0
9.2、使用 Redis命令
推荐查看对应的接口文档。
Jedis jedis = new Jedis("localhost", 6379);//也可以再加一个 true,表示使用 ssl验证 System.out.println(jedis.ping()); // 开始使用 jedis 在 Redis 中的命令 jedis.set("a", "111"); jedis.close(); //关闭连接 或者 try(Jedis jedis = new Jedis("localhost", 6379)) { }
9.2.1、使用 Redis事务
JSONObject jsonObject = new JSONObject(); jsonObject.put("hello","world"); jsonObject.put("nice","meeting"); Transaction multi = jedis.multi(); //开启事务 String json = jsonObject.toJSONString(); // jedis.watch(json); try{ multi.set("user1", json); multi.exec(); }catch(Exception e){ multi.discard(); e.printStackTrace(); }finally{ jedis.close(); } jedis.get("user1");
9.3、池化 JedisPool
在不同的线程中使用相同的 Jedis实例会发生并发错误。但是创建太多的 Jedis实例也不好,因为这意味着会建立很多 Socket连接,也会导致不必要的错误发生。
单一 Jedis实例不是线程安全的。为了避免这些问题,可以使用 JedisPool。JedisPool 是一个线程安全的网络连接池。可以用 JedisPool 创建一些可靠的 Jedis实例,可以从池中拿到 Jedis实例然后使用。
9.3.1、创建连接池
// 三种方式 JedisPool pool = new JedisPool(); 或 JedisPool pool = new JedisPool("127.0.0.1",6379); 或 JedisPoolConfig config = new JedisPoolConfig(); //设置最大连接数 config.setMaxTotal(80); //设置最大空闲数 config.setMaxIdle(20); //设置超时时间 config.setMaxWaitMillis(3000); JedisPool pool = new JedisPool(config, "127.0.0.1", 6379);
9.3.2、获取jedis对象
Jedis jedis = pool.getResource(); // 关闭资源 jedis.close();
9.3.3、工具类
public class JedisUtils { public static JedisPool jedisPool; static { //ResourceBundle会查找classpath下的xxx.properties的文件 ResourceBundle resourceBundle = ResourceBundle.getBundle("redis"); int maxTotal = Integer.parseInt(resourceBundle.getString("redis.pool.maxTotal")); int maxIdle = Integer.parseInt(resourceBundle.getString("redis.pool.maxIdle")); int maxWait = Integer.parseInt(resourceBundle.getString("redis.pool.maxWait")); String ip = resourceBundle.getString("redis.ip"); int port = Integer.parseInt(resourceBundle.getString("redis.port")); JedisPoolConfig config = new JedisPoolConfig(); // 设置最大连接数 config.setMaxTotal(maxTotal); // 设置最大空闲数 config.setMaxIdle(maxIdle); // 设置超时时间 config.setMaxWaitMillis(maxWait); // 初始化连接池 jedisPool = new JedisPool(config, ip, port); } }
10、SpringBoot 整合
Redis 都需要序列化,SpringBoot 操作数据,是通过使用 Spring Data,操作 jpa、jdbc、mongodb、redis 就都会使用到 Spring Data。Spring Data 和 Spring Boot是同等级的项目。引入spring-boot-starter-data-redis依赖。
<!-- redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
10.1、Jedis 与 lettuce
SpringBoot2.X 后,Jedis 被替换成了 lettuce
-
Jedis:采用直连,多个线程操作不安全,若想避免不安全,使用 jedis pool 连接池。更像 BIO模式
-
lettuce:采用 netty,实例可以在多个线程中共享,不存在线程不安全的情况。可以减少线程数量,更像 NIO模式。性能更高些
10.2、使用模板类
SpringBoot 的 starter 提供了两个默认的模板 Bean:RedisTemplate、StringRedisTemplate
starter 已经给我们提供了两个默认的模板类,在 RedisAutoConfiguration类中,一个是 RedisTemplate,另一个是 StringRedisTemplate。
redisTemplate 的 opsForValue()方法,代表操作字符串,即 String类型,然后就可以进行字符串的相关操作了,
即 opsForXxx() 方法,是去指定你想操作的数据类型,对于非数据类型的操作,就是直接操作就可以了,
opsForValue、opsForList、opsForSet、opsForHash、opsForGeo、opsForZSet、opsForHyperLoglog....
还可以获取连接,通过连接对象可以进行清空数据库操作
@Resource private RedisTemplate rt; @Test void test1(){ rt.opsForValue().set("name", "123"); rt.opsForValue().get("name"); rt.delete("name"); RedisConnection conection = rt.getConnectionFactory().getConnection(); }
10.3、自定义 RedisTemplate
保存对象需要序列化。这是一种思想,任何地方保存对象都需要序列化,但是使用 JDK 自带的序列化,Redis 中显示会有问题,即乱码,所以需要 RedisTemplate 来配置一下。
10.3.1、配置类
@Configuration @EnableCaching public class RedisConfig { /** * 自定义key规则 * * @return */ @Bean public KeyGenerator keyGenerator() { return new KeyGenerator() { @Override public Object generate(Object target, Method method, Object... params) { StringBuilder sb = new StringBuilder(); sb.append(target.getClass().getName()); sb.append(method.getName()); for (Object obj : params) { sb.append(obj.toString()); } return sb.toString(); } }; } /** * 设置RedisTemplate规则 * * @param redisConnectionFactory * @return */ @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); //解决查询缓存转换异常的问题 ObjectMapper om = new ObjectMapper(); // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常 om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); //序列号key value redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } /** * 设置CacheManager缓存规则 * * @param factory * @return */ @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; } }
10.3.2、做工具类
@AutoWired 来获得我们注入到容器里面的 RedisTemplate,因为这个是做过序列化的,即上面写的,然后通过它来将那些.opsForXxx 方法封装一下,我们以后直接 set就完事了,不需要 ops 了。
10.4、StringRedisTemplate 和 RedisTemplate
SpringBoot 中提供了两个模板类来操作 Redis:StringRedisTemplate、RedisTemplate
具体区别如下:
-
StringRedisTemplate 继承自 RedisTemplate 。
-
两者的数据是不共通的;也就是说 StringRedisTemplate 只能管理 StringRedisTemplate 里面的数据, RedisTemplate 只能管理 RedisTemplate 中的数据。
-
它们采用的序列化策略是不同的两种,一种是 String 的序列化策略,一种是 JDK 的序列化策略。StringRedisTemplate 默认采用的是 String 的序列化策略,RedisTemplate 是 JDK,保存的 key 和 value 都是采用对应的策略序列化保存的。
10.5、封装模板
@Service public class CacheService extends CachingConfigurerSupport { @Autowired private StringRedisTemplate stringRedisTemplate; public CacheService(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } public StringRedisTemplate getstringRedisTemplate() { return this.stringRedisTemplate; } /** -------------------key相关操作--------------------- */ // 删除key public void delete(String key) { stringRedisTemplate.delete(key); } // 批量删除key public void delete(Collection<String> keys) { stringRedisTemplate.delete(keys); } // 这里的方法很多,后面的省去不写,具体的参照代码中的CacheService类 }
10.6、事务操作
由于 Spring 没有专门的 Redis事务管理器,所以只能借用 JDBC 提供的,只不过无所谓,正常情况下反正我们也要用到。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
@Service public class RedisService { @Resource RedisTemplate<Object, Object> template; @PostConstruct public void init() { // 开启事务 template.setEnbaleTransactionSupport(true); // JSON格式的序列化,或者让对象类实现 Serializable接口 template.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class)); } // 添加此注解,使得事务中 @Transactional public void test() { template.multi(); template.opsForValue().set("k1", "123"); template.exec(); } }
11、Redis 的持久化
Redis 是内存数据库,如果不保存到磁盘,断电即失,所以必须持久化。在主从复制中,rdb文件就是用来当备用的,放在从机上面 ,因为不占主机内存,而 aof几乎不使用。
持久化的实现方式有两种:直接保存已有数据、保存所有的命令
11.1、RDB(Redis DataBase)
保存所有的数据
-
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是 Snapshot快照,它恢复时是将快照文件直接读到内存里。
-
Redis 会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。
-
整个过程中,主进程是不进行任何 IO操作的,这确保了极高的性能。
-
如果需要进行大规模的数据恢复,并且对于数据恢复的完整性不是非常敏感,那么 RDB方式比 AOF方式更高效,RDB 的缺点是:在最后一次持久化前如果宕机,那么期间的数据可能丢失。
-
RDB 保存的文件:dump.rdb,可以通过 dbfilename设置
11.1.1、rdb 保存机制触发条件
-
save 的规则满足的情况下,会自动触发 rdb。就是 save 900 1、save 300 10 那些规则
-
退出 Redis,会触发 rdb备份,产生 rdb备份文件
-
在控制台中输入 save命令可直接让服务器生成 rdb文件,而不是子进程,让后台子进程进行备份的命令是 bgsave
11.1.2、使用 rdb文件恢复数据库
-
只需要将rdb 文件放在redis的启动目录就可以了,redis启动的时候会自动检查 dump.rdb 这个文件,然后恢复里面的数据
-
通过 config get dir ,获取rdb的存放目录
11.1.3、rdb 的优缺点
优点:
-
加载速度快
-
数据体积小 缺点:
-
会发生数据丢失。若 Redis 意外宕机了,那么最后几次修改的数据就没了
-
fork 子进程的时候,会占用一定的内存空间,消耗资源多
-
存储速度慢
11.2、AOF(Append Only File)
空间换时间。
-
将我们的所有的命令都记录下来,保存到文件中,恢复的时候,就把这个文件全部执行一遍
-
以日志的形式来记录每个写操作,将 Redis 执行过的所有关于写的指令记录下来,只许追加文件,不可修改文件
-
Redis 刚启动的时候会读取该文件,来重新构建数据,换而言之,Redis 重启的话就会根据日志文件的内容将里面记录的写命令按顺序执行一遍,从而完成数据的恢复工作
-
AOF 默认不开启,可通过 appendonly 开启
-
AOF 保存的是 appendonly.aof 文件,可通过 appendfilename 修改
-
若 aof 文件出现错误,Redis 是无法启动的,可通过 redis-check-aof 来进行修复文件。 redis-check-aof --fix appendonly.aof
-
aof 有三种同步规则;
-
每次写操作都同步 always
-
每秒做同步,但可能丢失 1s 的写操作 everysec
-
不默认做同步 no
-
还有很多规则,可以自行去找该怎么改
-
-
Redis 有 AOF重写机制的优化,会将多条功能类似的命令变成一条,即进行重写,控制台输入 bgrewriteaof 执行
11.2.1、aof 的优缺点
优点:
-
相比于rdb,数据更完整,实时存储
-
存储速度快
-
消耗资源少 缺点:
-
由于操作太多,到最后 aof文件可能变得无比巨大,可以用重写来减少 aof文件大小,bgrewriteaof
-
加载速度慢,服务器每次启动都要进行过程重演,比 rdb 更耗时,aof 的运行效率比 rdb 慢
11.2.2、aof 的重写规则
当满足规则,就会 fork一个新的进程来将我们的文件进行重写
-
百分比大于设定规则 auto-aof-rewrite-percentage
-
如果 aof文件大于 auto-aof-rewrite-min-size
11.3、性能建议
12、Redis发布订阅
-
一般通信,需要队列。发送者 和 订阅者 通过 队列 通信
-
Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息
-
Redis客户端可以订阅任意数量的频道
12.1、使用命令
psubscribe pattern [pattern...] //订阅1个或多个符合给定模式的频道 pubsub subcommand [argument [argument...]] //查看订阅与发布状态 publish channel message //将信息发送到指定频道 punsubscribe [pattern [pattern...]] //退订所有给定模式的频道 subscribe channel [channel...] //订阅所有给定的频道 unsubscribe [channel [channel...]] //退订所有给定的频道
12.1.1、简单测试
-
服务器只是搭建环境,发布者和订阅者都是客户,与服务器无关
//Redis的Client1,订阅者 subscribe channel1 //订阅这个频道,然后就不可以输入命令了,因为在进行频道收听 //Redis的Client2,发送者 publish channel1 "Hello,World!" //向这个频道发送消息,然后订阅者就可以收到了
12.2、原理
-
Redis 是使用 C 实现的,若想了解底层源码,可去分析 Redis源码中的 pubsub.c文件
-
Redis 是通过 publish、subscribe、psubscribe 等命令实现订阅和发布
-
通过subscribe命令订阅某频道后,redis-server 里维护了一个字典,字典的key就是频道,字典的值则是链表,链表中是所有订阅此频道的客户端。所以subscribe 命令,就是将客户端添加到指定频道(channel)的订阅链表中
-
通过publish 向订阅者发送消息,redis-server 则使用给定的频道,使用这个频道去搜索字段的key,找到对应key后遍历这个key的链表,将消息发送给所有的人
12.2.1、使用场景
-
实时消息系统
-
实时聊天(将频道当作聊天室,订阅者向发布者发送消息,发布者群发给所有人)
-
订阅、关注系统
-
稍微复杂的场景,我们会使用 消息中间件MQ
13、Redis 与分布式
13.1、主从复制
13.1.1、主从复制简介
主从复制是指将一台 Redis服务器的数据,复制到其他的 Redis服务器。前者称为主节点(master),后者为从节点(slave),数据的复制是单向的,只能由主节点到从节点,Master 以被写为主,Slave 以被读为主。一个主节点可有多个从节点,一个从节点只能有一个主节点。
在未配置之前,每一台 Redis服务器都可以是主节点。
假如主机宕机了,默认情况下,从机还是从机,想改成主机需要手动修改。如果主机恢复了,主从复制继续正常运行(但是在主机宕机这段时间内,从机一直在等待)。
如果是通过命令行配置的主从关系,那么当从机宕机重启后,它就变成主机了,再次变成主机的从机后,会进行全量复制。但是如果是在配置文件中修改的话,那从机宕机重启之后还会保持主从关系,并且会做增量复制。
13.1.1.1、主从复制的作用
-
读写分离,负载均衡,主节点被写,从节点被读,提高了性能,均衡服务器的负载。尤其是少些多读的情况下,多个从节点可以分担读负载,提高并发性能,并且一个从节点挂掉,还有其他的从节点可以正常地提高服务。
-
数据冗余,即数据备份,主从复制实现了数据的热备份,是持久化之外的另一种数据冗余方式
-
故障恢复,当主节点出错时,可由从节点提供服务,实现快速的故障恢复,是服务的冗余
-
高可用(集群)基石,主从复制是哨兵和集群能够实施的基础
13.1.1.2、数据同步方式和同步流程
-
全量复制:用于初次复制或其它无法进行部分复制的情况,将主节点中的所有数据都发送给从节点。当数据量过大的时候,会造成很大的网络开销。
-
增量复制:用于处理在主从复制中因网络闪退等原因造成的数据丢失等场景,当从节点再次连上主节点,如果条件允许,主节点会补发丢失数据给从节点,因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销。但需要注意,如果网络中断时间过长,造成主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制。 无论是已处于从节点状态还是刚刚启动完成的服务器,都会从主节点同步数据,整个同步流程为:
-
从节点执行 replicaof host port 命令后,从节点会保存主节点相关的地址信息。
-
从节点通过每秒运行的定时任务发现配置了新的主节点后,会尝试与该节点建立网络连接,专门用于接收主节点发送的复制命令。
-
连接成功后,第一次会将主节点的数据进行全量复制,之后采用增量复制,持续将新来的写命令同步给从节点。
-
当我们的主机服务器关闭后,从机会疯狂报错,因为想要连接到主机,然后数据是可以正常访问到的,因为已经备份了。
13.1.1.3、偏移量
使用 info replication 可以看到主机的主从复制信息中有一个 offset,这个就是偏移量。
主机和从机都会维护一个复制偏移量,主机每次向从机中传递 N个字节的时候,会将自己的复制偏移量加上 N。从机中收到主机的 N个字节的数据,就会将自己的复制偏移量加上 N,通过主从机的偏移量对比可以很清楚的知道主从机的数据是否处于一致,如果不一致就需要进行增量同步了。
13.1.2、主从复制实践1 - Windows下
-
复制两份初始的 Redis文件夹
-
修改 redis.windows.conf文件,
# 主节点的端口 port 6001 # 从节点的端口 port 6002
-
在从机的文件夹下 cmd,redis-server.exe redis.windows.conf,启动从机服务器;然后以相同方式启动主机服务器。
-
在从机或者主机的文件夹下 cmd,redis-cli.exe -p 6001,让客户端连接到主机,然后输入 info replication 查看当前服务器的主从复制信息。exit 一下,然后再连接到从节点查看主从复制信息,可以发现默认都是 master。
-
客户端连接到从机中后使用命令 replicaof ip port(老版本是用这个命令 slaveof ip port),replicaof 127.0.0.1 6001,将 6002 变成 6001 的从机。
-
主从关系绑定之后,主节点可写,写入的数据会同步给从机,从机是只读的,不可写入。
-
使用 replicaof no one(老版本是用这个命令 slaveof no one)可以解除主从关系,让从机变回 master。
-
除了在连接了从机的客户端中使用命令绑定主从关系,还可以在配置文件中配置。打开从机文件夹,修改 redis.windows.conf,添加上这句话 replicaof 127.0.0.1 6001。就会在开启服务器之后自动绑定主从关系。
-
并且我们还可以让从机2变成从机1的从节点
13.1.3、主从关系的种类
-
一主多从:即多个从机连接到同一个主机。但当主机宕机后,从机只会报错然后等待连接到主机,浪费资源。并且当从节点变多,主从同步的压力就变大了。
-
链路模式:即主机1的从机是从机1,从机1的从机是从机2,形成一个链路。但一旦中间出现问题,就会导致链路出问题。
13.1.4、主从复制实践2 - docker下
docker-部署 redis 主从同步(一主,两从+哨兵模式)tag:redis:6.2.6_docker容器中 redis 主节点和哨兵同时部署在一个容器中-CSDN博客
13.2、哨兵模式
13.2.1、哨兵模式简介
主从复制关系中,一旦主节点出现问题,那么整个主从系统就无法写入了,除非去手动干预。所以需要哨兵模式(Sentinel),自动维护主从系统,这个和分布式微服务中的哨兵不同。
哨兵是一个独立的进程,它会独立运行,其原理是:哨兵通过发送命令,等待Redis服务器响应,从而监控运行中的多个Redis实例。能够后台监控主机是否故障,若故障了则根据投票数,自动地将从机变成主机。若此后旧主机恢复,旧主机会变成新主机的从机。
哨兵会对所有的节点进行监控,即哨兵按时间间隔发送请求,如果发现主节点出现问题,即在固定时间段内未收到主节点的响应,那么就断定此 Redis服务器出故障了,然后就会通过 发布订阅模式 通知其他从机,然后会立即让从节点进行投票,选举一个新的主节点出来。
但是如果只有一个哨兵,并且哨兵也出故障了,那就无法达到期望,所以需要多哨兵模式,哨兵之间也会相互监督。
13.2.1.1、哨兵模式的优缺点
优点:
-
基于主从复制,所有的主从复制的优点,哨兵模式都有
-
可进行主从切换,故障可以转移,系统高可用
-
从手动到自动,更加健壮 缺点:
-
Redis 不容易在线扩容,集群容量一旦到达上限,在线扩容十分麻烦
13.2.1.2、选举规则
-
首先会根据优先级进行选择,可以在配置文件中进行配置,添加 replica-priority配置项(默认是100),越小表示优先级越高。
-
如果优先级一样,那就选择偏移量最大的
-
要是还选不出来,那就选择 runid(启动时随机生成的)最小的。
13.2.2、哨兵模式实践1 - Windows下
-
如果是云服务器的话,修改每个 Redis服务器文件夹下的 redis.windows.conf文件
# 如果使用的是云服务,把这行代码注释起来 # bing 127.0.0.1 # 去除保护模式 protected-mode no
-
复制一份初始的 Redis文件,修改 redis.windows.conf文件,删去全部内容,然后加入这行代码 sentinel monitor sentinel1 127.0.0.1 6001 1。前两个单词是固定的,第三个是此监控对象的自定义名称,后面就是主节点的 ip port,最后一个 1 代表决定票数,即只要有 x个哨兵投票认为主节点已经挂了,就可以得出主节点已经挂了的结果。
-
修改完后在此文件夹下 cmd,redis-server.exe redis.windows.conf --sentinel,
-
然后把开几个 Redis服务器,形成一主多从,
-
然后再修改哨兵的 redis.windows.conf文件,(如果是云服务的话,需要配置的这个地址是外网的 IP地址,为了方便访问到。)
-
关闭主节点,一开始从节点会正常报错,哨兵会去尝试连接;一段时间后哨兵断定主节点真的挂了,然后就会在从节点中选举新的主节点。
-
可以复制多份哨兵文件夹,比如此处需要 3个哨兵,修改 redis.windows.conf文件,改 port,改决定票数。
-
Java 使用 Jedis 可以对主节点的切换无感:
public class Main { public static void main(String[] args) { //这里我们直接使用JedisSentinelPool来获取Master节点 //需要把三个哨兵的地址都填入 try (JedisSentinelPool pool = new JedisSentinelPool("lbwnb", new HashSet<>(Arrays.asList("192.168.0.12:26741", "192.168.0.12:26740", "192.168.0.12:26739")))) { Jedis jedis = pool.getResource(); //直接询问并得到Jedis对象,这就是连接的Master节点 jedis.set("test", "114514"); //直接写入即可,实际上就是向Master节点写入 Jedis jedis2 = pool.getResource(); //再次获取 System.out.println(jedis2.get("test")); //读取操作 } catch (Exception e) { e.printStackTrace(); } } }
13.2.3、哨兵模式实践2 - Linux下
-
编辑 sentinel.conf,vim sentinel.conf,这里是最基本的配置。哨兵模式有很多配置,推荐自行搜索学习。
-
写入:sentinel monitor 被监控的名称 host port 1
-
host 就是127.0.0.1 ,post 就是监视的端口号,比如 6379,1代表当主机挂掉了,是否启动投票机制,重新选举一个Slave成为主机
-
-
启动哨兵:redis-sentinel myConfig/sentinel.conf
13.3、集群搭建
如果 Redis服务器的内存不够用了,但是现在又需要继续存储内容,那么这个时候就可以利用集群来实现扩容。这时我们就可以让 N台机器上的 Redis 来分别存储各个部分的数据(每个 Redis 可以存储 1/N 的数据量),这样就实现了容量的横向扩展。同时每台 Redis 还可以配一个从节点,这样就可以更好地保证数据的安全性。
13.3.1、计算数据该写到哪个节点
首先,一个 Redis集群一共包含 16384个插槽,集群中的每个 Redis服务器实例负责维护一部分插槽以及插槽所映射的键值数据。
根据公式算出的结果值是多少,就应该存放到对应维护的 Redis 下。比如 Redis节点1 负责 0-2000 的插槽,而这时客户端插入了一个新的数据 a=10,a 在 Hash计算后结果为 666,那么 a 就应该存放到 1号 Redis节点中。所以本质上就是通过哈希算法将插入的数据分摊到各个节点的。
13.3.1.1、插槽
插槽就是键的 Hash计算后的一个结果,(这里出现了计算机网络中的 CRC循环冗余校验),这里采用 CRC16,能得到 16个 bit位的数据,即算出来之后结果是 0-2^16(65535)之间,然后再进行取模,得到最终结果:
-
Redis key 的路由计算公式:slot = CRC16(key) % 16384
13.3.2、集群搭建实践1 - Windows下
-
创建六份 Redis文件夹,就来 3个主节点,然后每个主节点一个从节点。
-
修改每个 Redis服务器文件夹下的 redis.windows.conf文件,
# 如果使用的是云服务,把这行代码注释起来 # bing 127.0.0.1 # 去除保护模式 protected-mode no # 修改端口 port 6001 # 去掉注释,开启集群 cluster-enabled yes
-
在各个文件夹下 cmd,开启 6个服务器;然后再来个 cmd,运行这个命令,--cluster-replicas 1 是指自动为每个节点配置一个从节点,那么一共 6个 Redis服务器,就会自动把前三个作为主节点,后三个分别作为从节点。
redis-cli.exe --cluster create --cluster-replicas 1 127.0.0.1:6001 127.0.0.1:6002 127.0.0.1:6003 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003
运行命令之后,他就会显示分配的插槽情况和主从情况,然后输入 yes 接受结果。
-
运行完命令后,连接到一个客户端 redis-cli.exe -p 6001,然后 set 一个 k:v,如果这个 v 结果 hash计算后的值不在 6001 管的范围内,就会报错,然后提示你该去哪。
-
如果不想产生这种错误,想让它自动帮我们放到对应的插槽中,就以集群方式设置值,在 set命令后加上 -c。
-
如果某个主节点挂掉了,那么就会把它对应的从节点变成主节点。然后如果这个挂掉的主节点重启,它就不再是主节点了,而是会变成从节点。
-
当某一个主从关系全部挂掉了,那么他们负责的插槽区间的数据就无法被插入了。
-
可以使用 cluster nodes命令来查看当前所有节点的信息
-
Java 使用 Jedis 使用集群
public class Main { public static void main(String[] args) { //和客户端一样,随便连一个就行,也可以多写几个,构造方法有很多种可以选择 try(JedisCluster cluster = new JedisCluster(new HostAndPort("192.168.0.8", 6003))){ System.out.println("集群实例数量:"+cluster.getClusterNodes().size()); cluster.set("a", "yyds"); System.out.println(cluster.get("a")); } } }
14、三大缓存问题:缓存穿透、缓存击穿、缓存雪崩
缓存的位置:缓存层放在 MySQL数据库 与 用户数据请求之间,用户所有的读数据请求会先到缓存中查询,若缓存命中,那么就返回数据,但是如果缓存未命中,那么就会去 MySQL数据库查询
Redis缓存的使用,极大的提升了应用程序的性能和效率,尤其是数据查询方面,但同时也带来了一些问题,其中最致命的问题就是 数据的一致性问题,严格来说,数据一致性问题无解,因为如果对数据的一致性要求特别高,那么不能使用缓存了。当然还有其他问题:缓存穿透、缓存雪崩、缓存击穿
14.1、什么是三大缓存问题
14.1.1、缓存穿透
高并发场景下,用户访问一个缓存、数据库中都不存在的数据,那么先是缓存未命中,然后去查数据库,然后数据库中也没找到,这个过程是非常消耗数据库性能的。然后这个时候被恶意攻击了,即明明是不火热的数据,但是缺有大量的访问请求,然后这么多访问请求全部打到了数据库上面,拖垮了数据库,造成了缓存穿透。
问题原因:
-
访问不存在的数据
-
恶意访问
14.1.1.1、推荐的解决方案
-
布隆过滤器
-
缓存空对象(缓存占位符)
-
短缓存
14.1.2、缓存击穿
高并发场景下,一个热点 key 在缓存中,然后它失效了,此时大量的访问请求打到缓存层上,未命中,然后大量访问请求就会打到数据库上,拖垮了数据库,造成了缓存击穿。
14.1.2.1、推荐的解决方案
-
设置热点数据永不过期
-
加互斥锁,加分布式锁
-
唯一 DB请求,共享结果 假如使用热点数据永不过期的方法,如果热点较多,会浪费空间,所以加互斥锁是较好的选择,使用分布式锁,让他去排队,保证对于每个 key 同时只有一个线程去查询后端服务,其他线程未获得分布式锁的权限,则会等待。这个方式将高并发的压力转移到了分布式锁上,所以对分布式锁的压力较大。
14.1.3、缓存雪崩
缓存中肯定有大量设有缓存时间的数据,然后假如设定的过期时间都相同,那么当他们都过期,然后我们再将数据设置到缓存中,这段时间内,所有的请求都是会直接打到 DB 上面的,所以我们应该让缓存数据在一段时间内像排队一样均匀地过期,即加上随机偏差。
缓存集中过期其实不致命,最致命的是缓存服务器某个节点宕机或断网。
14.1.3.1、推荐的解决方案
-
Redis 高可用集群
-
限流降级。在缓存失效后,通过加锁或队列来控制数据库写缓存的线程数量,或者可以停掉一些其他服务,来保证主要服务的正常运行,
-
数据预热。在正式部署之前,我先把可能的数据先预先访问一遍,这样一来,部分可能会被大量访问的数据就会被加载到缓存中,在即将发送大并发访问前,手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均衡分布
14.2、缓存穿透的解决方案
14.2.1、布隆过滤器
使用布隆过滤器能够告诉你,某个数据一定不存在或可能会存在。从而过滤请求。
14.2.1.1、布隆过滤器简介
Bloom Filter 是 1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。主要用于判断一个元素是否在一个集合中。
通常我们会遇到很多要判断一个元素是否在某个集合中的业务场景,一般想到的是将集合中所有元素保存起来,然后通过比较确定。链表、树、散列表(又叫哈希表,Hash table)等等数据结构都是这种思路。但是随着集合中元素的增加,我们需要的存储空间也会呈现线性增长,最终达到瓶颈。同时检索速度也越来越慢,上述三种结构的检索时间复杂度分别为O(n),O(logn),O(1)。
布隆过滤器是一种数据结构,对所有可能查询的参数以 hash 的形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力。
14.2.1.2、hash函数的原理
哈希函数的概念是:将任意大小的输入数据,通过特定的运算函数,转换成特定大小的输出数据,转换后的数据称为哈希值或哈希编码,也叫散列值。
所有散列函数都有如下基本特性:
-
如果两个散列值是不相同的,那么输入一定不同;
-
散列函数具有确定性的结果,具有这种性质的散列函数称为单向散列函数。
-
如果结果相同,那么输入可能相同也可能不同,有一个不同的误差率。
-
散列函数的输入和输出不是唯一对应关系的,这种情况称为“散列碰撞(collision)”
14.2.1.3、布隆过滤器的原理
但是用 hash表存储大数据量时,空间效率还是很低,当只有一个 hash 函数时,还很容易发生哈希碰撞,那就来多个 hash函数,这就是布隆过滤器。
-
数据结构:BloomFilter 是由一个固定大小的二进制向量或者位图(bitmap)和一系列映射函数组成的。在初始状态时,对于长度为 m 的位数组,它的所有位都被置为0,
-
当有变量被加入集合时,通过 K 个映射函数将这个变量映射成位图中的 K 个点,把它们置为 1(假定有两个变量通过 3 个映射函数进行散列)。
-
当查询某个变量的时候,我们只需要看看这些点是不是都是 1 就可以大概率知道集合中有没有它了
-
如果这些点有任何一个 0,则被查询变量一定不在;
-
如果都是 1,则被查询变量可能存在也可能不存在。
-
为什么说是可能存在,而不是一定存在呢?那是因为映射函数本身就是散列函数,散列函数是会有碰撞的。即有一定误差。
-
14.2.1.4、布隆过滤器的误判率和特性
布隆过滤器的误判是指多个不同的输入经过哈希之后在相同的 bit位置了,这样就无法判断究竟是哪个输入产生的,因此误判的根源在于相同的 bit 位被多次映射且置 1。
这种情况也造成了布隆过滤器的删除问题,因为布隆过滤器的每一个 bit 并不是独占的,很有可能多个元素共享了某一位。如果我们直接删除这一位的话,会影响其他的元素。
添加元素:
-
将要添加的元素给 k 个哈希函数
-
得到对应于位数组上的 k 个位置
-
将这k个位置设为 1 查询元素:
-
将要查询的元素给k个哈希函数
-
得到对应于位数组上的k个位置
-
如果k个位置有一个为 0,则肯定不在集合中
-
如果k个位置全部为 1,则可能在集合中 布隆过滤器的特性:
-
一个元素如果判断结果为存在,那么他可能存在也可能不存在;但是判断结果为不存在的时候则一定不存在。
-
布隆过滤器可以添加元素,但是不能删除元素。因为删掉元素会导致误判率增加。即样本库只能增加、修改,但不可删除
14.2.1.5、使用场景和实例
-
比如黑名单拦截,通过已拦截的地址,将他们通过 n个哈希函数,形成一个位图,然后布隆过滤器将样本收集起来成为样本库,样本库只能增改,不可删,一个地址来了,通过 n个哈希函数,如果得到的几乎都是 1,说明是黑名单里面的,然后会有一个误判率,即一个新的地址来了,它不是黑名单里面的,但是得到的 1的个数计算概率,根据误判率,若小于误判率,就把它当成是黑名单里面的。
-
网页 URL 去重、垃圾邮件识别、大集合中重复元素的判断和缓存穿透等问题。
-
布隆过滤器的典型应用有:数据库防止穿库。 Google Bigtable,HBase 和 Cassandra 以及 Postgresql 使用BloomFilter来减少不存在的行或列的磁盘查找。避免代价高昂的磁盘查找会大大提高数据库查询操作的性能。
-
业务场景中判断用户是否阅读过某视频或文章,比如抖音或头条,当然会导致一定的误判,但不会让用户看到重复的内容。
-
缓存宕机、缓存击穿场景,一般判断用户是否在缓存中,如果在则直接返回结果,不在则查询 db,如果来一波冷数据,会导致缓存大量击穿,造成雪崩效应,这时候可以用布隆过滤器当缓存的索引,只有在布隆过滤器中,才去查询缓存,如果没查询到,则穿透到db。如果不在布隆器中,则直接返回。
-
WEB拦截器,如果相同请求则拦截,防止重复被攻击。用户第一次请求,将请求参数放入布隆过滤器中,当第二次请求时,先判断请求参数是否被布隆过滤器命中。可以提高缓存命中率。Squid 网页代理缓存服务器在 cache digests 中就使用了布隆过滤器。Google Chrome浏览器使用了布隆过滤器加速安全浏览服务
-
Venti 文档存储系统也采用布隆过滤器来检测先前存储的数据。
-
SPIN 模型检测器也使用布隆过滤器在大规模验证问题时跟踪可达状态空间
14.2.1.6、常用的布隆过滤器
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>23.0</version> </dependency> public class GuavaBloomFilterDemo { public static void main(String[] args) { //后边两个参数:预计包含的数据量,和允许的误差值 BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 100000, 0.01); for (int i = 0; i < 100000; i++) { bloomFilter.put(i); } System.out.println(bloomFilter.mightContain(1)); System.out.println(bloomFilter.mightContain(2)); System.out.println(bloomFilter.mightContain(3)); System.out.println(bloomFilter.mightContain(100001)); //bloomFilter.writeTo(); } }
-
Redis 中的布隆过滤器插件
// 直接编译进行安装 git clone https://github.com/RedisBloom/RedisBloom.git cd RedisBloom make #编译 会生成一个rebloom.so文件 redis-server --loadmodule /path/to/rebloom.so #运行redis时加载布隆过滤器模块 redis-cli # 启动连接容器中的 redis 客户端验证 // 使用Docker进行安装 docker pull redislabs/rebloom:latest # 拉取镜像 docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest #运行容器 docker exec -it redis-redisbloom bash redis-cli bf.add 添加元素到布隆过滤器 bf.exists 判断元素是否在布隆过滤器 bf.madd 添加多个元素到布隆过滤器,bf.add 只能添加一个 bf.mexists 判断多个元素是否在布隆过滤器 public class RedissonBloomFilterDemo { public static void main(String[] args) { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); RedissonClient redisson = Redisson.create(config); RBloomFilter<String> bloomFilter = redisson.getBloomFilter("user"); // 初始化布隆过滤器,预计统计元素数量为55000000,期望误差率为0.03 bloomFilter.tryInit(55000000L, 0.03); bloomFilter.add("Tom"); bloomFilter.add("Jack"); System.out.println(bloomFilter.count()); //2 System.out.println(bloomFilter.contains("Tom")); //true System.out.println(bloomFilter.contains("Linda")); //false } }
14.2.2、缓存空对象
-
当存储层未命中后,我们就在缓存中加一个空对象,将空对象缓存起来,同时设置一个过期时间,之后再访问这个数据,就可以从缓存中获取了,从而保护了后端数据源,即MySQL这些
14.2.2.1、缓存空对象的缺点
-
因为空值会被缓存起来,则会消耗更多的空间,
-
即使对空值设置了过期时间,还是会存在 缓存层和存储层的数据 会有一段时间,缓存层与存储层的数据不一致,这对于需要保持一致性的业务来说会有些影响
14.3、缓存击穿的解决方案
14.4、缓存雪崩的解决方案
15、简单使用
15.1、实例1
15.1.1、引入依赖
<!-- redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- spring2.X集成redis所需common-pool2--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>
15.1.2、添加 application信息配置
spring: #redis配置 redis: host: 192.168.174.128 port: 6379 database: 0 timeout: 1800000 lettuce: pool: max-active: 20 max-wait: -1 #最大阻塞等待时间(负数表示没限制) max-idle: 5 min-idle: 0
15.1.3、配置 config
/** * Redis配置类 */ @Configuration @EnableCaching public class RedisConfig { /** * 自定义key规则 * * @return */ @Bean public KeyGenerator keyGenerator() { return new KeyGenerator() { @Override public Object generate(Object target, Method method, Object... params) { StringBuilder sb = new StringBuilder(); sb.append(target.getClass().getName()); sb.append(method.getName()); for (Object obj : params) { sb.append(obj.toString()); } return sb.toString(); } }; } /** * 设置RedisTemplate规则 * * @param redisConnectionFactory * @return */ @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); //解决查询缓存转换异常的问题 ObjectMapper om = new ObjectMapper(); // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常 om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); //序列号key value redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } /** * 设置CacheManager缓存规则 * * @param factory * @return */ @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; } }
15.1.4、通过注解进行缓存
使用常用标签 @Cacheable(使用缓存)、@CachePut、@CacheEvict(清空缓存)
在返回数据的 impl 的方法上,加上 @Cacheable注解。keyGenerator 是我们 RedisConfig 中配置的,每调用一次不同参数的该方法,就会在 Redis中添加 key:value
在导入 excel表格的方法上加入 @CacheEvict,使得加入新数据后,清空缓存
@Override @Cacheable(value = "dict", keyGenerator = "keyGenerator") public List<Dict> findChildById(Long id) { QueryWrapper<Dict> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("parent_id", id); List<Dict> dictList = baseMapper.selectList(queryWrapper); dictList.forEach((dict) -> { dict.setHasChildren(hasChildren(dict.getId())); }); return dictList; }
15.1.5、使用 redisTemplate
自动注入后就可以直接使用了。
@Resource private RedisTemplate<String, String> redisTemplate;
15.2、Redis 做 MyBatis 的二级缓存
在学习 Mybatis 的缓存机制吗,我们当时介绍了二级缓存,它是 Mapper级别的缓存,能够作用于所有会话。
但是当时我们提出了一个问题,由于 Mybatis 的默认二级缓存只能是单机的,如果存在多台服务器访问同一个数据库,实际上一级缓存只会在各自的服务器上生效,但是我们希望的是多台服务器都能使用同一个二级缓存,这样就不会造成过多的资源浪费。
所以我们可以将 Redis 作为 Mybatis 的二级缓存,我们只需要手动实现 Mybatis 提供的 Cache接口。
这样以后取一些常用数据时,就会去 Redis 取缓存了。
除了 Mybatis 的二级缓存,还可以去实现 PersistentTokenRepository接口,实现 Token 持久化存储。
15.2.1、引入 Mybatis依赖
<dependency> <groupId>org.mybaits.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.0</version> </dependency>
15.2.2、自定义 Cache
// org.apache.ibatis.cache public class RedisMybatisCache implements Cache { private final String id; private static RedisTemplate<Object, Object> template; // 注意构造方法必须带一个 String类型的参数接收 id public RedisMybatisCache(String id) { this.id = id; } // 初始化时通过配置类将 RedisTemplate 给过来 public static void setTemplate(RedisTemplate<Object, Object> template) { RedisMybatisCache.template = template; } @Override public String getId() { return id; } @Override public void putObject(Object o, Object o1) { // 这里直接向 Redis数据库中丢数据即可,o 就是 Key, o1 就是 Value, 60秒为过期时间 template.opsForValue().set(o, o1, 60, TimeUnit.SECONDS); } @Override public Object getObject(Object o) { // 这里根据 Key 直接从 Redis数据库中获取值即可 return template.opsForValue().get(o); } @Override public Object removeObject(Object o) { // 根据 Key 删除 return template.delete(o); } @Override public void clear() { //由于 template 中没封装清除操作,只能通过 connection 来执行 template.execute((RedisCallback<Void>) connection -> { // 通过 connection对象执行清空操作 connection.flushDb(); return null; }); } @Override // 这里也是使用 connection对象来获取当前的 Key数量 public int getSize() { return template.execute(RedisServerCommands::dbSize).intValue(); } }
15.2.3、Mybatis配置类
@Configuration public class MybatisConfiguration { @Resource RedisTemplate<Object, Object> template; @PostConstruct public void init() { RedisMybatisCache.setTemplate(template); } }
15.2.4、CacheMapper
@Mapper // 指定缓存的实现类 @CacheNamespace(implementation = RedisMybatisCache.class) public interface CacheMapper { @select("select name from student where sid = 1") String getSid(); }
15.2.5、Test
@Autowired CacheMapper cacheMapper; @Test void test1() { cacheMapper.getSid(); }
16、Key 存 : 层级目录,Value 存 json
Redis 中,为了使得 key 能见名知意,使用 : 进行分层,第一层用作文件夹名称,然后之后的每一对 :,都是属性值:值。
比如这个 Redis数据是关于 store商店的,这个商店的编号是 110,这个商店的药房编号是 220,那么他的 redis数据的 key 就是:store:storecode:110:pharmacy:220。然后其 value 存储对应的 json格式数据。