【狂神说Java】Redis最新超详细版教程通俗易懂_哔哩哔哩_bilibili
总体大纲outline
nosql讲解,非关系型数据库
阿里巴巴架构演进
nosql数据模型
Nosql四大分类
CAP
BASE
Redis入门
五大基本类型
- String
- List
- Set
- Hash
- Zset
三种特殊数据类型
- geo
- hyprologlog
- bitmap
Redis配置文件如何读取
Redis持久化
- RDB
- AOF
Redis的一些事务操作
Redis实现订阅发布
- 消息队列
Redis主从复制
- 哨兵模式,现在公司中所有的集群都采用哨兵模式
缓存穿透,以及解决方式
缓存雪崩,以及雪崩方式,微服务一环套一环,可能会导致雪崩
基础API之jedis详情
SpringBoot集成Redis操作
Redis的实践分析
Nosql
大数据时代,一般的数据库无法进行分析处理了Hadoop
单机SQL时代,更多使用静态网页
- 如果数据量过大,一个机器放不下
- 数据量超过300万时一定要创建索引,数据库现在使用B+Tree,一个机器的内存也放不下
- 数据库访问量比较大,一开始是读写混合的,一个服务器也承受不了
出现这三种情况,就需要升级,但是如果没有这么大的数据量,应该从单机开始
Memcached缓存+MySQL+垂直拆分(读写分离)
网站百80%的操作都是在读,减轻数据库的压力可以使用缓存提高效率,缓存使用什么技术都没所谓
- 发展过程,先是优化数据结构和索引–文件缓存(IO操作)–Memcached(当时最热门的技术)
分库分表+水平拆分+M有SQL集群
使用缓存解决了读的问题,用分集群的方式解决读的问题,数据存在不同的库中
早些年,MySAM使用 表锁,读取一行要将整个表锁起来,十分影响效率,高并发下会出现严重问题
后来转为innodb 行锁
慢慢的就开始使用分库分表来解决写的压力,将表拆分,不同的业务使用不同表,存放在不同的数据库中,不同的业务使用单独的数据库,结合微服务,mysql推出了表分区,但是很少公司使用
Mysql的集群已经满足了那个年代的需求
最近的年代
定位也是一种数据,Mysql的关系型数据库已经不够用了,数据量多,变化快,
非关系型数据库
- Json
- Bson
- 图型数据库
放在缓存中一段时间之后再进行持久化的操作,来保证效率和安全
存储很大的文件,会导致数据表很大效率会变低, 如果有一种专门的数据库来处理这种数据Mysql压力就会变得十分小,研究如何处理这些问题
如果数据量变大,要在对数据库进行更改,增加一行是很难的
用户先访问企业防火墙,到负载均衡的主机,到App服务器,到mysql实例,然后是独立功能的服务器
最后总结为什么要用NoSQL
用户的个人信息、社交网络、地理位置、用户自己生产的数据、用户日志等等数据爆发式的生长,
NoSQL=Not Only SQL 不仅仅是SQL
泛指非关系型数据库,Web2.0的诞生,传统的关系型数据库很难对付Web2.0时代,尤其是超大规模的高并发的社区,
NoSQL在大数据环境下发展十分迅速
要存储的数据不是固定格式,以键值对来控制,Map< String , Object >,数据之间没有关系就很好扩展,Java现在要做面向接口编程也是为了解耦,
使用Redis也是为了高性能,官方的数据读取速度每秒11w次,写8w次
数据类型是多样性的,不需要事先设计数据库,不需要设计键值对,随取随用
传统的RDBMS和NoSQL
传统的RDBMS
- 结构化组织
- sql
- 数据和关系都存储在表中 row column
- 数据定义语言
- 严格的一致性
- 基础的事务
- 。。。。
NoSQL
- 不仅仅是数据
- 没有固定查询语言
- 很多的存储方式,列存储、文档存储、图形存储(社交关系)
- 可以不用满足严格一致性,数据是可以有误差的,要保证的是最终一致性
- CAP定理,和BASE理论
- 高性能、高可用、高可扩展
大数据时代的3V和3高
大数据时代的3V,主要是描述问题的
- 海量的Volume
- 多样的Variety
- 实时Velocity
大数据时代的3高,主要是对程序的要求
- 高并发
- 高可拓(随时水平拆分,机器不足,可用扩展来)
- 高性能
真正在公司中实践一定是:NoSQL+RDBMS一起使用才是最好的
阿里巴巴框架演进
第五代架构改进
敏捷开发
极限编程
- 业务快速增长,每天都要上线大量的小需求
- 应用系统日益膨胀,耦合恶化,架构越来越复杂,带来更高的开发成本,如何保持业务开发的敏捷性
开放,提升网站的开放性,吸引第三方开发者加入网站的建设
体验,网站并发压力快速增长,用户对体验提出了更高的要求
使用了各种数据库,这么多种类型的数据库导致数据架构非常复杂,要简化架构,增加一层就行,像是jdbc一样,
商品中的信息存在不同的数据库中
商品的基本信息
名称、价格、商家信息:
- 关系型数据库就可以解决 MySQL / Oracle
- 淘宝早些年就去IOE了,去掉IBM小型机,Oracle数据库,EMC存储设备
商家的描述、评论(文字比较多)
- 文档型数据库,MongoDB,
图片
- 分布式文件系统,FastDFS
- 淘宝自己的TFS
- Google的GFS
- Hadoop HDFS
- 阿里云的 oss
商品的关键字
搜索引擎
- solr
- elasticsearch
- 淘宝用的是 Isearch
商品热门的波段信息
- 内存数据库
- Redis 、Tair 、Memacache
商品的交易,外部支付接口
- 第三方应用
大型互联网应用问题:
- 数据类型太多
- 数据源繁多,经常重构
- 数据要改造,大面积改造
阿里的解决方案
统一数据服务层UDSL,在网站应用集群和底层数据源之间,构建一层代理,统一数据层
模型数据映射
- 实现 业务模型 各属性 与 底层不同类型数据源的模型数据映射
统一的查询和更新API
- 提供了基于业务模型的统一的查询和更新的API,简化网站应用跨不同数据源的开发模式
- 性能优化
设计了一套统一的DSL,提供了统一的增删改查的API,开发速度问题是解决了,但是性能还是问题
网站数据庞大,只能缓存热点数据,解决方案,开发热点缓存平台,提供UDSL作为缓存系统
以上都是NoSQL入门概述
NoSQL的四大分类
- 新浪:Redis
- 美团:Redis+Tair
- 阿里,百度:Redis+memecache
文档型数据库(Bson格式和Json格式)
- MongoDB
- MongoDB是一个基于分布式文件存储的数据库,C++编写,用来处理大量的文档
- MongoDB是一个介于关系型数据库和非关系型中间的产品,非关系型数据库中功能最丰富的,最像关系型数据库的
- ConthDB
列存储数据库
- HBase
- 分布式文件系统
图形关系数据库
- 不是用来存放图形的,是用来寸关系的,朋友圈社交,广告推荐
- Neo4j,InfoGrid
Redis入门
Redis (Remote Dictionary Server),远程字典服务
开源、使用C语言编写,支持网络、基于内存可持久化的日志型,Key-Value数据库,提供多种语言的API,可以用多种语言调用 ,NoSQL技术之一,也被称之为结构化数据库之一
读的速度是11w,写的速度是8w
Redis能干嘛
- 内存存储,持久化,内存是断电即失的,持久化很重要, 持久化有两种机制(RBD,AOF)
- 效率高,可以用于高速缓存
- 发布订阅系统
- 地图信息分析
- 计数器,(浏览量)
- 。。。
特性
- 多样的数据类型
- 持久化
- 集群
- 事务
- 。。
- Redis官网 https://redis.io
- Redis中文网https://www.redis.net.cn
windows启动Redis
-
安装Redis
brew install redis
//安装redis
brew info redis
//查看软件详细信息,以来关系,注意事项等
brew list redis
安装包所在的位置
-
启动Redis
redis-server
-
启动之后不要关闭
-
连接测试
redis-cli
-
默认端口是6379
-
基本操作
-
Redis推荐使用Linux开发
-
好吧这里加入了Linux的知识,要开始学习Linux
-
退出Redis
redis-cli shutdown
CentOS7 安装Redis
常用的命令
netstat -lnpt |grep 6379
//查看6379=端口占用情况,netstat CentOS不自带,需要另外安装
yum install -y net-tools
kill -9 [PID]
//结束对应PID进程
ln -s /usr/local/redis/bin/redis-cli /usr/bin/redis
//创建链接,使之可以直接使用/bin之中的命令,这创建应该回到~目录进行创建,称为命令软链接
cp /usr/local/redis-5.0.3/redis.conf /usr/local/redis/bin/
//复制操作
make install PREFIX=/usr/local/redis
//安装到指定目录
tar -zxvf redis-5.0.3.tar.gz
//解压
wget http://download.redis.io/releases/redis-5.0.3.tar.gz
//通过链接下载
主要操作
不同版本的redis有不同的操作,选择高版本的redis,基本就只是解压,安装,选择配置文件启动
make install
make uninstall
Linux启动
退出
Redis测试性能
在/usr/local/bin 下有很多工具
benchmark压力测试工具,官方自带的性能测试工具
- -h 指定服务器主机名
- -p 指定服务器端口
- -c 指定并发连接数
- -n 指定请求数
测试100个并发,每个并发100000个请求
redis-benchmark -p 6379 -c 100 -n 10000
不加-h 就默认本机
解读测试数据
- 10000个set请求使用0.17秒
- 每次请求都有100个并行的客户端
- 每次写入三个字符串
- 只有一台服务器来连接
- 下面的是每毫秒处理百分之多少的请求,
- 每秒能处理五万多次请求
基础知识
Redis是单线程的,Redis是基于内存操作的,CPU不是Redis的瓶颈,根据机器的内存和网络带宽的,可以用单线程实现
每秒10w+的QPS,完全不比使用key-value的Memecache差
单线程Redis
- 误区1,高性能服务器一定是多线程
- 误区2,多线程一定比单线程效率高
- 多线程要涉及CPU的上下文切换
- 核心:Redis是将所有数据全部放到内存中去操作,效率就是高,CPU上下文切换是耗时的操作,对于系统来说没有上下文切换,系统效率就是最高的,多次读写都是在一个cpu上的,在内存情况下效率就是最高的
Redis五大数据类型
- String
- List
- Set
- Hash
- Zset
Redis官方介绍
Redis是内存中的数据结构存储系统,可以作用数据库、缓存、消息中间MQ
支持多种数据类型
- String字符串
- hash散列
- list列表
- set 集合
- sorted sets有序集合
- bimemaps
- hyperloglog
- geospatial
Redis内置了主从复制replication、LUAscripting脚本,LRU 驱动事件,transactions事务,和不同级别的磁盘持久化persistence
通过Redis哨兵Sentinel,和自动分区Cluster,提供高可用性high availability
Redis
redis-server &
#后台启动
redis-server /etc/local/bin/redis.conf
#指定文件启动
-p
#指定端口启动
- keys * 查询全部key
- select 3 切换数据库3
- dbsize 查看数据库大小
- flushdb 清空当前库
- flushall 清空所有数据
- exists 判断某个key是否存在 EXISTS name
- move 移除key move name 1
- expire 设置过期时间,可以用作单点登录 expire name 10 10秒后过期
- ttl 查看过期时间 EXISTS name
- type 查看key的类型 type name
- config set requirepass XXX 设置密码
- config set requirepass “” 取消密码
- auth xxx 登录
String
append 追加value
追加不存在的key会set key
strlen 查看value长度
incr xc 自增1,可以用作增加浏览量increase
decr xc 递减1
incrby 能设置自增量的自增 incrby xc 12 decrby xc 12
getrange 截取范围,下标从0开始 getrange xc 0 2 取xc第一到第三的数 getrange xc 0 -1 全部
setrange 修改范围的值 setrange xc 1 22 123456变122456
setex (set wth expire) 设置key时顺便设置生存时间 setex xc aaa 30
setnx ( set if not exist )参数不存在就设置,在分布式锁经常使用,如果存在就创建失败.
setex xc aa
mset 批量设置,空格间隔 mset xc1 1 xc2 2 xc3 3
mget mget xc1 xc2 xc3
msetnx 原子性操作,批量设置,要么都成功或失败,nx如果存在就创建失败
对象操作
设置一个对象,值为json字符串来保存一个对象
user:{id}:{filed}
getset 先get再set,规则就按getset来,无论key存不存在,都按getset来
数据结构是相同的
String类似的使用场景:value除了是外面的字符串还可以是我们的数字
- 计数器
- 统计多单位的数量
- 粉丝数
- 对象缓存存储
List
在Redis中可以将List作为、栈、队列、阻塞队列
- 实际上是一个链表,left、right 都可以插入值
- 如果key不存在,创建新的链表
- 如果key存在,新增value
- 移除所有值,空链表
- 在两边插入效率最高,对中间元素操作,效率会降低
消息排队、消息队列、堆、栈
- lpush left push
- lrange 显示list 范围range
- rpush right push
- Rpop right pop
- Lpop left pop
- Llen xc 返回xc集合(list)的长度
- lindex list 0 获取list中第一给值
- lrem remove指定下标
- ltrim list截取保留指定数据
- Rpoplpush pop再push到别的list
- lset 设置指定下表值
- exists 存在
- Linsert 将某个具体的value插入到某个元素的前面或后面
Set
集合中的值不能重复,set是无需不重复原则
string和list的元素都是value,set中是member
- sadd sadd myset xc像myset中插入xc
- smembers 查看集合的全部值
- sismember 判断是否存在 SISMEMBER myset xc 判断xc是否存在
- scard 获取集合元素个数 scard myset
- srem 移除某一个member srem myset myxc 移除myset集合指定myxc
- srandmember 随机抽取几个member (srandmember myset)srandmember myset 2
- spop 随机删除一个数据 spop myset
- smove myset mysets xc2 将myset集合里的xc2移动到mysets里
- sdiff difference set 差集 ,结果来自first_key为基准
- sinter intersection set 交集
- sunion union联合 并集
- 将用户放到set中,共同关注、共同爱好、推荐好友
Hash
map集合,key - map 本质和string类型没有太大区别,还是一个简单的key - value,像是增加了一层
- hset hset key field value 存值
- hget 取值
- hmset 存多个数据
- hmget 取多个数据
- hgetall 查全部数据
- Hdel 删除 hdel myhash xc 删除myhash里的xc数据
- hlen hlen myhash 获取myhash字段长度
- hexists 判断hash中指定字段是否存在 hexists myhash xc
- hkeys 获取key所有的field
- hvals 获取所有的value
- hincrby 递增,参数设置为负数就是递减
- hsetnx 是否存在,不存在就创建设置,存在就不设置
-
做用户信息保存,比用string类型好点,存储经常变动的信息,hash更适合存储对象,
ZSet(有序集合)
在set的基础上增加了一个值
可以作为一个排行榜功能,进场刷新,或者任务等级排序之类的,都可以做
- 排序
- zrangebyscore
- zcard salary 获取salary集合的个数
- zconut zcount myset 1 3 获取myset1-3区间的数量
三种特殊数据类型
- geospatial
- hyperloglog
- bitmaps
Geospatial
可以推算地理位置的信息,两地之间的距离,方圆几公里之内的人
需要注意:
- 地球南北两极无法直接添加,
- 一般会下载城市数据,直接通过Java程序一次性导入
- 有效经纬度范围从-180到180,超出范围时会返回错误,
- key由(纬度,经度,名称)构成
查看官网,一共有六个相关命令
- add、dist、hash、pos、radius、rediusbymember
- geodist 返回两个给定位置之间的距离
- geohash 返回geohash对位置进行的编码,用于内部调试,一般用不到
- geopos 返回指定member的经纬度信息
- georadius : 根据半径查找,需要给定中心点数据
- georadiusbymember : 也是根据半径查找,但是中心点是已经存在的member
- zrange 遍历member
- zrem 移除member
-
纬度经度,member名称,geoadd可以一次添加多个
-
可以使用geopos读取地理位置
-
geodist,输出两地距离,加上unit单位,设置输出距离单位
-
m、km、mi 英里、ft 英尺
-
-
我附近的人功能,通过半径来查询,获取附近的人的定位地址
-
georadius命令来实现,longitude纬度、latitude经度、radius半径 单位,输入查询位置的经纬度就能从key集合中找到在半径范围的元素
-
后面跟了几个参数,withdist、withcoord、withhash、asc、desc、count
-
Withdist:返回元素位置的同时,把与中心之间相差的距离一同返回
-
withcoord : 将元素经纬度一同返回
- withhash : 返回经过geohash编码的有序集合分值,主要用于底层调试,作用不大
- asc : 根据中心的位置,从近到远的方式返回位置元素
- desc:从远到近的方式返回元素
- count :获取前n个匹配元素,对于提高效率是有效的
-
HyperLogLog
是一种概率数据结构,计数唯一事物,从技术上讲估计一个集合的基数,通常计数唯一项需要使用成比例的内存,因为需要记录使用过的元素,以免多次记录,但是hyperloglog的算法可以用内存换精度,虽然有误差,但是误差小于1%,算法的神奇之处在于只需要很小的内存,最大也不超过12k,类似集合的功能,能记录2^64的计数,从内存的角度来说,hyperloglog是首选
能用在网页的UV
- 传统方式,set保存用户的id,用户可能是uuid,这样可能占用巨大的内存
- 但是只是需要计数功能并不需要保存用户的id
- pfadd
- pfcount 计数
- pfmarge 合并 //合并到第一个key
Bitmaps
统计用户信息,活跃与不活跃,登录未登录只有两种状态的数据,可以使用BitMaps
可以用作打卡功能实现,到达一定数目之后进行统计,判断预期数目与统计得出的数目是否达到预期
-
setbit 中的offset是偏移量,可以看作下标,value只能是0或1
-
getbit
-
bitcount 统计key offset 为1的个数 bitcount sign
-
bitpos 查看 key offset 为0或1的位置,并且可以设置range
-
bitop 对一个或多个保存二进制位的字符串key进行位元操作,将结果保存在deskkey上
-
and、or、not、xor
-
除了not之外,其他都能加入多个key进行运算
-
事务transition
Redis事务的本质是一组命令的集合,一个事务中所有命令都被序列化,在事务执行过程中,会按照顺序执行!
一次性、顺序性、排他性,一次执行多个指令,其他客户端提交的命令请求不会插入到事务执行命令序列中
单条命令是原子性执行的,但事务不保证原子性,且没有回滚,事务中任意命令执行失败,其余命令仍会被执行
- 开启事务(multi)
- 命令入队(。。。)
- 执行事务(exec)
- 取消事务(discard)
- 监视、加锁(watch)
- 取消监视、解锁(unwatch)
锁
乐观锁应该适用于读多写少的情况,悲观锁应该适用于写多读少的情况
悲观锁
- 为了避免其他人同时修改,直接对数据进行加锁以防止并发,修改数据前锁定,修改的方式被称为悲观并发控制 Pessimistic Concurrency Control 悲观、并发、控制
- 独占性、排他性
- 在整个数据处理过程中,数据处于锁定状态
- 线程操作数据,对数据添加排他锁
- 假设最坏的情况,每次都默认其他线程更改数据
- 传统数据库使用的几种加锁机制,都是在操作之前上锁
- 行锁
- 表锁
- 读锁
- 写锁
- 在Java中同步用synchronized关键字实现
- 悲观锁住要分共享锁和排他锁
- 共享锁 shared locks
- 读锁、S锁,多个事务对同一个数据可以共享一把锁,都能访问数据,只能读不能修改
- 排他锁 exclusive locks
- 写锁、X锁、不能与其他锁并存,一个事务获取了数据行的排他锁,其他事物就不能再获得该行的其他锁,获取排他锁的事物可以对数据行读取和修改
- 共享锁 shared locks
- 悲观并发控制 先取锁再访问 的保守策略,开销大,还增加死锁的概率,降低并发性,其他事务必须等待
Jedis
官方推荐的操作Redis的中间件,SpringBoot已经有整合RedisTemplate
- 导入包
- 建立连接
- 操作
- 可关闭连接,或者直接就使用jedis连接池
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
测试
package com.xc;
import com.alibaba.fastjson.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
/**
* 星晨
*/
public class TestPing {
public static void main(String[] args) {
//1.new Jedis对象即可
Jedis jedis = new Jedis("127.0.0.1",6379);
System.out.println(jedis.ping());//连接成功打印PONG
jedis.flushDB();//清空当前数据
JSONObject jsonObject = new JSONObject();
jsonObject.put("hello","world");
jsonObject.put("name","xingchen");
//开启事务
Transaction multi = jedis.multi();
String result = jsonObject.toJSONString();
try {
multi.set("user1",result);
multi.set("user2",result);
// int i = 1/0;//代码抛出异常事务,执行失败!
multi.exec();//执行事务
} catch (Exception e){
multi.discard();//放弃事务
e.printStackTrace();
} finally {
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
jedis.close();//关闭连接
}
}
}
SpringBoot整合
SpringBoot操作数据:是封装在Spring-data中的,jpa、jdbc、mongodb、redis
在SpringBoot2.x以后与原来使用的jedis被替换成来看lettuce,底层已经不使用jedis了
- jedis:采用的直连,多个线程操作的话,不安全,要提高安全性要使用jedis pool连接池 BIO
- lettuce:采用netty,高性能网络框架,异步请求,实例在多线程中可以共享,不存在线程不安全的情况,dubbo底层也是用netty,可以减少线程数量,更像NIO模式
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
原理讲解
SpringBoot所有配置类,都会有一个自动配置类
自动配置类都会绑定一个properties配置文件
RedisAutoConfiguation
启动配置类中有一个RedisProperties配置类
里面有很多以前缀spring.redis开头的配置,可以在application中配置
如host、password、、配置
RedisAutoConfiguation中封装了两个Bean
- RedisTemplate
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
-
没有过多的设置,Redis的对象都是需要序列化的
-
两个泛型都是object,后面使用需要强制转换
-
靠自己重写config来替换这个template
-
StringRedisTamplate
- 大部分情况下String类型是最常用的,就会多一个stringRedisTemplate
@ConditionalOnMissingBean(name = "redisTemplate")
//重写一个redisTemplate就能替换掉这个bean
整合实现
- 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置连接
# 配置Redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
- 测试
package com.xc;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;
@SpringBootTest
class Redis02SpringBootApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
//redisTemplate 操作不同的数据类型,api和我们的指令是一样的
//opsForValue 操作字符串 类似String
//opsForList 操作List 类似List
//opsForSet
//opsForHash
//opsForZSet
//opsForGeo
//opsForHyperLogLog
//除了基本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务和基本的CRUD
//获取redis的连接对象
// RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
// connection.flushDb();
// connection.flushAll();
redisTemplate.opsForValue().set("mykey","xingchen");
System.out.println(redisTemplate.opsForValue().get("mykey"));
}
}
Redis工具类
package com.xc.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public final class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time, TimeUnit timeUnit) {
try {
if (time > 0) {
redisTemplate.expire(key, time, timeUnit);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time, TimeUnit timeUnit) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time, timeUnit);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time, TimeUnit timeUnit) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time, timeUnit);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, TimeUnit timeUnit, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time, timeUnit);
}
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
*/
public boolean lSet(String key, Object value, long time,TimeUnit timeUnit) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time,timeUnit);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time,TimeUnit timeUnit) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) {
expire(key, time,timeUnit);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================HyperLogLog=================================
public long pfadd(String key, String value) {
return redisTemplate.opsForHyperLogLog().add(key, value);
}
public long pfcount(String key) {
return redisTemplate.opsForHyperLogLog().size(key);
}
public void pfremove(String key) {
redisTemplate.opsForHyperLogLog().delete(key);
}
public void pfmerge(String key1, String key2) {
redisTemplate.opsForHyperLogLog().union(key1, key2);
}
}
Redis.conf详解
Redis持久化
持久化RDB、AOF,重点
Redis是内存数据库,断电即失去,只要是内存数据库就一定会有持久化操作
RDB(Redis DataBase)
在指定的时间 间隔内将内存中的数据集快照写入到磁盘中,Snapshot快照,恢复时将快照文件直接读到内存中
- 单独创建一个子进程,fork分支
- 将内存内容写入临时RDB文件
- 再用临时文件替换上次持久化完成的文件
整个过程主进程不进行任何io操作,保证了性能,如果进行大规模数据恢复,RDB和AOP都可以进行数据恢复,RDB数据恢复完整性不敏感,RDB更加高效,缺点时最后一次持久化后的数据可能丢失,默认使用的就是RDB,一般情况不需要修改这个配置
RDB保存的文件是dump.rdb
AOF保存的文件是appendonly.aof
配置快照在snapshots配置区域下
dump.rdb文件
-
通过Redis config get dir 获取
config get dir
-
触发机制
- save规则触发
- 执行flushall命令
- 关闭redis
-
备份会自动生成dump.rdb文件
如何恢复备份文件
只要将rdb文件放在redis规定的目录,redis启动时会自动检查dump.rdb文件恢复数据
查看位置,config get dir
在生产环境中最好对dump.rdb文件进行备份
RDB优缺点
优点:
- 父进程正常处理用户请求,fork分支一个子进程进行备份
- 适合大规模的数据恢复,如果服务器宕机了,不要删除rdb文件,重启自然在目录下,自动会读取
缺点:
- 需要一定的时间间隔,可以自行修改设置
- 如果redis意外宕机,最后一次的修改数据会丢失
- fork进程的时候,会占用一定的内存空间
AOF(Append Only File)
将所执行的所有命令都记录下来,处读操作以外,恢复时重新执行一次,如果是大数据就需要写很久
aof默认是文件无限追加,大小会不断扩张
在主从复制中,rdb是备用的,在从机上使用,aof一般不使用
- fork分支出子进程
- 根据内存中的数据子进程创建临时aof文件
- 父进程执行的命令存放在缓存中,并且写入原aof文件
- 子进程完成新aof文件通知父进程
- 父进程将缓存中的命令写入临时文件
- 父进程用临时文件替换旧aof文件并重命名
- 后面的命令都追加到新的aof文件中
开启AOF
配置文件Append Only Modo区块中设置
appendonly no
#默认关闭appendonly 手动设置yes开启
appendfilename "appendonly.aof"
#默认名字
# appendfsync always
appendfsync everysec
# appendfsync no
#每次都进行修改
#每秒钟都进行修改
#不进行修改
no-appendfsync-on-rewrite no
#是否进行重写
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
#percentage重写百分比
#重写时文件最小的体积
#一般保持默认,一般只需要开启
这些配置也能在连接redis后在redis中通过config set 进行更改
与RDB类似的触发机制,也能生成配置文件
进行了一些操作,如list在同一个key上覆盖值操作,aof是一同操作的,把之前的值进行了覆盖,但是保存的并不是最新的值,而是把全部进行的操作保存了下来,lpush lpop,当从aof文件中恢复数据时,不管最新的值是什么都重新的进行一遍操作,这样在时间上和效率上并不是最优的,但是能保证在每次的操作能进行备份,保证数据不丢失,如果出于绝对的安全考虑可以开启aof
aof文件损坏情况
-
人为测试aof文件损坏,aof文件是根据文件的大小进行比对,判断文件是否损坏,使用
-
haoyun@HAOYUN ~ % redis-check-aof --fix /usr/local/var/db/redis/appendonly.aof AOF analyzed: size=23, ok_up_to=23, diff=0 AOF is valid
-
损坏的aof会导致redis无法打开
-
这个修复真垃圾,给我数据删没了,删除规律数据不好修复,但是加入明显没有逻辑的错误,还是能修复
-
redis-check-rdb 能修复rdb文件
优缺点
优点:
- 可设置文件修改每次都同步备份,文件完整性更好,但是消耗性能
- 设置每秒同步一次可能会丢失一秒的数据
- 从不同步效率最高
缺点
- 对于数据文件,aof远远大于rdb,修复速度也比rdb慢
- aof是io操作,所以默认是aof
- aof文件会无限扩大
扩展
-
rdb持久化方式能够在指定的时间间隔内对数据进行快照存储
-
aof持久化方式记录每次对服务器写的操作,服务器重启时,重新执行命令来恢复原始数据,追加在文件末尾,能对aof文件进行重写,避免体积过大
-
如果只做缓存不需要使用任何持久化
-
同时开启两种持计划方式
-
- 重启时优先载入aof文件来恢复数据
- 只会找aof文件,但是推荐只使用rdb用于备份,能快速重启,并且不会有aof可能潜在的bug
-
性能建议
-
rdb文件只做后背用途,建议只在slave上持久化rdb文件,15分钟备份一次,使用save 900 1 规则
-
使用aof,即便在最恶劣的环境下也不会丢失超过2秒的数据
- 代价:持续的io
- rewrite 过程中产生的新数据写到新文件造成的阻塞不可避免,尽量减少rewrite的频率
- 不使用aof,也可以通过Master-Slave Replication 实现高可用性也可以,能省去一大笔io,减少rewrite带来的系统波动
- 代价:如果Master-Slave 同时倒掉,回丢失十几分钟的数据,启动脚本也要比较Master-Slave中的rdb文件,选择最新的文件,载入新的,微博就是这种架构
-
Redis订阅发布
Redis主从复制
一个Master有多个slave,将一台redis服务器数据,复制到其他的redis服务器,前者称为主节点(masterleader),后者称为从节点(slave、follower),数据是单向的,只能从主节点到从节点,Master以写为主,Slave以读为主
默认情况下,每台redis服务器都是主节点,一个Master可以有多少Slave或没有从节点,一个从节点只能有一个主节点
主从复制作用包括:
- 数据冗余
- 实现了数据的热备份,是持久化之外的一种数据冗余方式
- 故障恢复
- 主节点出现问题,从节点可以提供服务,实现快速的故障恢复,实际上是一种服务的冗余
- 负载均衡
- 在主从复制的基础上,配合读写分离,主节点提供写服务,从节点提供读服务,写redis数据时连接主节点,读redis数据连接从节点,分担服务器负载,尤其在写少读多的场景下通过,多个从节点分担负载,可以提高redis性能
- 高可用(集群)基石
- 哨兵、集群,能够实施的基础,主从复制时高可用的基础
- 哨兵、集群,能够实施的基础,主从复制时高可用的基础
不能只使用一台redis的原因:
- 从结构上讲,单个redis服务器会发生单点故障,一台服务器需要处理所有请求,压力大
- 从容量上讲,单个redis服务器内存容量有限,并且不能完全使用全部的内存,单台redis的最大内存不应该超过20g压力过大
通常的电商网站都是一次上传吗,无数次浏览,读多写少 ,主从复制,读写分离,80%的情况都在进行读操作,起码一主二从
需要配置的config选项
- daemonize
- port
- pidfile
- logfile
- dbfilename
- rdb-del-sync-files
Redis replication实现
查看当前服务
ps -ef|grep redis
一主二从
默认情况下,每台Redis服务器都是主节点;我们一般情况下只用配置从机就好了
认老大,一主(6379)、二从(6380、6381)
-
真实的主从配置要在配置文件中配置,在redis-cli中配置的是暂时的
-
配置在redis.conf文件中replication区块下
replicaof <masterip> <masterport>
replicaof 127.0.0.1 6379
-
配置文件设置好,启动时就不用重新设置
-
根据读写分离的原则,主机只能写,从机只能读
-
slave 会自动write master中的数据,但是不能往slave中写数据
哨兵模式
当master宕机时让slave变为master
slaveof no one
#让自己变为主机
这种设置是手动的,使用哨兵模式将自动选取master
此时master恢复后使用slaveof no one 的主机也还会继续当master,要重新作为slave只能重新配置
单哨兵模式、多哨兵模式
概述切换技术的方法是,当master服务器宕机后,需要人工切换,费事,更多时候选择优先考虑是哨兵模式,redis2.8 开始正确提供sentinel(哨兵 )
能够监控后台的主机是否故障,根据投票自动将从库专为主库
哨兵模式是一种特殊模式,哨兵是一个独立的进程,作为进程独立运行,原理是哨兵通过发送命令,等待redis服务器响应,从而监控多个redis实例
像每台发送信息确定主机是否存活,优点类似于springcloud的心跳检测
这种图成为单机哨兵,当单个哨兵也宕机也会有风险,创建多个哨兵是个不错的选择,称为多哨兵模式
当哨兵模式检测到master宕机,会自动将slave切换成master,通过发布订阅模式通知其他的从服务器,修改配置文件,让他们切换主机
多哨兵模式
- 假设master宕机,sentinel先检测到这个结果,系统并不会马上进行failover(故障切换、失效备援)这个现象称为主观下线,当后面的哨兵也检测到主服务器不可用,sentinel之间会发起一次投票,投票的结果由随机一个sentinel发起,进行failover操作,得到sentinel票数多的slave能成功切换为master,切换成功后,通过发布订阅模式,让各个哨兵把自己监控的服务器实现切换主机,这个过程称为客观下线
多哨兵模式实现
1、配置sentinel.conf
sentinel monitor 被监控的名称 主机地址 端口 1
sentinel monitor mymaster 127.0.0.1 6379 1
后面的这个数字1,代表主机挂了,slave(从机)投票看让谁接替成为主机,票数最多的就会成为主机。
2、启动哨兵
-
然后启动就行
-
默认端口为26379,默认pid为69427
-
当master 79 宕机,sentinel选举了81为newmaster
-
master节点断开,这时候从slave中选择一个座位master,其中有投票算法,自行了解
-
当79重新启动后,是以80作为master的slave role存在
主机宕机之后哨兵会重新在从机中选出一个从机做为新主机,如果旧主机重新启动回来了,只能归并到新的主机下,当作从机,这就是哨兵模式的规则。
哨兵模式优缺点
优点
- 基于集群,基于主从复制,所有的主从配置的优点,它全有
- 主从可以切换,故障可以切换,系统的可用性提高
- 哨兵模式就是主从模式的升级,手动到自动,更加健壮
缺点
- redis不好在线扩容,集群容量一旦达到上限,在线扩容就十分麻烦
- 哨兵模式需要很多配置
- 多哨兵,多端口配置复杂,一般由运维来配置
- 多哨兵,多端口配置复杂,一般由运维来配置
Redis缓存穿透、击穿、雪崩(面试高频,工作常用)
都是服务的三高问题
- 高并发
- 高可用
- 高性能
面试高频,工作常用
redis缓存的使用极大的提升了应用程序的性能和效率,特别是数据查询方面,但同时,它也带来了一些问题,数据一致性问题,严格意义上来讲,问题无解,对一致性要求极高,不推荐使用缓存
布隆过滤器、缓存空对象
缓存穿透
用户查询一个数据,redis数据库中没有,也就是缓存没命中,于是向持久层数据库查询,发现也没有,于是查询失败,用户很多的时候,缓存都没有命中,都请求持久层数据库,给持久层数据库造成巨大压力,称为缓存穿透
在直达持久层的路径上加上过滤器、或者缓存中专门增加一个为空的请求
布隆过滤器
- 布隆过滤器是一种数据结构,对所有可能的查询参数以hash形式存储,在控制层进行校验,不符合则丢弃,从而避免了对底层存储系统查询压力
缓存空对象(查不到)
- 当持久化层不命中后,将返回的空对象存储起来,同时设置一个过期时间,之后再访问这个数据就从缓存中获取,保护持久层数据源
- 需要面临的问题
- 存储空的key也需要空间
- 对空值设置了过期时间,还会存在缓存层和存储层的数据有一段时间窗口不一致,对于需要保持一致性的业务会有影响
缓存击穿(量太大了,缓存过期)
例子微博服务器热搜,巨大访问量访问同一个key
一个key非常热点,不停扛着大并发,集中对一个点进行访问,当个key失效的瞬间,持续大并发导致穿破缓存,直接请求数据库
某个key在过期的瞬间,大量的访问会同时访问数据库来查询最新的数据,并且回写缓存,导致数据库瞬间压力过大
解决方案
- 设置热点数据不过期
- 一直缓存也会浪费空间
- 加互斥锁
- 分布式锁:使用分布式锁,保证对于每个key同时只有一个线程查询后端服务,其他线程没有获得分布式锁的权限,只需要等待即可,这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大
缓存雪崩
在某一个时间段,缓存集中过期失效,redis宕机
产生雪崩的原因之一,设置缓存的存活时间较短,大并发访问时刚好都过期,直接访问了数据库,对数据库而言,会产生周期性压力波峰,暴增时数据库可能会宕机
双十一时会停掉一些服务,保证主要的一些服务可用,springcloud中说明过
解决方案:
- 增加集群中服务器数量
- 异地多活
- 限流降级
- 缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量,对某个key只允许一个线程查询数据和写缓存,其他线程等待
- 数据预热
- 正式部署之前,把可能的数据提前访问一遍,可能大量访问的数据就会加载到缓存中,加载不同的key,设置不同的过期时间,让缓存时间尽量均匀
- 正式部署之前,把可能的数据提前访问一遍,可能大量访问的数据就会加载到缓存中,加载不同的key,设置不同的过期时间,让缓存时间尽量均匀