Redis入门到入土-章节一

205 阅读21分钟

Redis概述

redis是一个高性能的内存键值对数据库,可用于数据库缓存,计数器,消息代理,分布式锁。支持String,List,Hash,Set,SortedSet五种数据类型。

与传统数据库采用磁盘存储数据相比,Redis是一种内存优化型数据库,读写速度快

五种数据类型

String

最简单的存储结构,根据value的不同类型,可分为以下几种情况:

  1. 简单字符串(Simple String):当value是一个普通字符串时,可将其视为简单字符串,这种类型的值没有任何特殊的数据结构或语义

image.png

  1. 整数(Integer):当value可以被解析为整数时,Redis会将其存储为整数类型。这样可以在执行一些特定的命令时提供更高效的操作。如自增(INCR)和自减(DECR)

image.png

  1. 浮点数(Floating Point):当value可以被解析为浮点数时,Redis会将其存储为浮点数类型。浮点数支持一些特定的操作,例如增加(INCRBYFLOAT)和减少(DECRBYFLOAT)。

image.png

这个没有用过

4. 位图(Bitmap):当value被用作位图时,可以将其视为一个二进制字符串。Redis提供了一系列的位操作命令,可以对位图进行处理,例如设置(SETBIT)、获取(GETBIT)和统计位的数量(BITCOUNT)。

String类型在Redis中的灵活性和存储容量上的限制确实能够满足大多数常见需求,并且使用简单的字节数组来表示数据,提高了读写效率。

然而,当需要存储复杂结构的数据时,String类型的处理能力相对较弱。此时,可以考虑借助其他工具或结合Redis的其他数据结构,如哈希(Hash)、列表(List)、集合(Set)或有序集合(Sorted Set)来处理复杂的数据操作。

总体而言,Redis的String类型是非常实用和常用的数据类型之一,对于大部分简单数据的存储和读取来说,String类型都会是一个很好的选择。对于更复杂的数据结构和处理需求,可以结合其他Redis数据结构来实现更灵活的功能。

String中的命令

SET key value:设置指定key的值为value。
GET key:获取指定key的值。

INCR key:将指定key的值加1,并返回增加后的值。
DECR key:将指定key的值减1,并返回减少后的值。
INCRBY key increment:将指定key的值增加指定的增量值(整数)。
DECRBY key decrement:将指定key的值减少指定的减量值(整数)。

    INCR和DECR只能操作value为整数类型key,如果对其他类型的value操作会出现 
    ERR value is not an integer or out of range

APPEND key value:将指定value追加到指定key的值末尾。
    可以理解为StringBuilder的append()方法
    
STRLEN key:获取指定key的值的长度。

MSET key1 value1 key2 value2 …:同时设置多个key-value对。

MGET key1 key2 …:同时获取多个key的值。

SETEX key seconds value:设置指定key的值,并指定过期时间(以秒为单位)。

SETNX key value:如果指定key不存在,则设置它的值为value。

GETSET key value:设置指定key的值,并返回原来的值。

Hash

散列,value为无序字典,相当于Map<Object,Map<Object,Object>>,适合存储对象和结构化数据。常见的使用场景包括:

  • 缓存对象:将对象以哈希结构存储在Redis中,可快速读取,修改和删除对象的某个字段
  • 存储用户信息:可以将用户的ID作为key,将用户信息的各个字段存储为哈希结构的field和value。
  • 计数器:使用哈希结构的field来存储计数器的名称,value存储实际的计数值,方便对计数器进行增量操作。
  • 应用配置:可以将应用的配置信息以哈希结构的形式存储,方便读取和更新配置。

通过合理使用哈希结构,可以有效地管理和操作复杂的数据,并且提供了快速的读写能力。需要注意的是,当哈希结构中的字段数量较多时,可能会影响性能和内存消耗

存储的数据大概长这样

image.png

常见命令

HSET key field value:设置指定key下的指定field的值为value。
HGET key field:获取指定key下指定field的值。

HMSET key field1 value1 field2 value2 …:同时设置多个field-value对。
HMGET key field1 field2 …:同时获取多个field的值。

HGETALL key:获取指定key下所有的field-value对。
HDEL key field1 field2 …:删除指定key下的一个或多个field。
HEXISTS key field:检查指定key下的指定field是否存在。
HLEN key:获取指定key下field的数量。
HINCRBY key field increment:将指定key下的指定field的值增加指定的增量值(整数)。
HKEYS key:获取指定key下所有的field。
HVALS key:获取指定key下所有的value。

