一、Strings 类型
set key value //设置key值为value
del key //删除key
get key //获取key
incr key //key自增1
decr key //key自减1
ttl key //查看key存活时间
setnx key value //判断key有没有key,如果没有就创建,有就不做操作
setex key times value //设置key的存活时间
二、Lists(列表)
redis列表也是使用字符串来接收值的,存进去list的值中也会被存储成字符串
列表最多可存储 232 - 1 元素 (4294967295, 每个列表可存储40多亿)。
lpush key value1 value2 //在左边往key中添加value1、value2
rpush key value //在右边往key中添加value
lpop key //lpop 从左边弹出一个元素
rpop key //从右边弹出一个元素
lrange key start end //遍历key [start,end] 0,-1是查看所有
三、Set集合
3.1、无序集合(sets)
redis中的set是String类型的无序集合,不允许重复,如果第二次添加相同的元素会添加失败
sadd key value //在key中添加一个元素
srem key value //删除key中的value元素
smembers key //查看所有的key
spop key value //删除key中的value
3.2、有序集合(sorted sets)
zset和set一样是String类型的集合,zset可以是有序集合
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
zset的成员是唯一的,但分数(score)却可以重复。
zadd key score value //往key中添加value分数为score
zrem key value //删除key中value
zrange key start end //升序遍历key [start,end]
zrevrange key start end //倒序遍历key [start,end]
zrank key value //升序排序,查看value的排名
zrevrank key value //倒序排序,查看value对应的排名
zincrby key num value //给key的value属性添加num
zcard key //查看key的个数
四、哈希(hashes)
hashes存的是字符串和字符串之间的映射,适合存储一个对象(比如:用户)
hset key filed value //给key添加字段值value
hget key filed //获取key中的filed
hdel key filed //删除key中的filed
hkeys key //查看key中所有的filed
hvals key //查看key中所有的value
hincrby key filed num //给key中的filed字段添加num
hexists key filed //判断key中是否包含filed
hgetall key //查看key中所有的filed和value
五、持久化
redis的持久化分为RDB和AOF两种模式;
**RDB:**就是在不同的时刻,把redis存储的数据写入到磁盘等存储介质上
**AOF:**就是在将redis执行过的所有的指令记录下来,在下次redis重新启动的时候,把记录的命令全部执行一遍就可以实现数据恢复了
1、RDB模式
RDB是一个快照式模式,是将redis某一时刻的数据存储到磁盘等存储介质中
**RDB持久化的过程:**redis会先将数据存储到一个临时文件中,当持久化过程结束时,才会把临时文件替换到上次持久化完成的文件中
对于RDB模式,redis会单独创建一个子进程来执行持久化,而主进程是不会进行任何的IO操作的,这样就确保了redis极高的性能。
缺点:如果你对数据的完整性比较敏感,那么RDB就不适合,因为即使你每3分钟持久化一次(写入磁盘),如果期间redis出现故障,或者电脑关机仍然会有3分钟的数据丢失。所以这个时候可以使用AOF模式
2、AOF模式
AOF是将执行过的命令记录下来,在恢复数据的时候从前到后的循序执行一遍命令;AOF持久化机制会记录所有的写操作,包括增、删、改等。每当执行完一个写命令(例如SET、DEL、INCRBY等),就会在AOF文件末尾追加相应-的命令。
注意:由于AOF机制会记录所有的写操作,所以随着时间长了会导致AOF文件过大,从而影响redis的性能和磁盘空间,为了避免这种情况,可以选择适当的AOF缓存策略
2.1、AOF的缓存策略:
1、always:表示每次写操作完成后都会相应的把对应的命令追加到AOF缓存区中,保证了数据的安全性和实时性,但会带来相对较高的写入延迟和额外的IO操作。
2、everysec(默认):表示每秒钟记录一次AOF缓存区发生的所有命令,然后将这些命令一起写入AOF文件。相比于always策略,everysec可以一定程度上降低写入延迟和IO负载,但一些极端的情况下仍然可能丢失最后一秒内未记录到AOF的部分写入。
3、no:表示完全不启用AOF缓存机制,所有的写操作直接写入AOF文件。这种策略可以最大限度地保证系统的写入性和稳定性,但同时也会导致更大的数据风险。
**注意:**如果需要使用其他缓存策略,可以通过修改配置文件中的appendfsync参数进行设置。
appendfsync always //切换always模式
2.2、AOF重写
1、redis会创建一个子进程,这个子进程会首先读取现有的AOF文件,并将其包含的指令进行分析压缩并写入一个临时文件中。
与此同时,主进程会将接收到的写的指令写入内存缓存区中,一边继续写入到原有的AOF文件中,这样做是包装原有的AOF文件的可用性,避免在重写过程中出现意外。
当子进程完成工作后,它会给主进程发送一个信号,主进程会将内存中的指令追加到新的AOF文件中。当追加结束后,redis会用新的AOF文件来代替旧的AOF文件,之后有新的指令就追加到新的AOF文件中
原理:AOF文件重写(rewrit e)机制,即当AOF文件的大小超过所设定的阈值时,redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。举个例子或许更形象,假如我们调用了100次INCR指令,在AOF文件中就要存储100条指令,但这明显是很低效的,完全可以把这100条指令合并成一条SET指令,这就是重写机制的原理。
**如何选择:**官方建议是两个同时使用
六、事物
1.MULTI用来组装一个事务;
2.EXEC用来执行一个事务;
3.DISCARD用来取消一个事务;
4.WATCH用来监视一些key,一旦这些key在事务执行之前被改变,则取消事务的执行。
127.0.0.1:6379> multi //启动事物
OK
127.0.0.1:6379> set age 23
QUEUED
//age不是集合,所以如下是一条明显错误的指令
127.0.0.1:6379> sadd age 15
QUEUED
127.0.0.1:6379> set age 29
QUEUED
127.0.0.1:6379> exec //执行事务时,redis不会理睬第2条指令执行错误
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) OK
127.0.0.1:6379> get age
"29" //可以看出第3条指令被成功执行了
使用watch实行监听key的变动,支持监听多个key,当被监听的key的值改动时,在执行exec执行事物的时候,就会返回nil,表示事物无法触发
127.0.0.1:6379> set age 23
OK
127.0.0.1:6379> watch age //开始监视age
OK
127.0.0.1:6379> set age 24 //在EXEC之前,age的值被修改了
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set age 25
QUEUED
127.0.0.1:6379> get age
QUEUED
127.0.0.1:6379> exec //触发EXEC
(nil) //事务无法被执行
事物
在开启事物 multi 的时候,后面的命令都会添加到一个队列中,不会开始执行;只有使用执行事物 exec 的命令的时候才会执行队列中的命令
watch用来监视一些key,支持同时监视多个key, 只要还没真正触发事务,**watch **都会尽职尽责的监视,一旦发现某个key被修改了,在执行 exec 时就会返回nil,表示事务无法触发。
注意: 执行的时候,如果中间有个别命令出现异常,则该命令会抛出异常,其他命令正常执行
什么是缓存击穿、缓存穿透、缓存雪崩?
缓存穿透 : 读请求访问时,缓存和数据库都没有某个值,这样就会导致每次对这个值的查询请求都会穿透到数据库,从而对数据库造成极大的压力。这就是缓存穿透。
怎么避免?
-
布隆过滤器:在缓存层和数据库之间加上布隆过滤器,用于判断请求的 key 是否存在于缓存或者数据库中。如果 key 不存在,则直接返回不存在,避免对数据库进行不必要的操作。
通过计算数据的hash值,通过hash值来计算存储的位置,把对应位置上的数据改为1
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
=================================================
@Configuration
public class BloomFilterConfig {
// 设置位数组大小为10000,哈希函数数量为3
private static final int SIZE = 10000;
private static final int HASH_NUM = 3;
@Bean
public BloomFilter<String> bloomFilter() {
return BloomFilter.create(Funnels.unencodedCharsFunnel(), SIZE, HASH_NUM);
}
}
=================================================
@Service
public class BloomFilterService {
@Autowired
private BloomFilter<String> bloomFilter;
//contains 方法用于判断 key 是否存在于布隆过滤器中
public boolean contains(String key) {
return bloomFilter.mightContain(key);
}
//add 方法用于将 key 添加到布隆过滤器中
public void add(String key) {
bloomFilter.put(key);
}
}
-
空值缓存:把数据库中为空的值也进行缓存,当查询一个不存在的 key 时,可以把这个 key 对应的值设置为空值并进行缓存,这样后续的查询就会直接从缓存中获取空值,避免了对数据库的访问。
-
数据预热:提前将常用的数据或者热点数据预先加载到缓存中,避免第一次访问没有缓存而需要从数据库中加载。
//步骤一
public class DataPreheatInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
//把一些热点key存储到redis中,秒杀的商品或者热点新闻
// 数据预热操作,例如从数据库中加载数据并缓存到 Redis 中
}
}
//步骤二
在项目的 resources 目录下创建一个名为 META-INF 的文件夹,并在其下创建一个名为 spring.factories 的文件。在 spring.factories 文件中添加以下内容:
org.springframework.context.ApplicationContextInitializer=com.example.demo.DataPreheatInitializer
缓存雪崩(大量不同的key):是指缓存中数据大批量到过期时间,而当前的查询数据量还是比较大,请求都直接访问到数据库上,导致数据库积压严重。导致数据库压力过大甚至down机。
怎么避免?
尽量设置key的过期时间不在同一时间点,尽量使得它们均匀分散开来。
**缓存击穿(大量相同的key):**在key过期的时候,而恰好在这个时候有大量的请求访问同一个key,而导致请求全部都访问到数据库上。
解决方案:
- 给热点数据设置过期时间:如果一个数据被热点访问,那么我们可以将它的缓存时间设置为较短的过期时间,比如 5 分钟,这样热点数据在 5 分钟内不会被过多的请求击穿。在缓存过期后,下一次查询会重新去数据库中获取数据并更新缓存。
- 使用互斥锁:当多个请求在同一时刻同时访问一个缓存数据时,可以使用互斥锁来控制只有一个请求能够更新缓存。比如在 Redis 中,可以使用 SETNX 命令来尝试获取一个锁,如果获取锁成功,则执行缓存加载操作;否则等待一段时间后再次尝试获取锁。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
========================================================
@Component
public class RedisLockUtil { //分布式锁工具类
private final RedisTemplate<String, String> redisTemplate;
//锁过期时间
private static final long LOCK_EXPIRE = 3000L;
@Autowired
public RedisLockUtil(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
// 获取锁,返回是否获取成功
public boolean lock(String key) {
// 尝试获取锁,如果获取成功,则给锁设置过期时间
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "1", Duration.ofMillis(LOCK_EXPIRE));
return Boolean.TRUE.equals(result);
}
//解锁
public void unlock(String key) {
redisTemplate.delete(key);
}
}
=====================================================
@Autowired
private RedisLockUtil redisLockUtil;
public void doSomething(String key) {
// 尝试获取锁
if (redisLockUtil.lock(key)) {
try {
// 加锁成功,执行业务逻辑
} finally {
// 释放锁
redisLockUtil.unlock(key);
}
} else {
// 加锁失败,抛出异常或重试获取锁
}
}
热点key怎么解决?
- 分布式缓存
将缓存数据存储到多个缓存服务器上,并通过负载均衡(统一分配请求)来分担请求压力,从而避免单个热点导致的性能问题。常见的分布式缓存方案包括 Redis、Memcached 等2
- 缓存预热
在系统启动前或低峰期,预先将常用数据加载到缓存中,从而避免在高峰期由于缓存冷启动导致的性能问题。缓存预热可以使用定时任务或者异步线程来实现。
- 数据分片
将数据按照一定规则(如 ID 取模)分散到多台缓存服务器上,从而避免单个热点导致的性能问题。需要注意的是,数据分片需要考虑数据一致性和负载均衡等问题。
- 二级缓存
在主缓存(如 Redis)的基础上,增加一个次级缓存(如本地 Ehcache),用于保存频繁访问的数据,从而避免主缓存出现热点。需要注意的是,次级缓存需要考虑数据一致性和更新等问题。
- 带过期时间的缓存
对于热 key,可以为其设置较短的过期时间(如几秒钟),从而避免单个热点长时间占用缓存资源。同时,为了避免突发请求导致的缓存击穿问题,可以使用带过期时间的缓存预热和懒加载等方式来保证数据的可用性。
Redis的过期策略
**定时过期:**每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即随机抽取key进行清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
**惰性过期:**只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
**定期过期: **指 Redis 会周期性地遍历数据库中的所有 Key,删除过期的 Key。因为 Redis 的单线程特性,遍历数据库时不会影响其他操作。默认情况下,Redis 每隔 100ms 就会随机选择一些过期的 Key 进行删除,可以通过 hz
配置项来修改遍历频率。
Redis 内存淘汰策略
volatile-lru:当内存不足以容纳新写入数据时,从设置了过期时间的key中使用LRU(最近最少使用)算法进行淘汰;
allkeys-lru:当内存不足以容纳新写入数据时,从所有key中使用LRU(最近最少使用)算法进行淘汰。
volatile-lfu:4.0版本新增,当内存不足以容纳新写入数据时,在过期的key中,使用LFU算法进行删除key。
allkeys-lfu:4.0版本新增,当内存不足以容纳新写入数据时,从所有key中使用LFU算法进行淘汰;
volatile-random:当内存不足以容纳新写入数据时,从设置了过期时间的key中,随机淘汰数据;。
allkeys-random:当内存不足以容纳新写入数据时,从所有key中随机淘汰数据。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的key中,根据过期时间进行淘汰,越早过期的优先被淘汰;
noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。
Redis的内存淘汰算法
- LRU(最近使用是什么时候)(Least Recently Used):最近最少使用算法。其原理是,淘汰最近最少使用的键,即如果一个键在最近一段时间内没有被访问过,那么就认为这个键不太可能在后续被访问到,因此可以被淘汰掉。
- LFU(使用次数)(Least Frequently Used):最不经常使用算法。其原理是,淘汰访问频率最低的键,即如果一个键被访问的次数很少,那么就认为这个键不太重要,可以被淘汰掉。
- Random(随机算法):随机选择一个键进行淘汰。
- TTL(Time To Live):根据键的剩余时间来决定淘汰优先级,即距离过期时间更近的键会被更早淘汰。
怎么实现Redis的高可用?
我们在项目中使用Redis,肯定不会是单点部署Redis服务的。因为,单点部署一旦宕机,就不可用了。为了实现高可用,通常的做法是,将数据库复制多个副本以部署在不同的服务器上,其中一台挂了也可以继续提供服务。Redis 实现高可用有三种部署模式:主从模式,哨兵模式,集群模式。
主从模式
主从模式中,Redis部署了多台机器,有主节点,负责读写操作,有从节点,只负责读操作。从节点的数据来自主节点,实现原理就是主从复制机制
哨兵模式
哨兵模式是主要监控所有节点的状态的,在主节点出现故障或down机后,会进行故障转移,会从从节点中选举出来一个健康的从节点作为主节点,并通知其他的从节点切换到新的主节点上
集群模式
集群模式是有多组由一个主节点和多个从节点组合成的小组形成,每个小组都存储部分的数据,查询的数据通过计算数据的存储位置来找到指定的小组。
官方的说法:Redis集群模式使用分片来将数据分散到不同的节点上,在这些节点之间自动进行数据复制,以提供高可用性。每个节点都是相互独立的Redis实例,并带有自己的数据副本和故障转移实现。当某个节点出现故障时,集群会自动重新分配节点负载,并选择新的主节点以确保数据可用性
Redis集群模式具有以下特点:
- 高可用性:当某个节点宕机或者出现问题时,其他节点可以自动接管它的工作。
- 自动分片:每个节点只负责部分数据的存储和管理,在整个集群中完成了对所有键值对的均衡划分和负载均衡。
- 可扩展性:通过增加新的节点来扩展系统容量,并支持在线添加、删除、迁移、平衡等操作。
- 无单点故障:不像传统主从架构中存在单点故障风险。