这里写自定义目录标题
1. Redis的使用
在并发访问量高的情况下,数据库的弊端就会显现
1、随着访问量的增加,数据库的性能逐步降低
2、数据库属于磁盘存储,读写效率相对较低
3、数据库集群,同步数据称为一个问题
是Redis后,对于获取的请求,就不在请求数据库,直接到Redis获取即可
2. Redis的特点:
1、内存存储数据,读取效率高
2、属于no-sql类型的数据库
3、是key-value的存储形式
4、支持集群,集群内部自动同步数据
5、支持持久化技术
作用:用于数据缓存,提升获取数据的效率
缺点: 数据同步问题,缓存中的数据容易过期
3. 搭建Redis服务
cmd进入Redis的根目录,输入命令
redis-server.exe redis.windows.conf 回车即可
有三种方式操作Redis服务
1、桌面工具
2、Redis-cli.exe 此工具
3、Jedis java操作Redis
4. Redis的数据类型
4.1 string类型:可以字符串、整形、小数、布尔
【面试题】redis中的string类型的实现原理?
4.2 list类型:集合、列表类型
一个key,对应多个value,value之间使用逗号分隔
特点:可重复,有序
4.3 hash类型
一个key ,对应多个value,每个value又是 属性名-属性值结构,可以用来存储对象
Dept dept = new Dept(“1001”,“软件研发1部”,“设计城901”);
key-dept1
value
- deptNo 1001
- deptName 软件研发1部
- loc 设计城901
4.4 set
特点:一个key 对应多个值,无序、唯一
4.5 zset sortedset 有序的set集合
特点:一个key 对应多个值,唯一,但是有序,在添加值时,为每个值指定一个分数,按分数排序
总结:5种常用命令
-
key-string:最常用的,一般用于存储一个值。
-
key-hash:存储一个对象数据的。
-
key-list:使用list结构实现栈和队列结构。
-
key-set:交集,差集和并集的操作。
-
key-zset:排行榜,积分存储等操作。
另外三种数据结构(了解):
- HyperLogLog:计算近似值的。
- GEO:地理位置。
- BIT:一般存储的也是一个字符串,存储的是一个byte[]。
5. Redis的各种命令
每一个命令在Jedis中都说是对应的一个方法
作用:做数据缓存,用以提升获取数据的效率
特点:内存存储 nosql key-value存储 支持集群、持久化,输入易过期
数据类型:String list set hash zset sortedset
5.1 关于key的相关命令
【说明】Redis内存中存储的数据从存储时间上来分,有两种类型:
永久的:一直存活于内存中
瞬时的:有存活时间(秒 毫秒)
命令 | 含义 |
---|---|
DEL | 删除 |
EXISTS | 判断是否存在 |
EXPIRE | 以秒为单位设置存活时间 |
KEYS | 查看当前库下所有的key |
KEYS | keys m? 含义:类似于sql中的模糊查询(查看以m开头的key) |
MOVE | 移走 move mi 2(把当前库中mi移到索引2的库中) |
PERSIST | 设置永久存活有效 |
PEXPIRE | 以毫秒为单位设置获取时间 |
TTL | 以秒为单位查看剩余时间 |
PTTL | 以毫秒为单位查看剩余时间 |
RANDOMKEY | 有多个key,随机返回一个key |
RENAME | 重命名key(不建议,会覆盖) |
RENAMENX | 重命名key(推荐,不会覆盖) |
TYPE | 查看key是什么类型 |
5.2 String命令
命令 | 含义 |
---|---|
SET | 创建key value(同时可以通过EX秒/PX毫秒设置存活时间),如果创建了另一个set 同样的key会覆盖掉之前的key,所以要在后面加NX判断,如果set的key存在就不会成功 |
MSET | 可以设置多个key value |
MGET | 获取多个key |
APPEND | 在原有的字符串后面追加 |
INCR | 在原来的值上+1 |
INCRBY | 在原来的值上指定+几 |
INCRBYFLOAT | 在原来的值上指定+几(小数) |
DECR | 在原来的值上-1 |
DECRBY | 在原来的值上指定-几 |
GETRANGE | 下标截取 |
GETSET | getset db mysql(把新值mysql覆盖db) |
SETRANGE | 替换(覆盖) |
STRLEN | 长度 |
5.3 Hash命令
命令 | 含义 |
---|---|
HSET | 赋值 |
HMSET | 赋多个值 |
HGET | 获取值 |
HMGET | 获取多个值 |
HEXISTS | 判断是否存在 |
HDEL | 删除某个值 |
HGETALL | 获取所有属性值 |
HKEYS | 查看都有哪些key |
HLEN | 一共有几个键值对 |
HSETNX | 当你不存在时赋值,否则会覆盖 |
HVALS | 获取value值 |
5.4 List集合相关命令
命令 | 含义 |
---|---|
LSET | 改值 |
LPUSH | 左侧添加 (可重复) |
RPUSH | 右侧添加(可重复) |
LPOP | 删头 |
RPOP | 删尾 |
LLEN | 集合中有几个值 |
LINSERT | 插入(before在谁之前插入,after在谁2之后插入) |
LINDEX | 根据集合的对应索引的元素 |
LREM | lrem hello 0 a(删除hello里所有的a),lrem hello 3 a(正数,从左往右删掉3个a) |
LTRIM | 截取指定范围之内 ltrim dj 3 6(只保留从下标3截取到下标6的元素) |
RPOPLPUSH | 把 group1里的尾部元素删掉,放到group2里元素的头部 |
5.5 Set相关命令
命令 | 含义 |
---|---|
SADD | 添加元素(去重,无序) |
SCARD | 返回集合元素的数量 |
SDDIF | 以g4为参考,返回g4和g5不相同的元素(b,a),以g5为参考,返回g5和g4不相同的元素(f, g, n),不是返回两个集合不同的元素 |
SDIFFSTORE | 以g4为参考和g5对比不同的元素(a,b)添加到g6当中 |
SINTER | 返回的是g4和g5共同的元素 |
SMOVE | 把g4里的元素a移动到g5当中 |
SPOP | 随机删除 |
SDDIF
SDIFFSTORE
SINTER
SMOVE
5.6 zset sortedset
命令 | 含义 |
---|---|
ZADD | 添加元素(将一个或多个 member 元素及其 score 值加入到有序集 key 当中。) |
ZRANGEBYSCORE | 返回集合中指定的元素(生序排列) |
ZADD
ZRANGEBYSCORE
6. java操作Redis
缓存预热:在服务器启动后,把程序的“热点数据”,提前从数据库获取到,存入Redis的过程。
热点数据:
-
1、访问频率高
-
2、数据量较大的
1、新建项目
2、添加jedis的依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
3、操作
3、1:直接存储字符串
package com.qf.redis2204.test;
import redis.clients.jedis.Jedis;
public class Test1 {
public static void main(String[] args) {
//1、建立Java程序和Redis的连接
Jedis jedis=new Jedis("127.0.0.1",6379);
//2、选择一个库
jedis.select(2);
//3、直接存储字符串
String result=jedis.set("study","redis");
if(result.equalsIgnoreCase("ok")){
System.out.println("存储成功!");
}else{
System.out.println("存储失败!");
}
// if(result.toLowerCase().equals("ok")){
//
// }
//获取元素
String str=jedis.get("study");
System.out.println("从Redis中获取到的值:"+str);
//关闭资源
jedis.close();
}
}
3、2:从数据库得到的是集合,如何把集合存入Redis?
把对象转成byte[]类型进行存储,使用Redis的string类型
【注意】Redis的string类型最多只允许存储512M数据。
添加一个依赖:能把对象转成byte[]
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.18.RELEASE</version>
</dependency>
创建一个实体类:Student
package com.qf.redis2204.test;
import com.qf.redis2204.pojo.Student;
import org.springframework.util.SerializationUtils;
import redis.clients.jedis.Jedis;
import java.util.ArrayList;
public class Test2 {
public static void main(String[] args) {
//把对象存入Redis
Student student=new Student("qf001","张丛鑫",20);
Student student2=new Student("qf002","钱大野",20);
ArrayList<Student> list=new ArrayList<Student>();
list.add(student);
list.add(student2);
Jedis jedis=new Jedis("127.0.0.1",6379);
jedis.select(3);
//把对象转成byte[],key value都要转
String key1="张丛鑫";
byte[] byteKey1= SerializationUtils.serialize(key1);
byte[] byteValue1=SerializationUtils.serialize(student);
//出入redis
String result=jedis.set(byteKey1,byteValue1);
if("ok".equals(result.toLowerCase())){
System.out.println("存入成功!");
}else{
System.out.println("存入失败!");
}
//获取元素
byte[] getValue=jedis.get(byteKey1);
//把byte[] 转成对象类型
Student getStudent=(Student) SerializationUtils.deserialize(getValue);
System.out.println("获取到的学生:"+getStudent);
//关闭资源
jedis.close();
}
}
3、3:把对象转成JSON字符串
1、添加转换JSON的jar依赖
<!-- 导入fastJSON -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
package com.qf.redis2205.test;
import com.alibaba.fastjson.JSON;
import com.qf.redis2205.pojo.Student;
import com.qf.redis2205.util.RedisUtil;
import redis.clients.jedis.Jedis;
import java.util.ArrayList;
import java.util.List;
public class Test3 {
public static void main(String[] args) {
//把对象存入redis
Student stu1 = new Student("qf001", "aaa", 11);
Student stu2 = new Student("qf002", "bbb", 12);
ArrayList<Student> list = new ArrayList();
list.add(stu1);
list.add(stu2);
//创建连接
//Jedis jedis = new Jedis("127.0.0.1", 6379);
Jedis jedis = RedisUtil.getJedis();
//选择库
jedis.select(4);
//把集合转成json
String key = "java2205";
String json = JSON.toJSONString(list);
String result = jedis.set(key, json);
if ("ok".equals(result.toLowerCase())) {
System.out.println("写入json成功");
} else {
System.out.println("写入json失败");
}
//读取
String getJson = jedis.get(key);
//把json转为对象
// 只能转成单个对象
//JSON.parseObject(getJSON,Student.class);
//把json转为对象
List<Student> lists = JSON.parseArray(getJson, Student.class);
for (Student student : lists) {
System.out.println(student);
}
}
}
7. 工具类的使用
为了提升java连接Redis服务的效率,创建Redis的连接池
package com.qf.redis2204.util;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class RedisUtil {
//声明类级别的连接池对象
private static JedisPool jedisPool=null;
//在static代码块中,编写连接池的配置信息
static {
//创建连接池的配置对象
GenericObjectPoolConfig genericObjectPoolConfig=new GenericObjectPoolConfig();
//设置连接池中的最大连接数量
genericObjectPoolConfig.setMaxTotal(100);
//设置连接中的空闲连接的数量
genericObjectPoolConfig.setMaxIdle(10);
//设置最小空闲连接数量
genericObjectPoolConfig.setMinIdle(5);
//设置等待的超时时间
genericObjectPoolConfig.setMaxWaitMillis(3000);
//创建连接池对象
jedisPool=new JedisPool(genericObjectPoolConfig,"127.0.0.1",6379);
}
//编写从连接中获取连接的方法
public static Jedis getJedis(){
return jedisPool.getResource();
}
}
工具类的使用:
8. 管道技术
当频繁操作Redis是,如果逐个命令去发送、执行,这样效率会很低,把要执行的命令进行“积攒”,然后分批发送到Redis服务器,进行执行,这样来提升执行效率
管道技术:用于积攒命令
package com.qf.redis2204.test;
import com.qf.redis2204.util.RedisUtil;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
public class Test4 {
public static void main(String[] args) {
//执行100万个命令
//不使用管道
Jedis jedis= RedisUtil.getJedis();
jedis.select(5);
jedis.set("num","1");
//获取时间
long start1=System.currentTimeMillis();
for (int i=1; i<=1000000; i++){
//+1操作
jedis.incr("num");
}
//获取结束时间
long end1=System.currentTimeMillis();
jedis.select(6);
jedis.set("num","1");
//获取管道
Pipeline pipeline=jedis.pipelined();
//再次获取开始时间
long start2=System.currentTimeMillis();
for(int i=1; i<=1000000; i++){
//把命令积攒到管道中
pipeline.incr("num");
}
//把管道中的命令提交到Redis中,并且执行
pipeline.syncAndReturnAll();
//获取结束时间
long end2=System.currentTimeMillis();
System.out.println("不使用管道用时:"+(end1-start1));
System.out.println("使用管道技术用时:"+(end2-start2));
}
}
效果:
9. Redis的数据持久化机制-重要
使用Jedis操作Redis
常用String类型,只能存储512M
热点数据
存储方式有3种:
1、普通字符串
2、对象,转成byte[]
3、对象,转成JSON
连接池-凡在工具类中:
管道:提升工作效率
9.1 为什么要持久化?
Redis使用内存来存储数据,当Redis服务器重启、宕机了,内存的数据就被清除,当服务启动后,如果进行缓存预热,自行查取数据,存入Redis,效率低。
有持久化后,自行按照条件存储内存中的数据,当Redis启动后,自动加载持久化的数据,无需进行缓存预热,提升效率。
9.2 Redis支持的持久化方式有3种:
方式1:RDB快照方式持久化-默认的
1、当符合持久化条件时,Redis就fork出一个子进程,进行持久化
2、新创建一个指定名字的持久化文件 dump.rdb
3、把内存中的所有数据存储于持久化文件中
4、删除掉原有的持久化文件,用新的持久化文件代替
分析:
1、优点:由于是fork出的子进程进行的持久化,所以不影响Redis的运行
2、优点:使用新的文件替代旧的持久化文件,效率相对AOF方式更高
3、缺点:可能会丢失一部分数据,在本次持久化后,下次持久化之前,Redis中变化的数据会被丢失
注意
实际使用中,save 1 X 最多只允许丢失1秒的数据
方式2:AOF只追加文件方式持久化
Append only file 只追加文件方式持久化,需要开启
AOF方式持久化的条件也称为刷写模式,共有3种:
1、Redis自身进行持久化,影响Redis的性能
2、是一直在原有的持久化文件的基础上进行追加,导致此文件越来越大
3、性能比rdb快照方式慢
方式3:两种方式一起使用
如果两种方式同时开启:那么优先使用AOF方式的持久化文件进行加载
如果先开启了RDB,后开启AOF: AOF持久化文件的内容要覆盖掉RDB持久化的内容
10. 数据淘汰策略
数据淘汰:Redis删除内存中的数据的过程
Redis自身删除过期的数据,并不是立刻删除:
方式1:定期删除,Redis每隔一段时间,删除已经过期的数据,默认是每隔100毫秒,只随机删除其中3个过期的key。
方式:惰性删除,即使过期,也不删除,当获取数据的时候,判断你是否过期,如果过期则删除,获取到的就是nil。
Redis从4开始是多线程模型:
多线程:是接收命令、响应命令的执行结果是多线程的
单线程:核心的执行存储、读取数据的业务,依然是单线程
当Redis内存,存满了数据,还需要进行存储时,就需要Redis自身删除掉一部分数据,Redis的删除依据就是数据淘汰策略:
在Redis内存已经满的时候,添加了一个新的数据,执行淘汰机制。
- volatile-lru:在内存不足时,Redis会再设置过了生存时间的key中干掉一个最近最少使用的key。
- allkeys-lru:在内存不足时,Redis会再全部的key中干掉一个最近最少使用的key。
- volatile-lfu:在内存不足时,Redis会再设置过了生存时间的key中干掉一个最近最少频次使用的key。
- allkeys-lfu:在内存不足时,Redis会再全部的key中干掉一个最近最少频次使用的key。
- volatile-random:在内存不足时,Redis会再设置过了生存时间的key中随机干掉一个。
- allkeys-random:在内存不足时,Redis会再全部的key中随机干掉一个。
- volatile-ttl:在内存不足时,Redis会再设置过了生存时间的key中干掉一个剩余生存时间最少的key。
- noeviction:(默认)在内存不足时,直接报错。
指定淘汰机制的方式:maxmemory-policy 具体策略,设置Redis的最大内存:maxmemory 字节大小
设置数据淘汰策略:
设置Redis服务器能使用的内存的大小
11. Redis的集群
为了安全,给Redis设置访问密码
集群的作用:
-
1、提升了并发请求数据的处理能力
-
2、防止单点故障
Redis的集群通常有3种:
1、主从集群: 一台主服务器,多台从服务器
属于中心化的集群,生产环境中不会使用,由于主服务器只有一台,不高可用,容易发生单点故障
主从集群进行了读写分离:
主服务器:写入数据通过Redis
从服务器:自动从主服务器同步、拉取数据,负责读取数据使用
2、哨兵集群: 是以主从集群为基础,每个服务器,配置一个哨兵,用于监控主服务器,当主服务器宕机后,会进行投票选举,选举新的主服务器。
3、cluster集群: 只有Linux系统支持,去中心化的集群
至少有3对节点
当某个主服务器宕机后,对应的从服务器,自动升级为主服务器,当原来的主服务器启动后,就降级为从服务器
由于有多个主服务器,那么写数据时,通过哪台服务器写入呢?
cluster提供了hash槽0~16383 之间,共16384个值,如果有3个主服务器,每个主服务器平均分配hash槽的值
当出入数据时,先对key值进行hash计算,得到一个0~16383之间的一个值,计算的值在哪个主服务器对应的范围内,就通过哪个主服务器写入数据,其他主服务器拉取、同步数据
11.1 主从集群的搭建
一主三从
主服务器:端口 6379
从服务器:改端口号 6380
服从于主服务器的信息
先启动主服务器,然后启动从服务器
通过从服务器写数据会失败,从服务器是只读的,只能读取数据,可以通过主服务器写入数据
【说明】通过主服务器写入数据后,从服务器会自动从主服务器拉取数据。
11.2 哨兵集群
在主从集群的基础上搭建的,为每个Redis服务器,配置一个哨兵,都去监控主服务器
主观宕机:如果某个哨兵发送ping给主服务器,在规定的内,没接收到主服务器返回的pong,认为主服务器器主观宕机
【注意】
主观宕机后,并不进行投票选举,有可能是网络延迟造成
客观宕机:监控到主服务器宕机的哨兵的数量达到了阈值,认知客观宕机
【注意】
客观宕机后,才进行投票选举
投票协议:每个哨兵都可以投票给其他服务器,也可以征求其他服务器的投票
当某个Redis的投票数达到一半以上,此服务就成为新的主服务器,其他服务器从该服务器同步、拉取数据
自动故障迁移:投票、选举主服务器、同步、拉取数据的过程。
1、在每个Redis服务器下创建一个sentinel.conf配置文件
2、在每个服务下创建sentinel_startup.bat文件,用于启动哨兵
3、把以上两个文件分别复制到各个从服务器中,修改端口号及title对应的值
4、启动
5、关闭掉主服务器后,就会自动故障迁移
12. Redis的常见问题及解决方案
12.1 缓存穿透
发出大量的请求,请求Redis,请求的key都是Redis不存在的,导致大量的请求到达数据库,从而把数据库请求宕机,造成数据库不可用
【说明】先到Redis获取,如果获取到,则直接返回,如果获取不到,就到数据库获取
解决方案:
1、缓存空值: 当到数据库中获取不到数据后,把请求的key和null值进行Redis缓存,下次再请求此key时,直接返回空值
缺点:Redis缓存并没有拦截住请求,大量的请求还是到达数据库
缓存大量的无效的空值
2、布隆过滤: 把Redis中的所有的key,存储于web服务器的一个集合中
-
每当请求Redis时,判断请求的key是否在集合中
-
如果在集合中,则进行Redis获取
-
如果不在,说明是无效的key,则直接返回
缺点: 对Redis进行增加、删除key操作的同时,还需要对集合进行更新
12.2 缓存击穿
key是合理的,但是Redis中大量热点数据同时过期,导致即使合理的key值,但是到Redis中依然获取不到数据,进而大量的请求到达了数据库,降低数据库的性能
解决方案:
1、分散设置热点数据的过期时间,避免大量的热点数据同时过期-最优
2、去掉生存时间
3、给数据库加锁,限制数据库的并发访问量
12.3 缓存雪崩
缓存雪崩:缓存服务器不可用
原因:
1、所有的 百万级别的热点数据同时过期,导致Redis失去作用
2、Redis发生单点故障
解决方案:
1、使用集群
2、对key的过期时间设置一个范围的随机值,避免大量的key同时过期
12.4 缓存倾斜
有多台Redis服务器,每个Redis服务器缓存了不同的热点数据,会造成请求会集中在某个Redis服务器上,最终导致Redis宕机不可用。
解决方案:
1、对热点数据的Redis服务合理配置集群中服务的数量。
【说明】声明类,存储Redis的各个key值
package com.qf.springbootvue.util;
import java.util.ArrayList;
public class RedisKeys {
//声明集合
public static ArrayList<String> listKeys=new ArrayList<String>();
static {
//把所有的Redis中的key存入集合
listKeys.add(RedisKeys.BOOT_DEPT);
}
//声明key
public static String BOOT_DEPT="depts";
}
具体内容
@Override
public List<Dept> findDept() {
List<Dept> deptList=null;
String key="dpets";
//布隆过滤
boolean flag=false;
for (String strKey: RedisKeys.listKeys
) {
if(key.equals(strKey)){
flag=true;
break;
}
}
if(flag){
//先到Redis中获取部门
Jedis jedis= RedisUtil.getJedis();
//选择库
jedis.select(1);
String json=jedis.get(key);
//判断是否获取到
if(json!=null){
//如果获取到数据,直接返回
deptList=JSON.parseArray(json,Dept.class);
}else {
//如果获取不到数据,则到数据中获取
deptList=deptDao.findDept();
//存入Redis
String strJson= JSON.toJSONString(deptList);
jedis.set(key,strJson);
}
}
return deptList;
}