特点

  1. 存储多个字段和对应的值:哈希结构可以存储多个字段和对应的值,从而可以将多个相关属性组合在一起存储,类似于关联数组或字典。
  2. 快速查询单个字段值:通过指定哈希的key和field,可以在常量时间复杂度内快速获取到对应的值,不受哈希中字段数量的影响。
  3. 高效的存储和读取:哈希结构使用简单的字节数组表示数据,读写操作都是在内部的字节数组上进行,因此存储和读取的效率都非常高。
  4. 字段的唯一性:哈希结构中的字段名是唯一的,在同一个哈希中不允许存在重复的字段。
  5. 支持原子操作:Redis提供了一些针对哈希结构的原子操作,例如设置字段值、删除字段、增加字段值等,这些操作都是原子的,可以确保数据的一致性。
  6. 灵活性:哈希结构的字段和值可以是任意类型的数据,可用于存储复杂的数据结构,并且可以通过指定的字段进行精确查找和操作。
  7. 可以节省内存:如果哈希结构中只存储了部分字段,那么它相对于使用多个单独的键值对来存储相同的数据,可以节省一定的内存空间。

List

Redis中List(列表)类型是一种有序、可重复的数据结构。List中的每个元素都有一个索引,可以根据索引对元素进行访问、查找和操作

在存储数据方面,结构相当于Java中LinkedList结构,通过操作两端实现快速插入和删除

根据以上描述可得出List结构的特点

  • 有序性:List中的元素按照插入顺序进行排序,每个元素都有一个索引值,可以根据索引来访问或操作元素。
  • 可重复性:List允许存储重复的元素,同一个值可以被插入多次
  • 快速的访问和操作:List支持在两端(头部和尾部)进行元素的插入、删除和查找操作。因为List的底层实现是基于链表,所以在头部或尾部插入和删除元素的时间复杂度都是O(1)。
  • 支持负索引:List可以使用负数作为索引,负数索引表示从列表末尾开始的位置。例如,-1表示列表中的最后一个元素
  • 灵活的数据存储:List可以存储各种类型的元素,包括字符串、整数和其他复杂的数据结构。
  • 支持批量操作:Redis提供了一些对List进行批量操作的命令,例如从列表的头部或尾部插入多个元素、删除指定范围的元素等。
  • 可以实现队列或栈的功能:通过List的插入和删除操作,可以实现队列(FIFO)或栈(LIFO)等功能。

根据其中的特点,大概可知道一些适用场景:

  • 根据可从头部或尾部插入数据和读取数据的特性,可用List实现消息队列,完成简单的消息发布与消费
  • 根据有序性特点,可实现排行榜功能,根据用户积分...排行,方便获取指定范围内的数据
  • 历史记录:可以使用List记录某个实体的历史操作、状态变更等,可以按时间顺序查看或回放历史记录。

常用命令

LPUSH key value1 [value2 …]:将一个或多个值插入到列表的头部。
RPUSH key value1 [value2 …]:将一个或多个值插入到列表的尾部。
LPOP key:移除并返回列表的头部元素。
RPOP key:移除并返回列表的尾部元素。
LINDEX key index:获取列表指定索引位置上的元素。
LLEN key:获取列表的长度(元素个数)。
LRANGE key start stop:获取列表指定范围的元素,start和stop为索引值。
LINSERT key BEFORE|AFTER pivot value:在列表中指定元素pivot的前或后插入一个新元素。
LSET key index value:设置列表指定索引位置上的元素的值。
LREM key count value:移除列表中指定值的元素,count表示要移除的个数。
BLPOP key1 [key2 …] timeout:从多个列表中按顺序弹出第一个非空列表的头部元素,或阻塞等待一定时间。
BRPOP key1 [key2 …] timeout:从多个列表中按顺序弹出第一个非空列表的尾部元素,或阻塞等待一定时间。

image.png

Set

Redis中Set(集合)数据结构是无需不重复的,与HashSet类似,插入元素时,都会调用hash算法得到角标,数据存储顺序无序,元素不可重复,查找快,支持交,并,差集功能,可实现共同好友等功能

