Redis学习笔记
Redis
NoSQL的四大分类
NoSQL = Not Only SQL,泛指非关系型数据库。
KV键值对
- 新浪(Redis)
- 美团(Redis + Tair)
- 阿里、百度(Redis + memecache)
文档型数据库
- MongoDB —— MongoDB是一个基于分布式文件存储的数据库,C++编写,主要用来处理大量的文档。MongoDB是一个介于关系型数据库和非关系型数据库中间的产品。MongoDB是非关系型数据库中功能最丰富,最像关系型数据库的一个。
- ConthDB
列存储数据库
- HBase
- 分布式文件系统
图关系数据库
- NEO4J
Redis(Remote Dictionary Server)
简介
- Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
- Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
- 免费和开源,是当下最热门的NoSQL技术之一,也被人们称之为结构化数据库。
用途
Redis可用作数据库、缓存和消息中间件MQ
。具体用途如下:
- 内存存储并持久化(RDB和AOF)
- 效率高,可用于高速缓存
- 发布订阅系统
- 地图信息分析
- 计时器,计数器(如记录浏览量)
特性
Redis官方推荐在Linux服务器上搭建。
Redis是单线程
的,它很快,基于内存操作
。
CPU不是Redis的性能瓶颈,Redis的瓶颈是机器的内存和网络带宽。
- 多样的数据类型
- 持久化
- 集群
- 事务
为什么Redis采用单线程还这么快?
Redis是将所有的数据全部放到内存
中的,所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换:耗时的操作),对于内存系统来说,如果没有上下文切换效率就是最高的。多次读写都是在一个CPU上的,在内存情况下,这个就是最佳的方案。
Redis的安装
客户端连接Redis
redis-cli.exe -h 127.0.0.1 -p 6379
Redis压力测试
100000个写入请求,100个并发客户端
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
Redis基本知识
Redis有16个数据库,默认使用第0个,可以使用select进行切换。
基本指令
指令 | 作用 |
---|---|
keys * | 查看数据库所有的key |
flushdb | 清空当前数据库 |
flushall | 清空所有数据库 |
set [key] [value] | 设置键值对,重复设置一个键会将原来的值给覆盖掉 |
get [key] | 通过键名获取值 |
EXISTS [key] | 检查键是否存在 |
move [key] [0-15] | 将键移动到某个数据库 |
expire [key] [time] | 将键设置为n秒后过期 |
ttl [key] | 查看键剩余存活时间 |
type [key] | 查看键类型 |
Redis的五大数据类型
String
ps:value除了字符串还可以是数字
使用场景
- 计数器
- 统计多单位的数目
- 对象缓存存储
指令 | 作用 |
---|---|
append [key] [value] |
在键上追加值,若此键不存在,相当于set命令 |
strlen [key] |
获取键的值的长度 |
incr [key] |
键的值加1 |
decr [key] |
键的值减1 |
incrby [key] [step] |
键的值加n |
decrby [key] [step] |
键的值减n |
getrange [key] [start] [end] |
截取字符串。getrange [key] 0 -1 等同get命令 |
setrange [key] [location] [value] |
从指定位置开始替换 |
setex [key] [time] [value] |
可理解为set和expire连用 |
setnx [key] [value] |
若键不存在,创建并设置值(分布式锁中常用) |
mset [key1] [value1] [key2] [value2]...... |
批量设置 |
mget[key1] [key2]...... |
批量取值 |
msetnx [key1] [value1] [key2] [value2]...... |
若键不存在,创建并设置值(批量),具有原子性(若其中一个设置失败则整体失败) |
getset [key] [value] |
先get,返回get到的值,将value覆盖原有值 |
List
特点
- 值有序
- 可重
- 指令首的"l"含义:除了push和pop操作代表头尾的意思,其他都是list的意思
指令 | 作用 |
---|---|
lpush [listname] [value] |
往列表头部插入数据 |
lrange [listname] [range(0 1)] |
获取列表中某个范围的值(获取列表中第0和第1个值) |
rpush [listname] [value] |
往列表尾部插入数据 |
lpop [listname] |
移除列表头元素 |
rpop [listname] |
移除列表尾元素 |
lindex [listname] [index] |
通过下标获取值 |
llen [listname] |
获取列表长度 |
lrem [listname] [count] [value] |
移除n个重复的指定值 |
ltrim [listname] [range(1 2)] |
截取范围内值(截取列表中第1和第2个值)(注:原list将被改变,未被截取到的数据将被删除) |
rpoplpush [source] [destination] |
移除列表的最后一个元素并将其插入另一个列表的头部 |
lset [listname] [index] [value] |
修改下标值 |
exist [listname] |
判断某列表是否存在 |
`linsert [listname] [before | after] [value] [value]` |
底层实现
:快速链表
使用场景
:消息队列、栈
Set
特点
- 值无序
- 不可重复
- 指令首"s"的含义:代表set
指令 | 作用 |
---|---|
sadd [setname] [value] |
往集合中添加值 |
smembers [setname] |
查看某集合 |
sismember [setname] [value] |
判断集合中是否存在该值 (返回0表示不存在,返回1表示存在) |
scard [setname] |
获取集合元素个数 |
srem [setname] [value] |
移除集合中的指定元素 |
srandmember [setname] |
随机取出一个元素 |
srandmember [setname] [count] |
随机取出若干个元 |
spop [setname] |
移除一个元素 |
smove [source] [destination] [value] |
移除列表的一个指定元素并将其添加到另一个集合 |
diff [set1] [set2] |
找出属于set1但不属于set2的元素(求差集 ) |
sinter [set1] [set2] |
求交集 |
sunion [set1] [set2] |
求并集 |
使用场景
- 集合操作(交、并、差)
Hash
使用场景
- 对象存储
- 经常变更的K-V数据,例如用户信息
指令 | 作用 |
---|---|
hset [hashname] [key] [value] |
存键值对 |
hget [hashname] [key] |
由键取值 |
hmset [hashname] [key1] [value1] [key2][value2]...... |
批量存键值对 |
hmget [hashname] [key1] [key2]...... |
根据键批量取值 |
hgetall [hashname] |
获取所有键值对 |
hdel [hashname] [key] |
删除指定键值对 |
hlen [hashname] |
获取哈希表长度(即有几个键值对) |
hexist [hashname] [key] |
判断表中键值对是否存在(返回0表示不存在,返回1表示存在) |
hkeys [hashname] |
获取表中所有的key |
hvals [hashname] |
获取表中所有的value |
hincrby [hashname] [key] [increase] |
将key对应的value增加若干大小(可通过负数实现减少) |
hsetnx [hashname] [key] [value] |
若键不存在,创建并设置值 |
Zset
特点
- 在set基础上增加排序权重
- 有序集合
指令 | 作用 |
---|---|
zadd [setname] [score] [value] |
往集合中添加值 |
zrange [setname] [range(0 1)] |
获取集合中某个范围的值(获取集合中第0和第1个值) |
zrangebyscore [setname] -inf +inf |
按分数从小到大获取集合中的值 |
zrevrangebyscore [setname] +inf -inf |
按分数从大到小获取集合中的值 |
zrangebyscore [setname] -inf +inf withscores |
按分数从小到大获取集合中的值并显示分数 |
zrem [setname] [value] |
移除集合中的指定元素 |
zcard [setname] |
获取集合元素个数 |
zcount [setname] [scorerange(1 3)] |
某分数范围内的值的个数(分数在[1,3]范围内的值的个数) |
Redis三种特殊数据类型
geospatial
底层基于Zset
指令 | 作用 |
---|---|
geoadd [key] [longitude] [latitude] [cityname] |
添加地理位置(经纬度) |
geopos [key] [cityname] |
获取指定城市的地理位置 |
geodist [key] [cityname1] [cityname2] [unit] |
查询两地的直线距离(默认单位为米) |
georadius [key] [longitude] [latitude] [radius] [unit] [count] |
以某处为中心查询其方圆若干(单位)的城市(可限制显示数量) |
georadius [key] [longitude] [latitude] [radius] [unit] withdist [count] |
以某处为中心查询其方圆若干(单位)的城市(带直线距离) |
georadius [key] [longitude] [latitude] [radius] [unit] withcoord [count] |
以某处为中心查询其方圆若干(单位)的城市(带经纬度) |
georadiusbymember [key] [cityname] [radius] [unit] |
以某城市为中心查询其方圆若干(单位)的城市 |
geohash [key] [cityname] |
返回城市的哈希值(二维的经纬度转换成一维的字符串,两个字符串显示越靠近说明两地距离越接近) |
Hyperloglog
概述
它是用作基数统计的算法。基数计算(cardinality counting)指的是统计一批数据中的不重复元素的个数。
应用场景
- 网页的UV(用户访问量),同一个人访问网站多次,视为一次访问
优点
占用的内存是固定的,264个不同元素的数据,只需要废12KB内存。所以如果要从内存角度来比较的话,Hyperloglog是首选。
缺点
具有0.81%的错误率
指令 | 作用 |
---|---|
pfadd [key] [value1] [value2]...... |
存放数据 |
pfcount [key] |
统计元素数量 |
pfmerge [newkey] [key1] [key2]...... |
合并若干key到一个新的key |
Bitmap
概述
位图,即位存储,操作二进制位来进行记录,只有0和1两个状态。
应用场景
用户状态统计。用户活跃度,登录状态,打卡记录。拥有两个状态的用户信息的存储都可以用Bitmap。
指令 | 作用 |
---|---|
setbit [key] [offset] [value] |
存值 |
getbit [key] [offset] |
取值 |
bitcount [key] |
统计1的个数 |
Redis基本事务操作
Redis单条命令是保证原子性的,但是它的事务不保证原子性!
Redis事务本质
一组命令的集合!一个事务中的所有命令都会被序列化,在事务执行过程中,会按照一次性、顺序性、排他性等原则执行一系列的命令!
Redis事务没有隔离级别的概念!
所有的命令在事务中,并没有直接被执行!只有发起执行命令的时候才会执行!Exec
事务
- 开启事务 —— multi
- 命令入队 —— 一系列命令
- 执行事务 —— exec
- 放弃事务 —— discard
异常
- 编译型异常 —— 命令使用错误,事务中其他命令都不会被执行
- 运行时异常 —— 命令没有语法错误,但是运行时产生错误,除了报错的命令外,事务中其他命令依然会被执行
Redis监控——悲观锁和乐观锁
悲观锁
—— 认为什么时候都可能出现问题,因此无论做什么都加锁
乐观锁(常用)
—— 认为什么时候不会出现问题,不会对任何操作上锁。只会更新数据的时候去判断一下,在此期间是否有人修改过这个数据,步骤如下:
- 获取version (
watch
data) - 更新时比对version,若version发生变动,则告诉事务,事务执行失败,执行失败后先
unwatch data
,再watch data
更新version
Jedis —— Java连接开发工具
连接
1. 导入包
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
2.连接Redis
package com.youzikeji.redis;
import redis.clients.jedis.Jedis;
public class TestJedis {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
System.out.println(jedis.ping());
}
}
3.关闭连接
jedis.close();
事务
package com.youzikeji.redis;
import com.alibaba.fastjson.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
public class TestTransaction {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.flushDB();
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", "caoyusang");
jsonObject.put("age", 23);
String result = jsonObject.toString();
System.out.println(result);
// 开启事务
Transaction multi = jedis.multi();
// jedis.watch(result); // 开启监控——乐观锁
try {
multi.set("user1", result);
multi.lpush("list",result);
// int i= 1 / 0; // 代码出现异常,执行失败!
multi.exec(); // 执行事务
} catch (Exception e) {
multi.discard(); // 放弃事务
} finally {
System.out.println(jedis.get("user1"));
System.out.println(jedis.lpop("list"));
jedis.close(); // 关闭连接
}
}
}
SpringBoot整合Redis
简介
SpringBoot操作数据:spring-data (jpa jdbc mongodb redis elasticsearch)!
SpringBoot2.x之后,原来使用的Jedis被替换为了lettuce。
Jedis
:采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用jedis pool 连接池! 更像 BIO 模式
lettuce
: 采用netty,实例可以再多个线程中进行共享,不存在线程不安全的情况!可以减少线程数据了,更像NIO模式
源码分析
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
) // 我们可以自己定义一个redisTemplate来替换这个默认的!
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 默认的RedisTemplate 没有过多的设置,redis对象都是需要序列化的!
// 两个泛型都是object,object的类型,我们后续使用需要强制转换成<String,object>
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean // 由于String 是redis中最常使用的类型,所以说单独提出来了一个bean!
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
依赖导入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置连接
spring.redis.host=127.0.0.1
spring.redis.port=6379
自定义RedisTemplate
package com.youzikeji.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
// 为了自己开发方便,一般直接使用<String,Object>
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Json序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(om);
// String的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
测试
package com.youzikeji;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
@SpringBootTest
class SpringbootApplicationTests {
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate redisTemplate;
// redisTemplate 操作不同的数据类型,api和我们的命令一样
// opsForValue 操作字符串 类似 String
// opsForList 操作list 类似list
// 。。。依此类推
// 除了基本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务和基本的crud
// 获取redis 的连接对象
// RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
// connection.flushAll();
// connection.flushDb();
@Test
void contextLoads() {
redisTemplate.opsForValue().set("name", "caoyusang");
System.out.println(redisTemplate.opsForValue().get("name"));
}
}
Redis配置文件详解
unit
内存大小
include
引入其他配置文件的内容
网络
bind 127.0.0.1 # 绑定的ip
protected-mode yes # 保护模式
port 6379 # 端口设置
通用
daemonize yes # 以守护进程的方式运行,默认是no,我们需要自己开启为yes!
pidfile /var/run/redis_6379.pid # 如果以后台的方式运行,我们就需要指定一个pid文件!
# 日志
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably) 生产环境
# warning (only very important / critical messages are logged)
loglevel notice
logfile "" # 日志文件位置
databases 16 # 默认数据库个数 16个
always-show-logo yes # 是否总是显示logo
快照
redis是内存数据库,如果没有持久化,那么数据断电及失!
快照是持久化的一种方式,若在规定的时间内,redis-server执行了多少次操作,则会持久化到文件.rdb.aof
# 如果900s内,如果至少有1个key进行了修改,我们就进行持久化操作
save 900 1
# 如果300s内,如果至少有10个key进行了修改,我们就进行持久化操作
save 300 10
# 如果60s内,如果至少有10000个key进行了修改,我们就进行持久化操作
save 60 10000
stop-writes-on-bgsave-error yes # 持久化如果出错,是否还要继续工作!
rdbcompression yes # 是否压缩 rdb 文件,需要消耗一些cpu资源!
rdbchecksum yes # 保存rdb文件的时候,进行错误的检查校验!
dir ./ # rdb 文件保存的目录!
安全
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) ""
127.0.0.1:6379> config set requirepass "123456"
OK
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "123456"
用户连接限制
# maxclients 10000 设置能连接上redis的最大客户端的数量
# maxmemory <bytes> redis 配置最大的内存容量
# maxmemory-policy noeviction 内存达到上限之后的处理策略
1、volatile-lru:只对设置了过期时间的key进行LRU(默认值)
2、allkeys-lru : 删除lru算法的key
3、volatile-random:随机删除即将过期key
4、allkeys-random:随机删除
5、volatile-ttl : 删除即将过期的
6、noeviction : 永不过期,返回错误
AOF配置
appendonly no # 默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分所有的情况下,rdb完全够用!
appendfilename "appendonly.aof" # 持久化的文件的名字
# appendfsync always # 每次修改都会 sync,消耗性能
appendfsync everysec # 每秒执行一次 sync,可能会丢失这1s的数据
# appendfsync no # 不执行 sync ,这个时候操作系统自己同步数据,速度最快!
Redis持久化
RDB(Redis Database)
概述
在指定的时间间隔内将内存中的数据集快照写入磁盘,恢复数据时将快照文件直接读到内存里面。在创建快照之后,用户可以备份该快照,可以将快照复制到其他服务器以创建相同数据的服务器副本,或者在重启服务器后恢复数据。RDB是Redis默认的持久化方式。RDB文件默认为当前工作目录下的dump.rdb
,可以根据配置文件中的dbfilename
和dir
设置RDB的文件名和文件位置。
save和bgsave
执行save
和bgsave
命令,可以手动触发快照,生成RDB文件。
save | bgsave |
---|---|
同步操作,会阻塞服务器进程,服务器进程在RDB文件创建完成之前是不能处理任何的命令请求 | 异步操作,bgsave 命令会fork 一个子进程,然后该子进程会负责创建RDB文件,而服务器进程会继续处理命令请求 |
fork()
: 由操作系统提供的函数,用于创建当前进程的一个副本作为子进程。
bgsave命令fork的子进程会把数据集先写入临时文件,写入成功之后,再替换之前的RDB文件,用二进制压缩存储,这样可以保证RDB文件始终存储的是完整的持久化内容。
快照触发规则
- 手动执行save和bgsave命令
- 配置文件中设置
save <seconds> <changes>
规则,可以自动间隔性执行bgsave命令 - 执行flushall命令清空服务器数据
- 执行shutdown命令关闭Redis,会自动执行save命令
- 主从复制时,从库全量复制主库数据时,主库会执行bgsave命令
AOF(Append Only File)
概述
AOF持久化会把被执行的写命令写到AOF文件的末尾,记录数据的变化。默认情况下,Redis是没有开启AOF持久化的
,开启后,每执行一条更改
Redis数据的命令,都会把该命令追加到AOF文件中。
AOF实现
命令追加
服务器每执行一个写命令,都会把该命令以协议格式先追加到aof_buf缓存区的末尾
,而不是直接写入文件,避免每次有命令都直接写入硬盘,减少硬盘IO次数
。
文件写入与同步
将aof_buf
缓冲区的所有内容写入并同步到AOF文件,Redis支持多种写入同步策略:
appendfsync always | appendfsync everysec | appendfsync | |
---|---|---|---|
同步 | 即时同步 | 每秒同步一次 | 操作系统自己选择何时同步 |
缺点 | 消耗性能 | 至多会丢失一秒的数据 | 系统崩溃时,可能会丢失不定量的数据 |
优点 | 安全性最高 | 兼顾了数据安全和写入性能 |
不会影响Redis的性能 |
AOF重写
随着时间的推移,Redis执行的写命令会越来越多,AOF文件也会越来越大,过大的AOF文件可能会对Redis服务器造成影响,如果使用AOF文件来进行数据还原所需时间也会越长。
时间长了,AOF文件中通常会有一些冗余命令,比如:过期数据的命令、无效的命令(重复设置、删除)、多个命令可合并为一个命令(批处理命令
)。所以AOF文件是有精简压缩的空间
的。
AOF重写的目的就是减小AOF文件的体积,文件重写可分为手动触发和自动触发,手动触发执行bgrewriteaof
命令,该命令的执行跟 bgsave
触发快照时类似的,都是先fork
一个子进程做具体的工作;而自动触发会根据auto-aof-rewrite-percentage
和auto-aof-rewrite-min-size 64mb
配置来自动执行bgrewriteaof
命令。
# 表示当AOF文件的体积大于64MB,且AOF文件的体积比上一次重写后的体积大了一倍(100%)时,会执行bgrewriteaof命令
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
bgrewriteaof的重写流程
- 重写会有大量的写入操作,所以服务器进程会
fork
一个子进程来创建一个新的AOF文件。 - 在重写期间,服务器进程继续处理命令请求,如果有写入的命令,追加到
aof_buf
的同时,还会追加到aof_rewrite_buf
AOF重写缓冲区。 - 当子进程完成重写之后,会给父进程一个信号,然后父进程会把AOF重写缓冲区的内容写进新的AOF临时文件中,再对新的AOF文件改名完成替换,这样可以保证新的AOF文件与当前数据库数据的一致性。
RDB vs AOF
持久化方式 | RDB | AOF |
---|---|---|
启动优先级 | 低 | 高 |
体积 | 小 | 大 |
恢复速度 |
快 | 慢 |
数据安全性 |
会丢数据 | 由选择的aof文件写入同步策略决定 |
轻重 | 重 | 轻 |
Redis发布订阅
Redis缓存穿透和雪崩
缓存穿透
指查询一个缓存和数据库中都没有的数据
,由于大部分缓存策略是被动加载的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存
,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义
。用户不断发起请求,在流量大时,就可能对数据库形成巨大的压力,利用不存在的key频繁攻击应用也是很大的问题。
例如,用户查询一个 id = -1 的商品信息,一般数据库 id 值都是从 1 开始自增,很明显这条信息是不在数据库中,此时若用户不断发起请求的话,在流量大时,会给当前数据库的造成很大的并发访问压力。
缓存空对象
第一次请求缓存和数据库中都不存在 的信息,则从数据库中返回一个空对象
,并将这个空对象和请求关联起来存到缓存中
,当下次还是这个请求过来的时候,这时缓存就会命中,就直接从缓存中返回这个空对象,这样可以减少访问数据库的压力,提高当前数据库的访问性能。
该方法存在的问题
当有大量不存在的请求时,缓存中就会有许多空对象,占用许多内存空间,浪费资源
。
当然,可以给这些空对象设置过期时间
,让其在一段时间后得到自动清理。
接口层校验
接口层增加校验,比如用户鉴权校验,id根据数据场景做基础校验,id<=0的直接拦截。
布隆过滤器
布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中
。
把所有可能存在的请求值
都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在则判断缓存中是否存在对应的数据,存在则直接返回,不存在就再从数据库中找,数据库中找到对应的数据,则更新缓存,返回对应数据,如果数据库中也不存在对应数据,则返回空数据,加入布隆过滤器之后的缓存处理流程图如下:
该方法存在的问题 —— 误判
布隆过滤器可能会存在误判的情况:
-
布隆过滤器说某个元素存在,小概率会误判
-
布隆过滤器说某个元素不在,那么这个元素一定不在
缓存击穿
造成缓存击穿的原因有两个:
- 不经常被访问的“冷门”key,突然被大量用户请求访问
- 经常被访问的“热门”key,在其恰好过期时,有大量用户访问
常用解决方案 —— 加锁
对于key过期的时候,当key要查询数据库的时候加上一把锁,这时只能让第一个请求进行查询数据库,然后把从数据库中查询到的值存储到缓存中,对于剩下的相同的key,可以直接从缓存中获取即可。
缓存雪崩
描述的是这样一个场景:
缓存在同一时间大面积的失效
,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求
。
举个例子 :秒杀开始 12 个小时之前,我们统一存放了一批商品到 Redis 中,设置的缓存过期时间也是 12 个小时,那么秒杀开始的时候,这些秒杀的商品的访问直接就失效了。导致的情况就是,相应的请求直接就落到了数据库上,就像雪崩一样可怕。
解决方案
- Redis高可用 —— Redis有可能挂掉,多增加几台redis实例,(一主多从或者多主多从),这样一台挂掉之后其他的还可以继续工作,其实就是搭建集群。
- 限流降级 —— 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量,对某个key只允许一个线程查询数据和写缓存,其他线程等待。
- 数据预热 —— 在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key。
- 设置不同的过期时间 —— 让缓存失效的时间点尽量均匀。