文章目录
Redis入门
1.学习目标
2.Redis的介绍与安装
2.1.Redis是什么?
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)。
2.2.性能
下面是官方的bonch-mark数据:
测试完成了50个并发执行100000个请求。
设置和获取的值是一个256字节字符串。
结果:读的速度是110000次/s,写的速度是81000次/s
2.3.Redis历史简介
2008年,意大利一家创业公司Merzia的创始人Salvatore Sanfilippo为了避免MySQL的低性能,亲自定做一个数据库,并于2009年开发完成,这个就是Redis。
从2010年3月15日起,Redis的开发工作由VMware主持。从2013年5月开始,Redis的开发由Pivotal赞助。
从2013年5月开始,Redis的开发由Pivotal赞助。
说明: Pivotal公司是由EMC和VMware联合成立的一家新公司。Pivotal希望为新一代的应用提供一个原生的基础,建立在具有领导力的云和网络公司不断转型的IT特性之上。Pivotal的使命是推行这些创新,提供给企业IT架构师和独立软件提供商。
2.4.支持语言
2.5.支持的数据类型
string、hash、list、set、sorted set
2.6.安装
2.6.1.下载地址
2.6.2.安装至服务器
Xftp上传安装
可以用xftp把下载的安装包上传导服务器,然后解压。
Linux命令安装
- 下载
mkdir redisfilecd redisfile/wget http://download.redis.io/releases/redis-5.0.3.tar.gz
- 解压
tar -zxvf redis-5.0.3.tar.gz
- 安装
cd redis-5.0.3makemkdir -p /usr/local/redismake PREFIX=/usr/local/redis/ install
> 如果报错了,可能是少了C语言编译环境,因为redis是用C语言编写的。- 配置C语言环境yum -y install gcc-c++ automake autoconf
- 启动服务器
cd /usr/local/redis/bin./redis-server
> 为了更方便操作,可以把redis改成后台运行
2.6.3.redis后台运行
cd redis-5.0.3
cp redis.conf /usr/local/redis/bin/
cd /usr/local/redis/bin/
vim redis.conf
# 找到daemonize no,把no改成yes
- 启动
./redis-server redis.conf
- 查看是否启动
# 进入redis-cli客户端
./redis-cli
pingset name zhangsan
get name
# 有返回值说明启动成功
2.6.4.安装RDM可视化客户端
教程:https://blog.csdn.net/baidu_35692846/article/details/118399499
- 安装完后测试连接连接不上,说明只允许本机连接,这时候需要修改conf
vim redis.conf
- 可以加上:bind 允许访问的ip(工作时的操作)- 或者直接在bind 127.0.0.1前面加个#注释掉(学习时的操作)- 然后把protected mode 保护模式的yes改成no- 然后保存并退出:按esc,输入:wq,按回车。- 杀掉redis-server进程# 查看进程ps -ef|grep redis# 杀掉redis-server进程,注意,端口不一定是6588kill -9 6588
- 启动./redis-server redis.conf
> 客户端尝试连接,如果还是无法连接则关闭防火墙> >> systemctl stop firewalld.service> >
没有密码不太安全,所以需要加上密码vim redis.conf
找到requirepass ,然后取消注释,foobared就是密码,可以更改# 查端口ps aux|grep redis# 杀进程kill -9 进程占用的端口号# 再次启动./redis-server redis.conf
- 这时候我们已经连上了redis,我们发现有16个数据库,如果想增加可以据需更改conf文件,找到databases修改
- 修改完我们要在客户端删除原来的连接,然后重启redis,然后重新连接。
3.关系型数据库与非关系型数据库
3.1.关系型数据库
采用关系模型来组织数据的数据库,关系模型就是二维表格模型。一张二维表的表名就是关系,二维表中的一行就是—条记录,二维表中的一列就是一个字段。
3.1.1.优点
- 容易理解
- 使用方便,通用的sql语言
- 易于维护,丰富的完整性(实体完整性、参照完整性和用户定义的完整性)大大降低了数据冗余和数据不一致的 概率
3.1.2.缺点
- 磁盘I/O是并发的瓶颈
- 海量数据查询效率低
- 横向扩展困难,无法简单的通过添加硬件和服务节点来扩展性能和负载能力,当需要对数据库进行升级和扩展时,需要停机维护和数据迁移
- 多表的关联查询以及复杂的数据分析类型的复杂sql查询,性能欠佳。因为要保证acid,必须按照三范式设计。 数据库
3.1.3.数据库
- Orcale
- Sql Server
- MySql
- DB2
3.2.非关系型数据库
非关系型,分布式,一般不保证遵循ACID原则的数据存储系统。键值对存储,结构不固定。
3.2.1.优点
- 根据需要添加字段,不需要多表联查。仅需id取出对应的value
- 适用于SNS(社会化网络服务软件。比如facebook,微博)
- 严格上讲不是一种数据库,而是—种数据结构化存储方法的集合缺点
- 只适合存储一些较为简单的数据。不合适复杂查询的数据
- 不合适持久荏储海量数据
3.2.2.数据库
- K-V: Redis,Memcache
- 文档:MongoDB
- 搜索:Elasticsearch,Solr
- 可扩展性分布式:HBase
3.3.比较
内容关系型数据库非关系型数据库成本有些需要收费(Orcale)基本都是开源查询数据存储存于硬盘中,速度慢数据存于缓存中,速度快存储格式只支持基础类型K-V,文档,图片等扩展性有多表查询机制,扩展困难数据之间没有耦合,容易扩展持久性适用持久存储,海量存储不适用持久存储,海量存储数据一致性事务能力强,强调数据的强—致性事务能力弱,强调数据的最终—致性
4.Redis-cli操作Redis
4.1.Redis-cli连接Redis
- 先开redis服务器
cd /usr/local/redis/bin/
./redis-server redis.conf
- redis-clli连接redis
# 最后一个是redis的密码
./redis-cli -p 6379 -a password
4.2.Redis 字符串(String)
Redis 字符串数据类型的相关命令用于管理 redis 字符串值,
4.2.1.存一个值
#字段名为name,值为zhangsanset name zhangsan
# 取名为name的字段
get name
4.2.2.存多个值和取多个值
# 存多组值
mset sex 1 address shanghai
# 取多组值
mget name sex address
4.3.Redis 哈希(Hash)
Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。
Redis 中每个 hash 可以存储 232 - 1 键值对(40多亿)。
4.3.1.存取单个hash
#这里的user是redis的key,name是hash的key,zhangsan是hash的value
hset user name zhangsan
# 取值
hget user name
4.3.2.存取多个hash
# 存
hmset user age 18 sex 1
# 取
hmget user name age sex
4.3.3.取整个redis下的hash
# 取key为user的redis的hash值
hgetall user
4.3.4.删除操作
# 删除user下的name和age
hdel user name age
4.4.Redis 列表(List)
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
一个列表最多可以包含 232 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。
4.4.1.左添加
# 添加 lis在前zhangsan在后,因为是左添加,每次添加都从左边开始
lpush students zhjangsan lis
# 取
lrange students 0 2
4.4.2.右添加
# wwangwu在前zhaoliu在后,因为是右添加,每次添加都从右边开始
rpush students wangwu zhaoliu
# 取
lrange students 0 3
4.4.3.查询长度
# 查students的长度
llen students
4.4.4.删除
# 删除名为students的list的2条lisi(从左往右删)
lrem students 2 lisi
4.5.Redis 集合(Set)
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
集合对象的编码可以是 intset 或者 hashtable。
Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
4.5.1.添加
# 添加名为letters的set,值分别为aaa、bbb...eee
sadd letters aaa bbb ccc ddd eee
4.5.2.获取
# 查询名为letters的set的数据
smembers letters
4.5.3.查询长度
# 查询名为letters的set的长度
scard letters
4.5.4.删除
# 删除letters的aaa,ccc
srem letters aaa ccc
4.6.Redis 有序集合(sorted set)
Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。 集合中最大的成员数为 232 - 1(4294967295, 每个集合可存储40多亿个成员)。
4.6.1.增删改查
# score为名称
zadd score 7 zhangsan 3 si 6 wangwu 10 zhaoliu 1 tianqi
# 查值
zrange score
# 删除score下的zhangsan,lisi
zrem score zhangsan lisi
# 查长度
zcard score
4.7.Redis-cli的通用命令
set cart:user01:item01 apple
get cart:user01:item01
- RDM客户端的显示
4.7.1.设置key的失效时间
Redis有四个不同的命令可以用于设置键的生存时间(键可以存在多久)或过期时间(键什么时候会被删除):
EXP1RE :用于将键key的生存时间设置为tt1秒。
PEXPIRE :用于将键key的生存时间设置为tt1毫秒。
EXPIREAT < timestamp>:用于将键key的过期时间设置为timestamp所指定的秒数时间戳。
PEXPIREAT :用于将键key的过期时间设置为timestamp所指定的毫秒数时间戳
TTL:获取的值为-1说明此key没有设置有效期,当值为-2时证明过了有效期。
- 方法一
# 设置个失效时间为10秒的code,值为testset code test ex 10# 10s内可以取到get code# 查看是否过了失效期ttl code# 10s后失效了get code
- 方法2
# 10000毫秒失效,最后一个参数,xx表示如果code存在就会设置成功set code test px 10000 xx# 最后一个参数,nx表示如果code不存在就会设置成功set code test px 10000 nx
---
4.7.2.删除
# 这里的address就是想删除的key
del address
5.Java操作redis
5.1.新建项目
5.1.1.IDEA新建项目
5.1.2.pom.xml配置
<?xml version="1.0" encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.xmjj</groupId><artifactId>redisdemo</artifactId><version>0.0.1-SNAPSHOT</version><name>redisdemo</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>2.3.7.RELEASE</spring-boot.version></properties><dependencies><dependency><!-- redis依赖--><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><!-- 1.×的版本默认采用的连接池技术是Jedis,2.0 以上版本默认连接池是Lettuce,--><!-- 如果采用 Jedis,需要排除Lettuce的依赖。--><exclusions><exclusion><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId></exclusion></exclusions></dependency><!-- jedis依赖--><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId></dependency><!-- web组件--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--test组件--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement></project>
5.1.3.application.yml配置文件
# 应用名称spring:redis:#Redis服务器地址host: 你的Redis服务器地址
#Redis服务器姗口port:6379#Redis服务器密码password: root
#选择哪个库,默认e库database:0#连接超时时间timeout: 10000ms
jedis:pool:#最大连接数,默认8max-active:1024#最大连接阻塞等待时间,单位毫秒,默认-1msmax-wait: 10000ms
#最大空闲连接,默认8max-idle:200#最小空闲连接,默认emin-idle:5
5.1.4.RedisConfig配置类
package com.xmjj.redisdemo.cofig;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import redis.clients.jedis.JedisPool;import redis.clients.jedis.JedisPoolConfig;/**
* @author 冰咖啡
* Date:2022/3/7 11:33
* Description:
*/@ConfigurationpublicclassRedisConfig{//服务器地址@Value("${spring.redis.host}")private String host;//服务器端口@Value("${spring.redis.port}")privateint port;//服务器密码@Value("${spring.redis.password}")private String password;//连接超时时间@Value("${spring.redis.timeout}")private String timeout;//最大连接数@Value("${spring.redis.jedis.popl.max-active}")privateint maxTotal;//最大连接阻塞等待时间,单位毫秒,默认-1ms@Value("${spring.redis.jedis.popl.max-wait}")private String maxWaitMillis;//最大空闲连接@Value("${spring.redis.jedis.popl.max-idle}")privateint maxIdle;//最小空闲连接@Value("${spring.redis.jedis.popl.min-idle}")privateint minIdle;@Beanpublic JedisPool getJedisPool(){
JedisPoolConfig jedisPoolConfig =newJedisPoolConfig();//最大连接数
jedisPoolConfig.setMaxTotal(maxTotal);//最大阻塞时间
jedisPoolConfig.setMaxWaitMillis(Long.valueOf(maxWaitMillis.substring(0,maxWaitMillis.length()-2)));//最大空闲连接
jedisPoolConfig.setMaxIdle(maxIdle);//最小空闲连接
jedisPoolConfig.setMinIdle(minIdle);
JedisPool jedisPool =newJedisPool(jedisPoolConfig,host,port,Integer.valueOf(timeout.substring(0,timeout.length()-2)));return jedisPool;}}
5.1.5.测试连接
package com.xmjj.redisdemo;import org.junit.After;import org.junit.Before;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;import redis.clients.jedis.Jedis;import redis.clients.jedis.JedisPool;import java.util.List;@SpringBootTest@RunWith(SpringRunner.class)publicclassRedisdemoApplicationTests{// @Test// public void initConnt() {// //创建Jedis对象,连接redis服务器// Jedis jedis = new Jedis("192.168.77.134", 6379);// //设置密码// jedis.auth("root");// //指定数据库,默认为0// jedis.select(1);// //使用ping命令测试连接是否成功// String result = jedis.ping();// System.out.println(result);// //添加一条数据// jedis.set("name", "zhangsan");// //获取数据// String name = jedis.get("name");// System.out.println(name);// //释放资源// if (jedis != null) {// jedis.close();// }// }// @Test// public void initConnt2(){// //初始化redis连接对象// JedisPool jedisPool = new JedisPool(new JedisPoolConfig(),"192.168.77.134",6379,10000,"root");// //从连接池获取jedis对象// Jedis jedis = jedisPool.getResource();// //指定数据库,默认为0// jedis.select(2);// //使用pingml测试连接是否成功// String result = jedis.ping();// System.out.println(result);// //添加数据// jedis.set("name","zhangsan");// //读取数据// String name = jedis.get("name");// System.out.println(name);// //释放资源// if(jedis!=null){// jedis.close();// }// }@Autowiredprivate JedisPool jedisPool;private Jedis jedis = null;@BeforepublicvoidinitConnt(){try{
jedis = jedisPool.getResource();}catch(Exception e){
System.out.println(e);}}@TestpublicvoidtestString(){//使用ping,测试是否正常连接
String ping = jedis.ping();
System.out.println(ping);//添加一条数据
jedis.set("name","zhangsan");//获取一条数据
String name = jedis.get("name");
System.out.println(name);//添加多条数据
jedis.mset("address","sh","sex","1");//获取多条数据、参数的奇数位是key,偶数位是value
List<String> list = jedis.mget("address","name","sex");
list.forEach(System.out::println);//删除
jedis.del("name");}//释放资源@AfterpublicvoidcloseConnt(){if(null!=jedis){
jedis.close();}}}
5.1.6.Jedis操作string数据类型
publicvoidtestString(){//使用ping,测试是否正常连接
String ping = jedis.ping();
System.out.println(ping);//添加一条数据
jedis.set("name","zhangsan");//获取一条数据
String name = jedis.get("name");
System.out.println(name);//添加多条数据
jedis.mset("address","sh","sex","1");//获取多条数据、参数的奇数位是key,偶数位是value
List<String> list = jedis.mget("address","name","sex");
list.forEach(System.out::println);//删除
jedis.del("name");}
5.1.7.Jedis操作Hash数据类型
@TestpublicvoidtestHash(){/**
* 添加一条数据
* 第一个参数: redis的key
* 第二个参数: hash的key
* 第三个参数: hash的value
*/
jedis.hset("user","name","zhangsan");/**
* 获取一条数据
* 第一个参数: redis的key
* 第二个参数: hash的key
*/
String name = jedis.hget("user","name");
System.out.println(name);/**
* 添加多条数据
*/
Map<String,String> map =newHashMap<>();
map.put("age","20");
map.put("sex","1");
jedis.hmset("user",map);/**
* 获取多条数据
*/
List<String> list = jedis.hmget("user","age","sex");
list.forEach(System.out::println);/**
* 获取hash类型的所有的数据
*/
Map<String, String> user = jedis.hgetAll("user");
user.entrySet().forEach(e->{
System.out.println(e.getKey()+"----->"+e.getValue());});/**
* 删除
* 一个参数: redis的key
* 后面的参数: hash的key (可变)
*/
jedis.hdel("user","name","sex");}
5.1.8.Jedis操作List数据类型
@TestpublicvoidtestList(){//左添加
jedis.lpush("students","zhangsan","lisi");//右添加
jedis.rpush("students","wangwu","zhaoliu");/**
* 获取数据
* 第一个参数: redis的key
* 第二个参数:起始下标
* 第三个参数:结束下标
*/
List<String> students = jedis.lrange("students",0,3);
students.forEach(System.out::println);//获取总套数
Long total = jedis.llen("students");
System.out.println(total);/**
*删除数据
* 第一个参数:redis的key
* 第二个参数:删除值的个数
* 第三个参数:要删除的值
*/
jedis.lrem("students",1,"lisi");//获取总套数
total = jedis.llen("students");
System.out.println(total);//左弹出
System.out.println(jedis.lpop("students"));//右弹出
System.out.println(jedis.rpop("students"));//获取总套数
total = jedis.llen("students");
System.out.println(total);}
5.1.9.Jedis操作Set数据类型
@TestpublicvoidtestSet(){//添加数据
jedis.sadd("letters","aaa","bbb","ccc","ddd","eee");//获取数据
Set<String> set = jedis.smembers("letters");
set.forEach(System.out::println);//获取总条数
Long total = jedis.scard("letters");//删除数据
jedis.srem("letters","aaa","ccc");
System.out.println(jedis.scard("letters"));}
5.1.10.Jedis操作SortedSet数据类型
@TestpublicvoidtestSortedSet(){//添加数据
Map<String,Double> map =newHashMap<>();
map.put("zhangsan",7D);
map.put("lisi",3D);
map.put("wangwu",5D);
map.put("zhaoliu",6D);
map.put("tianqi",1D);
jedis.zadd("score",map);/**
* 获取数据
* 第一个参数:redis的key
* 第二个参数:起始下标
* 第三个参数:结束下标
*/
Set<String> set = jedis.zrange("score",0,4);
set.forEach(System.out::println);//获取总条数
Long total = jedis.zcard("score");
System.out.println(total);//删除
jedis.zrem("score","zhangsan","wangwu");
System.out.println(jedis.zcard("score"));}
5.1.11.层级目录+失效时间
- 层级目录
@TestpublicvoidtestDDir(){
jedis.set("cart:user01:item01","apple");
System.out.println(jedis.get("cart:user01:item01"));}
- 失效时间
@TestpublicvoidtestExpire(){//给已经存在在key设置失效时间// jedis.set("code","test");//设置失效时间,单位秒// jedis.expire("code",30);//设置失效时间,单位毫秒// jedis.pexpire("code",30000);// //查看失效时间,-1为未失效,-2为已失效// Long ttl = jedis.ttl("code");// System.out.println(ttl);//设置失效时间,单位秒
jedis.setex("code",30,"test");//设置失效时间,单位毫秒
jedis.psetex("code",30000,"test");//查看失效时间,单位毫秒
Long pttl = jedis.pttl("code");
System.out.println(pttl);// SetParams setParams = new SetParams();// //不存在则设置成功// setParams.nx();// //存在你则设置成功 setParams.xx();// //设置失效时间,单位秒 setParams.ex(30);// //设置失效时间,单位毫秒// setParams.px(30000);// jedis.set("code","test",setParams);}
5.1.12.获取所有的key+事务
- 获取key
@TestpublicvoidtestAllKey(){//当前数据库key的数量
Long size = jedis.dbSize();
System.out.println(size);//查询当前数据库所有的key
Set<String> set = jedis.keys("*");
set.forEach(System.out::println);}
- 事务
@TestpublicvoidtestMulti(){//开启事务
Transaction tx = jedis.multi();
tx.set("tel","10086");//提交事务
tx.exec();//回滚事务// tx.discard();}
5.1.13.Jedis获取操作byte数组
- 系列化工具类
package com.xmjj.redisdemo.util;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;/**
* @author 冰咖啡
* Date:2022/3/7 22:13
* Description:
*/publicclassSerializeUtil{/**
* 将java对象转换为byte数组,序列化过程
* @param object
* @return
*/publicstaticbyte[]serialize(Object object){
ObjectOutputStream oos = null;
ByteArrayOutputStream baos = null;try{// 序列化
baos =newByteArrayOutputStream();
oos =newObjectOutputStream(baos);
oos.writeObject(object);byte[] bytes = baos.toByteArray();return bytes;}catch(Exception e){
e.printStackTrace();}return null;}/**
* 将byte数组转换为java对象,反序列化
* @param bytes
* @return
*/publicstatic Object unserialize(byte[] bytes){
ByteArrayInputStream bais = null;try{// 反序列化
bais =newByteArrayInputStream(bytes);
ObjectInputStream ois =newObjectInputStream(bais);return ois.readObject();}catch(Exception e){
e.printStackTrace();}return null;}}
- User实体类
package com.xmjj.redisdemo.pojo;import java.io.Serializable;/**
* @author 冰咖啡
* Date:2022/3/7 22:16
* Description:
*/publicclassUserimplementsSerializable{privatestaticfinallong serivalVersionUID =9148937431079191022L;private Integer id;publicUser(){}@Overridepublic String toString(){return"User{"+"id="+ id +", username='"+ username +'\''+", password='"+ password +'\''+'}';}publicstaticlonggetSerivalVersionUID(){return serivalVersionUID;}public Integer getId(){return id;}publicvoidsetId(Integer id){this.id = id;}public String getUsername(){return username;}publicvoidsetUsername(String username){this.username = username;}public String getPassword(){return password;}publicvoidsetPassword(String password){this.password = password;}publicUser(Integer id, String username, String password){this.id = id;this.username = username;this.password = password;}private String username;private String password;}
- 测试
@TestpublicvoidtestByte(){
User user =newUser();
user.setId(2);
user.setUsername("zhangsan");
user.setPassword("123456");//序列化为byte数组byte[] userKey = SerializeUtil.serialize("user:"+ user.getId());byte[] userValue = SerializeUtil.serialize(user);// 存入redis
jedis.set(userKey,userValue);//取出byte[] bytes = jedis.get(userKey);//反序列化
User user1 =(User) SerializeUtil.unserialize(bytes);
System.out.println(user1);}
5.1.14.redis持久化方案
redis作为一个内存数据库,最担心的,就是万一机器死机宕机,数据就会消失掉,为了缓解这个问题,我们有三个方案。
BGSAVE机制
# 在后台异步保存当前数据库的数据到磁盘。
bgsave
优点:简单,缺点:繁琐
Redis 提供了 RDB 和 AOF 两种持久化方案,将内存中的数据自动保存到磁盘中
AOF持久化以独立日志的方式记录每次写命令,并在 Redis 重启时在重新执行。AOF 文件中的命令以达到恢复数据的目的。AOF 的主要作用是解决数据持久化的实时性RDB把当前 Redis 进程的数据生成时间点快照( point-in-time snapshot ) 保存到存储设备的过程。
RDB持久化
RDB是Redis用来进行持久化的一种方式,是把当前内存中的数据集快照写入磁盘,也就是 Snapshot 快照(数据库中所有键值对数据)。恢复时是将快照文件直接读到内存里。
RDB 有两种触发方式,分别是自动触发和手动触发。
- 进入redis.conf配置文件,修改save
save 900 1:表示900 秒内如果至少有 1 个 key 的值变化,则保存
save 300 10:表示300 秒内如果至少有 10 个 key 的值变化,则保存
save 60 10000:表示60 秒内如果至少有 10000 个 key 的值变化,则保存
修改完保存重启即可。
AOF持久化
AOF通过4点实现持久化
写入缓存每次执行命令后,进行append操作写入AOF缓存同步磁盘AOF 缓冲区根据对应的策略向硬盘进行同步操作AOF重写随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的重启加载当 Redis 重启时,可以加载 AOF 文件进行数据恢复
- 进入redis.conf配置文件,修改appendonly,no改成yes
- 重启 Redis 之后就会进行 AOF 文件的载入。
- 异常修复命令:
redis-check-aof --fix
6.Redis主从复用
6.1.读写分离
#引用公共配置
include /opt/redis/conf/redis-common.conf
#进程编号记录文件
pidfile "/var/run/redis-6380.pid"#进程端口号
port 6380
#日志记录文件
logfile "/opt/redis/log/redis-6380.log"#数据记录文件
dbfilename "dump-6380.rdb"#追加文件名称
appendfilename "appendonly-6380.aof"#下面的配置无需在 6379 里配置#备份服务器从属于 6379 推荐配置配局域网 IP
slaveof 192.168.40.100 6379
6.1.1.启动
cd /usr/local/redis/bin
./redis-server /opt/redis/conf/redis-6379.conf
./redis-server /opt/redis/conf/redis-6380.conf
./redis-server /opt/redis/conf/redis-6381.conf
6.1.2.进入客户端
#分别在不同会话打开
./redis-cli -p 6379 -a root
./redis-cli -p 6380 -a root
./redis-cli -p 6381 -a root
6.1.3.主从状态查看
info replication
6.1.4.读写操作
主服务器可读可写从服务器只读
在主服务器设置的值,在从服务器可以读取。
但是从服务器不能写,只能读。
6.2.哨兵配置
6.2.1.简介
当主服务器宕机后,需要手动把一台从服务器切换为主从服务器,这就需要人工干预,既费时费力,还会造成一段时间内服务不可用,这不是一种推荐的方式,因此笔者没有介绍主从切换技术。
更多的时候,我们优先考虑哨兵模式,它是当前企业应用的主流方式。
Redis可以存在多台服务器,并且实现了主从复制的功能。哨兵模式是一种特殊的模式,首先 Redis 提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。
其原理是哨兵通过发送命令,等待 Redis 服务器响应,从而监控运行的多个 Redis 实例
这里的哨兵有两个作用:
- 通过发送命令,让 Redis 服务器返回监测其运行状态,包括主服务器和从服务器。
- 当哨兵监测到 master 宕机,会自动将 slave 切换成 master,然后通过发布订阅模式通知到其他的从服务器,修改配置文件,让它们切换主机。
6.2.2.哨兵的安装
cd /root/redisfile/redis-5.0.3
# 把sentinel.conf复制到/opt/redis/conf/目录下cp sentinel.conf /opt/redis/conf/
cd /opt/redis/conf
#改名mv sentinel.conf sentinel-common.conf
#编辑配置文件
vim sentinel-common.conf
- 把port注释掉
- 把daomonize后台启动设置为yes
- 把pidfile进程文件注释掉
- 把logfile日志文件注释掉
- 修改哨兵模式的iip改成redis服务器的ip
- 主服务器的密码,这里的root是密码
- 在主服务器30秒没有ping-pong响应时,重新选取
- 当选取服务器超过180秒都没有超过。那么将放弃此次选取。进行新的选取
touch sentinel-26379.conf
touch sentinel-26380.conf
touch sentinel-26381.conf
#分别配置
vim sentinel-26379.conf
vim sentinel-26380.conf
vim sentinel-26381.conf
- sentinel-26379.conf
#引用公共配置
include /opt/redis/conf/sentinel-common.conf
#进程端口号
port 26379
#进程编号记录文件
pidfile /var/run/sentinel-26379.pid
#日志记录文件(为了方便查看日志,先注释掉,搭好环境后再打开)
logfile "/opt/redis/log/sentinel-26379.log"
- sentinel-26380.conf
#引用公共配置
include /opt/redis/conf/sentinel-common.conf
#进程端口号
port 26380
#进程编号记录文件
pidfile /var/run/sentinel-26380.pid
#日志记录文件(为了方便查看日志,先注释掉,搭好环境后再打开)
logfile "/opt/redis/log/sentinel-26380.log"
- sentinel-26381.conf
#引用公共配置
include /opt/redis/conf/sentinel-common.conf
#进程端口号
port 26381
#进程编号记录文件
pidfile /var/run/sentinel-26381.pid
#日志记录文件(为了方便查看日志,先注释掉,搭好环境后再打开)
logfile "/opt/redis/log/sentinel-26381.log"
6.2.3.主备切换
cd /usr/local/redis/bin
# 启动
./redis-sentinel /opt/redis/conf/sentinel-26379.conf
./redis-sentinel /opt/redis/conf/sentinel-26380.conf
./redis-sentinel /opt/redis/conf/sentinel-26381.conf
# 查看是否启动成功ps -ef|grep redis
tail -f /opt/redis/log/sentinel-26379.log
- 将主服务器杀死
ps -ef|grep redis
kill 3113
- 查看6379的log
# 查看服务器状态
./redis-cli -p 6379 -a root
info replication
- 重启6379服务器
./redis-server /opt/redis/conf/redis-6379.conf
- 6379的状态- 6380的状态
此时6380是主服务器
在6380设置一个值,在6379可以读取,但是6379设置一个值显示ERROR,因为此时6379是从服务器,6379是只读的状态了。
7.SpringDataRedis
SpringDataRedis是Spring大家族中的一个成员,提供了在srping应用中通过简单的配置访问redis服务,对reids底层开发包(Jedis, JRedis, and RJC)进行了高度封装,RedisTemplate提供了redis各种操作、异常处理及序列化,支持发布订阅,并对spring 3.1 cache进行了实现。 spring-data-redis针对jedis提供了如下功能:
- 连接池自动管理,提供了一个高度封装的“RedisTemplate”类
- 针对jedis客户端中大量api进行了归类封装,将同一类型操作封装为operation接口
7.1.创建项目
7.1.1.新建项目
7.1.2.pom.xml配置文件
<?xml version="1.0" encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.xmj</groupId><artifactId>springdataredis-demo</artifactId><version>0.0.1-SNAPSHOT</version><name>springdataredis-demo</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>2.3.7.RELEASE</spring-boot.version></properties><dependencies><!-- SpringDataRedis依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- commons-pool2 对象连接池依赖--><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><dependency><!-- web组件--><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- test组件--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><source>1.8</source><target>1.8</target><encoding>UTF-8</encoding></configuration></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>2.3.7.RELEASE</version><configuration><mainClass>com.xmj.springdataredisdemo.SpringdataredisDemoApplication</mainClass></configuration><executions><execution><id>repackage</id><goals><goal>repackage</goal></goals></execution></executions></plugin></plugins></build></project>
7.1.3.application.yml配置文件
spring:redis:# Redis服务器地址host: 192.168.77.134
# Redis服务器端口port:6380# Redis服务器密码password: root
# Redis服务器数据库database:0# 连接超时时间timeout: 10000ms
lettuce:pool:#最大连接数max-active:1024# #最大连接阻塞等待时间,单位毫秒,默认-1msmax-wait: 10000ms
##最大空闲连接,默认8max-idle:200##最小空闲连接,默认0min-idle:5
7.1.4.测试连接
package com.xmj.springdataredisdemo;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.core.RedisTemplate;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.core.ValueOperations;@SpringBootTestclassSpringdataredisDemoApplicationTests{@Autowiredprivate RedisTemplate<String,String> redisTemplate;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@TestpublicvoidinitCoon(){
ValueOperations ops = redisTemplate.opsForValue();
ops.set("name","zhangsan");
System.out.println((String) ops.get("name"));
ValueOperations<String, String> stringStringValueOperations = stringRedisTemplate.opsForValue();
stringStringValueOperations.set("age","20");
String age = stringStringValueOperations.get("age");
System.out.println(age);}}
7.2.SpringDataRedis序列化模板
7.2.1.自定义模板解决序列化问题
默认情况下的模板RedisTemplate<Object, Object>,默认序列化使用的是JdkserializationRedisSerializer,存储二进制字节码。这时需要自定义模板,半自定义模板后又想存储String 字符串时,可以使StringRedisTemplate的方式,他们俩并不冲突。
序列化问题:
要把 domain object做为key-value对保存在redis 中,就必须要解决对象的序列化问题。Spring Data Redis给我们提供了一些现成的方案:
jdkserializationRedisSerializer使用JDK提供的序列化功能。优点是反序列化时不需要提供类型信息(class),但缺点是序列化后的结果非常庞大,是]SON格式的5倍左右,这样就会消耗Redis 服务器的大星内存。
lackson23sonRedisSerializer使用Jackson库将对象序列化为SON字符串。优点是速度快,序列化后的字符串短小精悍。但缺点也非常致命,那就是此类的构造函数中有一个类型参数,必须提供要序列化对象的类型信息(.class对象)。通过查看源代码,发现其只在反序列化过程中用到了类型信息。
Generic]ackson2]sonRedisserializer通用型序列化,这种序列化方式不用自己手动指定对象的Class。
RedisConfig配置类
package com.xmj.springdataredisdemo.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.lettuce.LettuceConnection;import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;import org.springframework.data.redis.serializer.StringRedisSerializer;/**
* @author 冰咖啡
* Date:2022/3/9 17:11
* Description:
*/@ConfigurationpublicclassRedisConfig{@Beanpublic RedisTemplate<String,Object>redisTemplate(LettuceConnectionFactory lettuceConnectionFactory){
RedisTemplate<String,Object> redisTemplate =newRedisTemplate<>();//为String类型的key设置序列化
redisTemplate.setKeySerializer(newStringRedisSerializer());//为string类型的value设置序列化
redisTemplate.setValueSerializer(newGenericJackson2JsonRedisSerializer());//为hash类型的key设置序列化
redisTemplate.setHashKeySerializer(newStringRedisSerializer());//为hash类型的value设置序列化
redisTemplate.setHashValueSerializer(newGenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(lettuceConnectionFactory);return redisTemplate;}}
User实体类
package com.xmj.springdataredisdemo.pojo;import java.io.Serializable;/**
* @author 冰咖啡
* Date:2022/3/9 17:18
* Description:
*/publicclassUserimplementsSerializable{private Integer id;private String name;private Integer age;@Overridepublic String toString(){return"User{"+"id="+ id +", name='"+ name +'\''+", age="+ age +'}';}public Integer getId(){return id;}publicvoidsetId(Integer id){this.id = id;}public String getName(){return name;}publicvoidsetName(String name){this.name = name;}public Integer getAge(){return age;}publicvoidsetAge(Integer age){this.age = age;}}
测试
/**
* 测试序列化
*/@TestpublicvoidtestSerial(){
User user =newUser();
user.setId(1);
user.setName("zhangsan");
user.setAge(20);
ValueOperations ops = redisTemplate.opsForValue();
ops.set("user",user);
Object user1 = ops.get("user");
System.out.println(user1);}
SpringDataRedis操作string数据类型
@TestpublicvoidtestString(){
ValueOperations ops = redisTemplate.opsForValue();//添加一条数据
ops.set("name","zhangsan");//获取一条数据
Object name = ops.get("name");
System.out.println(name);//层级关系,目录形式存储数据
ops.set("user:01","list");//添加多条数据
Map<String,String> map=newHashMap<>();
map.put("age","20");
map.put("address","sh");
ops.multiSet(map);//
List<String> keys =newArrayList<>();
keys.add("name");
keys.add("age");
keys.add("address");
List list = ops.multiGet(keys);
list.forEach(System.out::println);//删除数据
redisTemplate.delete("name");
list = ops.multiGet(keys);
list.forEach(System.out::println);}
SpringDataRedis操作hash数据类型
@TestpublicvoidtestHash(){
HashOperations hashOperations = redisTemplate.opsForHash();/**
* 添加一条数据
* 第一个数据:redis的key
* 第二个数据:hash的key
* 第三个数据:hash的value
*/
hashOperations.put("user","name","zhangsan");/**
* 获取一条数据
* 第一个参数:redis的key
* 第二个参数:hash的key
*/
hashOperations.get("user","name");//添加多条数据
Map<String,String> map=newHashMap<>();
map.put("age","20");
map.put("address","sh");
hashOperations.putAll("user",map);//获取多条数据
List<String> keys =newArrayList<>();
keys.add("name");
keys.add("age");
keys.add("address");
List user = hashOperations.multiGet("user", keys);
user.forEach(System.out::println);//获取hash类型的所有数据
Map<String, String> entries = hashOperations.entries("user");
entries.entrySet().forEach(e->{
System.out.println(e.getKey()+"--->"+e.getValue());});//hash的删除
hashOperations.delete("user2","name","age");}
SpringDataRedis操作list
@TestpublicvoidtestList(){
ListOperations listOperations = redisTemplate.opsForList();//左添加
listOperations.leftPush("students","wangwu");
listOperations.leftPush("students","lisi");/**
* 左添加
* 第一个参数:redis的key
* 第二个参数:被左添加的数据
* 第三个参数:添加的数据,添加到第二个数据左边
*/
listOperations.leftPush("students","wangwu","zhangsan");//右添加
listOperations.rightPush("students","zhaoliu");
listOperations.rightPush("students","tianqi");//获取数据
List list = listOperations.range("students",0,2);
list.forEach(System.out::println);//获取总条数
Long size = listOperations.size("students");
System.out.println(size);//删除数据
listOperations.remove("students",1,"lisi");//左弹出
listOperations.leftPop("students");//右弹出
listOperations.rightPop("students");
list = listOperations.range("students",0,2);
list.forEach(System.out::println);}
SpringDataRedis操作set数据类型
@TestpublicvoidtestSet(){
SetOperations setOperations = redisTemplate.opsForSet();//添加数据
String[] letters =newString[]{"aaa","bbb","ccc","ddd"};
setOperations.add("letters",letters);
setOperations.add("letters",letters);//获取数据
Set set = setOperations.members("letters");
set.forEach(System.out::println);//删除数据
setOperations.remove("letters","aaa","bbb");
set = setOperations.members("letters");
set.forEach(System.out::println);}
SpringDataRedis操作sortedset数据类型
@TestpublicvoidtestSortedSet(){
ZSetOperations zSetOperations = redisTemplate.opsForZSet();//添加数据
ZSetOperations.TypedTuple<Object> objectTypedTuple1 =newDefaultTypedTuple<>("zhangsan",3D);
ZSetOperations.TypedTuple<Object> objectTypedTuple2 =newDefaultTypedTuple<>("lisi",4D);
ZSetOperations.TypedTuple<Object> objectTypedTuple3 =newDefaultTypedTuple<>("wangwu",5D);
ZSetOperations.TypedTuple<Object> objectTypedTuple4 =newDefaultTypedTuple<>("zhaoliu",6D);
ZSetOperations.TypedTuple<Object> objectTypedTuple5 =newDefaultTypedTuple<>("tianqi",7D);
Set<ZSetOperations.TypedTuple<Object>> tuples =newHashSet<>();
tuples.add(objectTypedTuple1);
tuples.add(objectTypedTuple2);
tuples.add(objectTypedTuple3);
tuples.add(objectTypedTuple4);
tuples.add(objectTypedTuple5);
zSetOperations.add("score",tuples);
Set score = zSetOperations.range("score",0,4);
score.forEach(System.out::println);
Long size = zSetOperations.size("score");
System.out.println(size);
zSetOperations.remove("score","zhangsan","lisi");
score = zSetOperations.range("score",0,4);
score.forEach(System.out::println);}
SpringData获取所有key+设置key失效时间
/**
* 获取所有的key
*/@TestpublicvoidtestAllKey(){
Set<String> keys = redisTemplate.keys("*");
keys.forEach(System.out::println);}
/**
* 失效时间
*/@TestpublicvoidtestExpire(){// ValueOperations ops = redisTemplate.opsForValue();// ///方法一,添加key的时候设置失效时间// ops.set("code","test",30, TimeUnit.SECONDS);
redisTemplate.expire("name",30,TimeUnit.SECONDS);//查看失效时间
Long expire = redisTemplate.getExpire("code");
System.out.println(expire);
expire = redisTemplate.getExpire("name");
System.out.println(expire);}
SpringData整合哨兵模式
- appllication.yml配置
spring:redis:# Redis服务器地址host: 192.168.77.134
# Redis服务器端口port:6380# Redis服务器密码password: root
# Redis服务器数据库database:0# 连接超时时间timeout: 10000ms
lettuce:pool:#最大连接数max-active:1024# #最大连接阻塞等待时间,单位毫秒,默认-1msmax-wait: 10000ms
##最大空闲连接,默认8max-idle:200##最小空闲连接,默认0min-idle:5# 哨兵模式sentinel:# 主节点名称master: mymaster
# 节点nodes: 192.168.77.134:26379,192.168.77.134:26380,192.168.77.134:26381
- RedisConfig配置类的RedisSentinelConfiguration方法
@Beadnpublic RedisSentinelConfiguration redisSentinelConfiguration(){ RedisSentinelConfiguration redisSentinelConfiguration =newRedisSentinelConfiguration()//主节点名称.master("mymaster")//哨兵.sentinel("192.168.77.134",26379).sentinel("192.168.77.134",26380).sentinel("192.168.77.134",26381); redisSentinelConfiguration.setPassword("root");return redisSentinelConfiguration;}
8.如何应对缓存穿透、缓存击穿、缓存雪崩问题
8.1.Key的过期淘汰机制
Redis可以对存储在Redis中的缓存数据设置过期时间,比如我们获取的短信验证码一般十分钟过期,我们这时候就需要在验证码存进Redis时添加一个key的过期时间,但是这里有一个需要格外注意的问题就是:并非key过期时间到了就一定会被Redis给删除。
8.1.1.定期删除
Redis默认是每隔100ms就随机抽取一些设置了过期时间的Key,检查其是否过期,如果过期就删除。为什么是随机抽取而不是检查所有key?因为你如果设置的key成千上万,每100毫秒都将所有存在的key检查一遍,会给CPU带来比较大的压力。
8.1.2.惰性删除
定期删除由于是随机抽取可能会导致很多过期Key到了过期时间并没有被删除。所以用户在从缓存获取数据的时候,redis会检查这个key是否过期了,如果过期就删除这个key。这时候就会在查询的时候将过期key从缓存中清除。
8.1.3.内存淘汰机制
仅仅使用定期删除+惰性删除机制还是会留下一个严重的隐患:如果定期删除留下了很多已经过期的key,而且用户长时间都没有使用过这些过期key,导致过期key无法被惰性删除,从而导致过期key一直堆积在内存里,最终造成Redis内存块被消耗殆尽。那这个问题如何解决呢?这个时候Redis内存淘汰机制应运而生了。Redis内存淘汰机制提供了6种数据淘汰策略:
- volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
- volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
- volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
- allkeys-lru:当内存不足以容纳新写入数据时移除最近最少使用的key。
- allkeys-random:从数据集中任意选择数据淘汰。
- no-enviction:当内存不足以容纳新写入数据时,新写入操作会报错。
一般情况下,推荐使用volatile-lru策略,对于配置信息等重要数据,不应该设置过期时间,这样Redis就永远不会淘汰这些重要数据。对于一般数据可以添加一个缓存时间,当数据失效则请求会从DB中获取并重新存入Redis中。
8.2.缓存击穿
首先我们来看下请求是如何取到数据的:当接收到用户请求,首先先尝试从Redis缓存中获取到数据,如果缓存中能取到数据则直接返回结果,当缓存中不存在数据时从DB获取数据,如果数据库成功取到数据,则更新Redis,然后返回数据,如果DB无数
8.2.1.定义
高并发的情况下,某个热门key突然过期,导致大星请求在Redis未找到缓存数据,进而全部去访问DB请求数据,引起DB压力瞬间增大。
8.2.2.解决方案
缓存击穿的情况下一般不容易造成DB的宕机,只是会造成对DB的周期性压力。对缓存击穿的解决方案—般可以这样:
- Redis中的数据不设置过期时间,然后在缓存的对象上添加一个属性标识过期时间,每次获取到数据时,校验对象中的过期时间属性,如果数据即将过期,则异步发起一个线程主动更新缓存中的数据。但是这种方案可能会导致有些请求会拿到过期的值,就得看业务能否可以接受,
- 如果要求数据必须是新数据,则最好的方案则为热点数据设置为永不过期,然后加一个互斥锁保证缓存的单线程写。
8.3.缓存穿透
8.3.1.定义
缓存穿透是指查询缓存和DB中都不存在的数据。比如通过id查询商品信息,id一般大于0,攻击者会故意传id为-1去查询,由于缓存是不命中则从DB中获取数据,这将会导致每次缓存都不命中数据导致每个请求都访问DB,造成缓存穿透。
8.3.2.解决方案:
- 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠
- 段时间重试
- 采用异步更新策略,无论key是否取到值,都直接返回。value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。
- 提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的key。迅速判断出,请求所携带的Key是否合法有效。如果不合法,则直接返回。
- 如果从数据库查询的对象为空,也放入缓存,只是设定的缓存过期时间较短,比如设置为60秒
8.4.缓存雪崩
8.4.1.定义
缓存中如果大量缓存在一段时间内集中过期了,这时候会发生大量的缓存击穿现象,所有的请求都落在了DB上,由于查询数据量巨大,引起DB压力过大甚至导致DB宕机。
8.4.2.解决方案
- 给缓存的失效时间,加上一个随机值,避免集体失效。如果Redis是集群部署,将热点数据均匀分布在不同的Redis库中也能避免全部失效的问题
- 使用互斥锁,但是该方案吞吐量明显下降了。
- 设置热点数据永远不过期。
- 双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间。自己做缓存预热操作。然后细分以下几个小点 - 1.从缓存A读数据库,有则直接返回- 2.A没有数据,直接从B读数据,直接返回,并且异步启动一个更新线程。- 3.更新线程同时更新缓存A和缓存B。 到了就一定会被Redis给删除。
定期删除
Redis默认是每隔100ms就随机抽取一些设置了过期时间的Key,检查其是否过期,如果过期就删除。为什么是随机抽取而不是检查所有key?因为你如果设置的key成千上万,每100毫秒都将所有存在的key检查一遍,会给CPU带来比较大的压力。
惰性删除
定期删除由于是随机抽取可能会导致很多过期Key到了过期时间并没有被删除。所以用户在从缓存获取数据的时候,redis会检查这个key是否过期了,如果过期就删除这个key。这时候就会在查询的时候将过期key从缓存中清除。
内存淘汰机制
仅仅使用定期删除+惰性删除机制还是会留下一个严重的隐患:如果定期删除留下了很多已经过期的key,而且用户长时间都没有使用过这些过期key,导致过期key无法被惰性删除,从而导致过期key一直堆积在内存里,最终造成Redis内存块被消耗殆尽。那这个问题如何解决呢?这个时候Redis内存淘汰机制应运而生了。Redis内存淘汰机制提供了6种数据淘汰策略:
- volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
- volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
- volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
- allkeys-lru:当内存不足以容纳新写入数据时移除最近最少使用的key。
- allkeys-random:从数据集中任意选择数据淘汰。
- no-enviction:当内存不足以容纳新写入数据时,新写入操作会报错。
一般情况下,推荐使用volatile-lru策略,对于配置信息等重要数据,不应该设置过期时间,这样Redis就永远不会淘汰这些重要数据。对于一般数据可以添加一个缓存时间,当数据失效则请求会从DB中获取并重新存入Redis中。
版权归原作者 冰咖啡iii 所有, 如有侵权,请联系我们删除。