常用命令

  1. SADD key member1 [member2 …]:向指定的Set中添加一个或多个元素。

插入数据,如果已经存在的数据,返回值为0

image.png

SREM key member1 [member2 …]:从指定的Set中移除一个或多个元素。

image.png

SCARD key:获取指定Set的元素数量(集合的基数)。

image.png

SISMEMBER key member:判断指定元素是否存在于Set中。

image.png

SMEMBERS key:获取指定Set中的所有元素。

image.png

SRANDMEMBER key [count]:随机获取Set中指定数量的元素(可重复)。

image.png

SPOP key:随机移除并返回Set中的一个元素。

image.png

SDIFF key1 [key2 …]:计算多个Set之间的差集,返回差集的结果。

image.png

SINTER key1 [key2 …]:计算多个Set之间的交集,返回交集的结果。

image.png

SUNION key1 [key2 …]:计算多个Set之间的并集,返回并集的结果。

image.png

SSCAN key cursor [MATCH pattern] [COUNT count]:迭代遍历Set中的元素。

贴不了图了

SMOVE source destination member:将指定元素从source Set移动到destination Set。

贴不了图了

SortedSet

可排序集合,每个元素带有一个score属性,可基于score属性对元素排序,底层是一个跳表(SkipList)加hash表,其元素可排序,不重复

特点

  • 元素的唯一性:每个元素在Sorted Set中是唯一的,重复的元素会被自动去重。
  • 元素的顺序性:每个元素都与一个分数相关联,通过分数可以对元素进行排序。
  • 高效的插入和删除:Sorted Set使用跳跃表实现,能够在平均O(log N)的时间复杂度内进行插入和删除操作。
  • 分数范围查询:可以根据分数的范围进行查询,快速地找到符合条件的元素。

常见命令

  • ZADD:向Sorted Set中添加一个或多个元素。
  • ZRANGE:按照元素在Sorted Set中的索引范围获取元素。
  • ZRANK:获取元素在Sorted Set中的排名(从0开始计数)。
  • ZSCORE:获取元素的分数。
  • ZREM:从Sorted Set中移除一个或多个元素。

Java中使用Redis

使用Jedis

方式一:

引入Jedis

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.3.2</version>
</dependency>

测试类

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;

public class JedisTest {
    private Jedis jedis;

    @BeforeEach
    void setUp() {
        // 方式1
        jedis = new Jedis("127.0.0.1",6379);
        jedis.select(0);
    }

    @Test
    void test1(){
        String status = jedis.set("jedisKey", "value1");
        System.out.println(status);
        String jedisKey = jedis.get("jedisKey");
        System.out.println(jedisKey);
    }

    @AfterEach
    void release(){
        if(jedis!=null){
            jedis.close();
        }
    }
}

屏幕截图 2023-07-19 140232.png

方式二:使用JedisPool

为了减少每次使用时的性能开销,先创建连接池

public class JedisPoolUtil {
    private static JedisPool jedisPool;

    static {
        JedisPoolConfig config = new JedisPoolConfig();
        // 连接池总数量
        config.setMaxTotal(9);
        // 连接池最大等待连接数
        config.setMaxIdle(9);
        // 连接池最小等待连接数
        config.setMinIdle(0);
        // 等待超时时间
        config.setMaxWaitMillis(1000);
        jedisPool = new JedisPool(config,"127.0.0.1",6379,1000);
    }

    public static Jedis getJedis(){
        return jedisPool.getResource();
    }
}

测试

...

@BeforeEach
    void setUp() {
        // 方式2
        jedis = JedisPoolUtil.getJedis();
        jedis.select(0);
    }
    
...

当然,到现在为止,大多数项目都使用的SpringBoot进行开发,所以上面使用Jedis只能算是自己练练手看看还有什么方式创建而已,现在开始讲在SrpingBoot中使用Redis

SpringBoot中使用Redis

在SpringBoot中,通常使用RedisTemplate进行redis的操作

RedisTmplate包括几种序列化方式,默认采用JDK序列化方式(JdkSerializationRedisSerializer),默认方式下存储的key-value都进行了序列化,造成可读性差,内存占用较大

在有需要的情况下,可通过自定义RedisTemplate序列化替换默认方式

@Configuration
void RedisConfig{
    @Bean
    void RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory connectionFactory){
        RedisTemplate<String,Object> redistemplate = new RedisTemplate();
        redistemplate.setConnection(connectionFactory);
        // 创建序列化工具
        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
        // 为key与value设置序列化类型
        redisTemplate.setKeySerializer(serializer);
        redisTemplate.setHashKeySerializer(serializer);
        redisTemplate.setValueSerializer(serializer);
        redisTemplate.setHashValueSerializer(serializer);
        return redisTemplate;
    }
}

什么?你问为什么要使用GenericJackson2JsonRedisSerializer这种序列化方式?

自己用了后感觉就是,存储对象不再乱码,且,且redis保存的数据中还存在当前被序列化的类字节码对象,这样的好处是在反序列化中能快速找到当前值的对象,缺点是占用了额外的空间

所以为了节省空间,存储java对象时手动将对象序列化和反序列化,其他类型时采用String序列化器

存储对象手动序列化时,可使用springmvc默认使用json处理工具(ObjectMapper)

SpringBoot中常量的两种序列化实战方案 方式一

1.1. 自定义RedisTemplate
1.2. 修改RedisTemplate的序列化器为GenericJackson2JsonRedisSerializer

方式二

2.1. 使用StringRedisTemplate 
2.2. 写入Redis时,手动将对象序列化为JSON 
2.3. 读取Redis时,手动将JSON反序列化为对象

四种序列化方式

JdkSerializationRedisSerializer:

√:支持任意Java对象的序列化和反序列化,无需特殊配置。

×:序列化后的数据相对较大,使用旧版本的Java序列化格式,不适合跨语言使用。

Jackson2JsonRedisSerializer:

√:序列化为JSON格式,对人类可读,适合调试和检查。支持多态类型的序列化。

×:只能序列化能够转换为JSON的对象,对于复杂对象需要额外的配置。序列化和反序列化的性能比较一般。

GenericJackson2JsonRedisSerializer:

√:具备Jackson2JsonRedisSerializer的优点,同时支持多态类型的序列化。

×:与Jackson2JsonRedisSerializer相同,序列化和反序列化的性能一般。

StringRedisSerializer:

√:对于键和字符串类型的值,直接使用UTF-8编码进行序列化和反序列化,存储和读取效率高。

×:对于非字符串类型的值,需要自己在应用层进行序列化和反序列化转换。不适合存储复杂对象。

所以:如果需要存储任意类型的Java对象,可以使用JdkSerializationRedisSerializer。如果对可读性和跨语言支持有要求,可以选择Jackson2JsonRedisSerializer或GenericJackson2JsonRedisSerializer。如果主要操作字符串类型的键值对,可以使用StringRedisSerializer来提高性能。

缓存

什么是缓存

通俗的话讲,就是加快访问速度咯

半官方的讲,数据交换缓冲区,存储数据的临时区域,读写性能高,降低后端负载,提高读写效率,降低响应时间

在实际应用中,如果数据一直向缓存中插入,最终也会导致访问数据变慢,所以何时?怎样更新缓存?

缓存更新策略

  • 内存淘汰:不用手动维护,当Redis内存不足时自动淘汰部分数据
  • 超时淘汰:添加数据时设置数据超时时间(TTL),到期后自动删除
  • 主动更新:在业务逻辑中,修改数据库的同时更新缓存

怎样更新缓存大概知道这三点

那么何时更新呢?

使用场景

1 低一致性需求,使用内存淘汰机制

2 高一致性,使用主动更新,并使用超时删除作为兜底方案

缓存穿透

八股文大家都背的很熟了,穿透指缓存中不存在需要查询的数据,导致每次查询都到必须到底层数据源中查询,从而绕过缓存。这可能是由于恶意攻击、频繁查询不存在的数据或缓存数据失效等原因引起的。

解决方案

方案一:缓存空值

当查询的数据不存在时,可以将空结果(如null)缓存起来,同时设置一个较短的过期时间。在后续查询中,可以命中缓存并返回空结果,避免频繁查询底层数据源

例:

public Object cachePenetration(Long id) {
    String redisKey = "";
    // 1. 查询缓存中是否存在数据
    String redisJson = stringRedisTemplate(redisKey + id);
    // 2. 存在数据直接返回
    if(StringUtil.isNotBlank(redisJson)) {
        return JSONUtil.toBean(redisJson, Object.class);
    }
    // 2.1. 存在数据但是为换行符等假数据,返回null
    if(redisJson != null) {
        return null;
    }
    // 3. 不存在数据,查询数据库
    Object result = this.getById(id);
    // 4. 数据库中不存在,设置null值并缓存到redis中
    if(result == null) {
        stringRedisTemplate.opsForValue().set(redisKey + id, JSONUtil.toJsonStr(result), 100, TimeUnit.SECONDS)
        return null;
    }
    // 5. 存在将数据缓存到redis中
    stringRedisTemplate.opsForValue().set(redisKey + id, "", 100, TimeUnit.SECONDS);
    // 6. 返回对象
    return null;
}

方案二:热点数据预加载

为了避免热点数据(访问量高的数据)在使用过程中出现缓存穿透问题,可以将数据预先加载到缓存中,以减少缓存穿透的影响。可以通过定时任务或在系统启动时加载这些数据

    哈哈哈,这里只有理论了,代码自己试试咯

方案三:异步加载数据

当发现查询的数据不存在缓存中时,可以通过异步方式从底层数据源加载数据并更新缓存,避免阻塞查询线程,提高系统吞吐量。

@Service
public class MyService {
    private final RedisTemplate<String, String> redisTemplate;

    public MyService(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public String getData(String key) {
        String data = redisTemplate.opsForValue().get(key);
        if (data == null) {
            loadDataAsync(key);
        }
        return data;
    }

    @Async // 标记为异步方法
    public CompletableFuture<Void> loadDataAsync(String key) {
        // 模拟从底层数据源加载数据的逻辑
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        String data = loadDataFromDataSource(key);

        // 将加载的数据存入Redis
        redisTemplate.opsForValue().set(key, data);

        return CompletableFuture.completedFuture(null);
    }

    private String loadDataFromDataSource(String key) {
        // 从底层数据源加载数据的逻辑
        String data = "Data for " + key;
        return data;
    }
}

方案四:限制查询频率

对频繁查询不存在的数据请求,可以在应用层面设置一些限制,降低被恶意攻击风险

通过限制请求ip
通过限制接口请求次数

方案五:布隆过滤器

大家都知道布隆过滤器,但是像我这种ds,只是听过,没有实际上手实际使用过,只有些理论知识,下面时一个简单的例子

import java.util.BitSet;
import java.util.HashSet;
import java.util.Set;

public class BloomFilter {
    private final int numBits;
    private final int numHashFunctions;
    // 用于存储位数组
    private final BitSet bitSet;
    // 存储具体缓存数据
    private final Set<String> cacheData;

    public BloomFilter(int numBits, int numHashFunctions) {
        this.numBits = numBits;
        this.numHashFunctions = numHashFunctions;
        this.bitSet = new BitSet(numBits);
        this.cacheData = new HashSet<>();
    }

    public boolean mightContain(String key) {
        // 先通过布隆过滤器快速判断是否存在于集合中
        if (!bitSet.get(hash(key))) {
            return false;
        }
        // 再通过缓存判断是否存在
        return cacheData.contains(key);
    }

    public void put(String key) {
        // 将元素加入布隆过滤器
        int[] hashes = getHashes(key);
        for (int hash : hashes) {
            bitSet.set(hash);
        }
        // 将元素加入缓存
        cacheData.add(key);
    }

    private int[] getHashes(String key) {
        int[] hashes = new int[numHashFunctions];
        // 使用多个哈希函数生成多个哈希值
        for (int i = 0; i < numHashFunctions; i++) {
            hashes[i] = hash(key + i);
        }
        return hashes;
    }

    private int hash(String data) {
        // 自定义哈希函数,可以使用不同的哈希算法
        return data.hashCode() % numBits;
    }
}

需要查询元素时,先调用mightContain方法判断是否需要进一步查询缓存或底层数据源,避免频繁查询不存在的数据

缓存雪崩

缓存雪崩指在同一时间段,redis中大量key失效或者redis服务宕机,导致请求大量直接访问后端数据源,从而使后端服务器负载剧增,甚至导致系统崩溃的现象。

这是概念相关的东西,那么在实际使用中何时会发生缓存雪崩呢?

发生情况

1. 缓存过期或清除时间集中:缓存中的数据往往会设置一个过期时间,当很多缓存在同一时间过期或被清除时,大量的请求会直接访问后端,造成压力过大。

2. 大规模数据更新:当系统中的大量数据同时更新时,原有的缓存将会失效,导致请求全部落到后端。

解决方法

  • 设置合理的缓存过期时间:将缓存的过期时间分散开,避免大量缓存同时失效
  • 引入热点数据永远不过期的策略:对于一些热点数据,可以设置其永不过期,确保关键数据的可用性。
  • 限流与降级:可以通过限制请求的并发量,或者降级部分功能,以减少后端的压力。
  • 备份缓存:可以采用多级缓存结构,多备份一份缓存,当一个缓存失效时,可以使用备份缓存,减轻后端请求的压力。
  • 监控与预警:通过监控缓存的状态,及时发现异常情况并进行预警,以便尽早采取措施应对。

缓存击穿

缓存击穿是指在缓存系统中,一个缓存键(key)对应的数据在缓存中不存在,但是对该数据的请求非常频繁,导致大量请求直接访问后端数据源,从而增加了后端的负载压力。

出现情况

热点数据失效:某个热点数据的缓存过期或被清除,而此时有大量的请求访问该数据,导致缓存无法命中,请求直接访问后端。

高并发请求:当有大量并发请求同时访问一个缓存键对应的数据,而该数据没有缓存,也没有采取限流措施的情况下,会导致后端压力剧增

解决方案

  1. 加载缓存时加锁:在加载数据到缓存的过程中,使用互斥锁来保证只有一个线程加载数据,其他线程等待,从而避免重复加载的问题。

    从redis中查询数据,判断是否命中,①命中就返回数据;②未命中尝试获取互斥锁,获取互斥锁成功,查询数据库,将获取的数据写入redis,释放互斥锁,返回数据,③获取互斥锁失败,休眠一段时间,再次从redis中查询数据

public Object cacheBreakdownWithLock(Long id) {
    // 1. 从redis中获取数据
    String redisJson = stringRedisTemplate.opsForValue().get(redisKey + id);
    // 2. 判断是否命中,命中返回数据
    if(StringUtil.isNotBlank(redisJson)) {
        return JSONUtil.toBean(redisJson,Object.class);
    }
    // 3. 判断是否为空
    if(redisJson != null) {
        return null;
    }
    // 4. 尝试获取锁
    boolean status = tryGetLock("lockKey");
    try{
        // 5. 获取锁失败,等待后重试
        if(!status){
            Thread.sleep(50);
            return cacheBreakdownWithLock(id);
        }
        // 6. 获取锁成功,查询数据库,需要大量时间处理的业务
        Object dataFromDb = this.getById(id);
        // 7. 数据库为空缓存空值
        if(dataFromDb == null) {
            stringRedisTemplate.opsForValue().set(redisKey + id,"",10000,TimeUnit.SECONDS);
            return null;
        }
        // 8. 数据库不为空缓存数据
        stringRedisTemplate.opsForValue().set(redisKey + id,JSONUtil.toJsonStr(dataFromDb),10000,TimeUnit.SECONDS);
        return dataFromDb;
    }catch(InterruptedException e) {
        throw new RunTimeException(e);
    } finally {
        releaseLock("lockKey");
    }
}
public boolean tryGetLock(String key) {
    Boolean status = stringRedisTemplate.opsForValue().setIfAbsent(key, "",1,TimeUnit.SECONDS);
    return BooleanUitl.isTrue(status);
}
public void releaseLock(String key) {
    stringRedisTemplate.delete(key);
}
  1. 设置热点数据永远不过期:对于一些热点数据,可以设置其永不过期,保证其始终缓存在缓存中,避免缓存失效带来的问题。
  2. 引入空值缓存:如果在后端数据源中查询得知某个缓存键对应的数据不存在,可以将该空结果缓存起来,设置一个较短的过期时间,避免频繁查询后端。
  3. 限流与降级:对于频繁请求的情况,可以通过限制请求的并发量或者降级部分功能,以减少后端的压力。
  4. 使用分布式锁:在缓存失效时,可以通过分布式锁来保证只有一个请求可以访问后端数据源,并在该请求加载数据到缓存后释放锁。
  5. 预加载缓存:可以在缓存过期前主动加载缓存数据,提前预加载,避免在热点数据失效时还未加载数据的情况。

第一节先到这里,第二节可能会讲到实际使用场景。

下一章:秒杀业务

谢谢大家看到了这里