Redis总结

目录

Redis篇

一.Spring缓存

1.概念

2.springboot中结合redis使用

二.redis

1.概念:

2.数据类型

3.常用命令

4.在springboot中整合redis

5.对字符串的序列化操作

6.用户登录的token保存在redis中,设置一个30分钟的过期时间示例

7.热搜的简单示例

8.RedisUtils简单工具类

9.Resis的持久化

10.缓存雪崩

11.缓存穿透

12.缓存击穿

13.缓存穿透和缓存击穿区别

三.Rdssion

1.概念:

2.redisson在springboot的使用添加分布式锁示例


Redis篇

一.Spring缓存

1.概念

在Spring中,可以通过使用缓存来提高应用程序的性能和效率。Spring框架提供了对缓存的集成和支持,可以方便地使用缓存来存储和获取数据。以下是Spring中关于缓存的一些重要概念和组件:

  1. 缓存管理器(Cache Manager):缓存管理器负责管理缓存的存储和获取,可以根据不同的需求选择不同的缓存管理器,比如Ehcache、Redis等。Spring提供了CacheManager接口和一些实现类,可以通过配置方式指定使用的缓存管理器。

  2. 缓存注解(Cache Annotation):Spring提供了一些注解,比如@Cacheable@CachePut@CacheEvict等,用于在方法上添加缓存相关的操作。使用这些注解可以方便地开启和使用缓存。

  3. 缓存配置(Cache Configuration):在Spring应用程序中配置缓存可以通过在配置文件(比如application.propertiesapplication.yml)中定义相关的属性,比如缓存类型、缓存名称、缓存的有效期等。Spring Boot提供了自动配置的方式,可以轻松地配置缓存。

  4. 缓存实例(Cache Instance):缓存实例代表了一个具体的缓存对象,可以通过使用缓存管理器来获取。使用缓存实例可以进行缓存的存储和获取操作。

使用缓存的步骤如下:

  1. 配置缓存管理器和缓存相关的属性。

  2. 在需要使用缓存的方法上添加缓存注解。

  3. 根据配置的缓存管理器和缓存注解,Spring会自动管理缓存的存储和获取操作。

2.springboot中结合redis使用

在Spring Boot中结合Redis使用缓存注解的示例,包括@Cacheable@CachePut@CacheEvict等注解的使用方式:

  1. 首先,在pom.xml文件中添加依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>
  1. application.properties文件中配置Redis相关属性:

spring.cache.type=redis
spring.redis.host=localhost
spring.redis.port=6379
  1. 创建一个配置类,配置Redis作为缓存管理器:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
​
@Configuration
public class RedisConfig {
    
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
        
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(config)
                .build();
    }
}

在上面的配置类中,我们配置了Redis的序列化方式,使用了StringRedisSerializer对缓存的key进行序列化,使用了GenericJackson2JsonRedisSerializer对缓存的value进行序列化。

  1. 创建一个业务类,使用缓存注解来添加缓存功能。比如:

import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;
​
@Service
public class UserService {
​
    @Cacheable(value = "users", key = "#id")
    public User getUser(String id) {
        // 模拟数据库查询
        // ...
        return user;
    }
​
    @CachePut(value = "users", key = "#user.id")
    public User saveUser(User user) {
        // 保存用户到数据库
        // ...
        return user;
    }
​
    @CacheEvict(value = "users", key = "#id")
    public void deleteUser(String id) {
        // 从数据库删除用户
        // ...
    }
}

在上面的示例中,我们使用了@Cacheable注解来表示该方法的返回值会被缓存,缓存的名称为"users",缓存的key为传入的参数id@CachePut注解表示该方法的返回值会更新或新增缓存,@CacheEvict注解表示该方法会删除指定的缓存。

  1. 创建一个Controller类,调用业务方法:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
​
@RestController
public class UserController {
​
    @Autowired
    private UserService userService;
​
    @GetMapping("/users/{id}")
    public User getUser(@PathVariable String id) {
        return userService.getUser(id);
    }
​
    @PostMapping("/users")
    public User saveUser(@RequestBody User user) {
        return userService.saveUser(user);
    }
​
    @PostMapping("/users/delete/{id}")
    public void deleteUser(@PathVariable String id) {
        userService.deleteUser(id);
    }
}

在上面的示例中,getUser()方法会从缓存中获取用户信息,如果缓存中不存在则会执行方法逻辑;saveUser()方法会保存用户信息到数据库,并更新/新增缓存;deleteUser()方法会从数据库删除用户,并删除缓存。

需要注意的是,为了使用缓存,需要在应用启动类上添加@EnableCaching注解,以启用Spring Boot的缓存功能。

二.redis

关于redis的下载安装和相关配置,请自行搜索

1.概念:

Redis是一种开源的高性能内存数据库,常用于缓存、消息队列、存储数据等场景。下面是关于Redis的一些概念:

  1. 键值存储:Redis以键值对的形式存储数据,每个键(key)都有一个唯一的值(value)与之对应。键可以是任何字符串,而值可以是字符串、列表、哈希表、集合、有序集合等多种数据类型。

  2. 内存数据库:Redis将数据存储在内存中,提供了快速的读写操作。与传统的关系型数据库相比,Redis因为不受磁盘的IO限制,具有极高的读写性能。

  3. 持久化:Redis提供了两种持久化的方式,即RDB(Redis Database)和AOF(Append Only File)。RDB是将数据库的快照以二进制形式保存到磁盘上,而AOF则是将每次写操作追加到文件的末尾。通过持久化,可以在Redis重启后恢复数据。

  4. 缓存:Redis常用于作为缓存层,将常用的数据存储在内存中,以提高读取性能。通过设置过期时间,可以自动删除过期的缓存数据。

  5. 发布订阅:Redis支持发布订阅模式,可以将消息发布到指定的频道,其他客户端可以订阅这些频道并接收到相应的消息。这在实现消息队列、实时聊天等场景下非常有用。

  6. 事务:Redis支持事务操作,可以对多个命令进行原子性的执行,保证操作的一致性。通过MULTI、EXEC、DISCARD和WATCH等命令,可以实现简单的事务控制。

  7. 集群:Redis提供了集群模式,可以将数据分布在多个节点上,实现数据的分布式存储和负载均衡。

Redis具有简单、快速、灵活等特点,广泛应用于高并发的Web应用、大规模数据处理等场景中。它提供了丰富的功能和灵活的数据结构,同时也具备高可用性和可扩展性。

2.数据类型

Redis提供了以下五种常用的数据类型:

  1. 字符串(String):字符串是最基本的数据类型,可以存储字符串、整数或浮点数。它支持对字符串的各种操作,如获取、设置、追加等。

  2. 列表(List):列表是一个有序的字符串集合,可以在列表的两端执行插入或删除操作。它可以用作栈(先进后出)或队列(先进先出)的数据结构。

  3. 哈希表(Hash):哈希表用于存储字段和相应的值之间的映射关系。每个哈希表可以包含多个字段,每个字段都有一个与之关联的值。

  4. 集合(Set):集合是一个无序的、唯一的字符串集合。它可以进行交集、并集、差集等各种集合操作,并且支持判断某个元素是否存在于集合中。

  5. 有序集合(Sorted Set):有序集合是一种有序的、唯一的字符串集合。每个成员都关联一个分数,可以根据分数对成员进行排序。有序集合常用于存储排行榜、计票系统等场景。

除了这些常用的数据类型,Redis还提供了一些其他的数据结构,如位图(Bitmap)、HyperLogLog、地理位置等,用于特定的应用场景。可以根据实际需求选择合适的数据类型来存储和处理数据。

3.常用命令

在Redis中常用的命令包括:

  1. 插入和修改数据

    • set key value:设置一个key的值为value。 示例:set name "John"

    • setex key seconds value:设置一个带有过期时间的key-value。 示例:setex token 3600 "abcd1234"

    • setnx key value:当key不存在时,设置一个key的值为value。 示例:setnx id 1001

    • append key value:将value追加到key的值之后。 示例:append message "Hello World!"

  2. 查询数据

    • get key获取key的值。 示例:get name

    • strlen key:获取key的值的长度。 示例:strlen name

    • exists key:检查key是否存在。 示例:exists name

  3. 删除数据:

    • del key(s)删除一个或多个key。 示例:del name

    • expire key seconds:设置key的过期时间。 示例:expire token 1800

    • ttl key获取key的剩余过期时间。 示例:ttl token

4.在springboot中整合redis

  1. 添加Redis依赖:在pom.xml文件中添加Redis相关依赖,如下所示:

<dependencies>
    <!-- 其他依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>
  1. 配置Redis连接信息:在application.yml文件中配置Redis的连接信息,示例如下:

spring:
  redis:
    host: 127.0.0.1
    port: 6379
  1. 创建Redis配置类:创建一个Redis的配置类,用于配置RedisTemplate等相关Bean,示例如下

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
​
@Configuration
public class RedisConfig {
  @Bean
  public RedisConnectionFactory redisConnectionFactory() {
    LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory();
    return lettuceConnectionFactory;
  }
​
  @Bean
  public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(redisConnectionFactory);
    return redisTemplate;
  }
}
  1. 使用RedisTemplate操作Redis:在需要使用Redis的地方,注入RedisTemplate对象,然后使用其提供的方法即可进行Redis操作,示例如下

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
​
@RestController
public class RedisController {
  @Autowired
  private RedisTemplate<String, Object> redisTemplate;
​
  @GetMapping("/redis/set")
  public String setKey() {
    redisTemplate.opsForValue().set("key", "value");
    return "OK";
  }
​
  @GetMapping("/redis/get")
  public String getKey() {
    Object value = redisTemplate.opsForValue().get("key");
    return value != null ? value.toString() : "null";
  }
}

通过以上步骤,你就可以在Spring Boot中成功整合Redis,并使用Redis相关的功能了。

5.对字符串的序列化操作

在Spring Boot中,对Redis的字符串设置进行序列化是为了在将对象存储到Redis中时将其转换为字符串格式,并在从Redis中获取对象时将其转换回相应的对象。

序列化的作用主要有以下几个方面:

  1. 数据存储:Redis是内存数据库,它的数据存储在内存中,如果直接将对象存储到Redis中,会占用大量的内存空间。因此,将对象进行序列化后再存储到Redis中,可以减少数据存储所占用的内存空间。

  2. 网络传输:在Redis集群或分布式部署中,数据需要在不同的节点之间进行传输。如果直接传输对象,会增加网络传输的负担,而序列化后的字符串可以更轻松地在网络中传输。

  3. 兼容性:将对象序列化为字符串后,可以方便地与其他支持相同序列化协议的系统进行交互。不同系统之间使用统一的序列化协议,可以提高系统之间的兼容性和互操作性。

在Spring Boot中,可以通过配置RedisTemplate的序列化方式,将对象序列化为字符串并存储到Redis中。常用的序列化方式有以下几种:

  • JacksonJsonRedisSerializer:将对象序列化为JSON格式的字符串。

  • StringRedisSerializer:将对象序列化为字符串。

  • JdkSerializationRedisSerializer:将对象序列化为字节数组。

在同一个配置类中配置Jackson对字符串和Hash类型的序列化,示例代码如下:

@Configuration
public class RedisConfig {
​
  @Bean
  public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(redisConnectionFactory);
​
    // 设置字符串的序列化器
    redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class));
    redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(String.class));
​
    // 设置Hash类型的序列化器
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.setHashKeySerializer(new StringRedisSerializer());
    redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
    redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
​
    redisTemplate.afterPropertiesSet();
    return redisTemplate;
  }
}

通过以上配置,可以同时对字符串和Hash类型进行序列化操作。在Redis中存储字符串和Hash类型的数据时,会自动进行相应的序列化操作,从Redis中获取数据时会自动进行反序列化操作

6.用户登录的token保存在redis中,设置一个30分钟的过期时间示例

下面是一个简单的示例,演示了如何在Spring Boot中使用Redis作为缓存来保存用户的登录Token,并设置了30分钟的过期时间:

@Service
public class TokenService {
  
  private static final long EXPIRATION_TIME = 30 * 60; // 30分钟,单位为秒
​
  private final RedisTemplate<String, String> redisTemplate;
​
  public TokenService(RedisTemplate<String, String> redisTemplate) {
    this.redisTemplate = redisTemplate;
  }
​
  public void saveToken(String userId, String token) {
    String key = "userToken:" + userId;
    redisTemplate.opsForValue().set(key, token, EXPIRATION_TIME, TimeUnit.SECONDS);
  }
​
  public String getToken(String userId) {
    String key = "userToken:" + userId;
    return redisTemplate.opsForValue().get(key);
  }
​
  public void removeToken(String userId) {
    String key = "userToken:" + userId;
    redisTemplate.delete(key);
  }
}

7.热搜的简单示例

@RestController
public class HotSearchController {
​
  @Autowired
  private StringRedisTemplate stringRedisTemplate;
​
  @PostMapping("/hotsearch")
  public void addHotSearch(@RequestParam String keyword) {
    stringRedisTemplate.opsForZSet().incrementScore("hotsearch", keyword, 1);
  }
​
  @GetMapping("/hotsearch")
  public List<String> getHotSearch() {
    Set<String> hotSearches = stringRedisTemplate.opsForZSet().reverseRange("hotsearch", 0, 10);
    return new ArrayList<>(hotSearches);
  }
}

在上述示例中,我们使用了Redis的有序集合(Sorted Set)来保存热搜关键词。每次有新的关键词被搜索,我们会通过opsForZSet().incrementScore()方法增加该关键词的分数,表示搜索热度。然后,我们可以通过opsForZSet().reverseRange()方法获取热搜关键词的排名,并返回给前端。

8.RedisUtils简单工具类

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
​
import java.util.Map;
import java.util.concurrent.TimeUnit;
​
@Component
public class RedisUtils {
​
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
​
    // 存储字符串值
    public void setString(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }
​
    // 获取字符串值
    public String getString(String key) {
        return (String) redisTemplate.opsForValue().get(key);
    }
​
    // 删除字符串值
    public void deleteString(String key) {
        redisTemplate.delete(key);
    }
​
    // 存储hash值
    public void setHash(String key, Map<String, Object> hash) {
        redisTemplate.opsForHash().putAll(key, hash);
    }
​
    // 获取hash值
    public Map<Object, Object> getHash(String key) {
        return redisTemplate.opsForHash().entries(key);
    }
​
    // 获取hash指定字段的值
    public Object getHashField(String key, String field) {
        return redisTemplate.opsForHash().get(key, field);
    }
​
    // 删除hash值
    public void deleteHash(String key, Object... fields) {
        redisTemplate.opsForHash().delete(key, fields);
    }
​
    // 设置过期时间(秒)
    public void expire(String key, long seconds) {
        redisTemplate.expire(key, seconds, TimeUnit.SECONDS);
    }
}

这个RedisUtils工具类封装了Redis的常用操作方法,包括存储和获取字符串值、存储和获取Hash值、删除字符串和Hash值以及设置过期时间。

9.Resis的持久化

(1)Redis有两种主要的持久化方式:RDB(Redis Database)和AOF(Append Only File)。

  1. RDB持久化:

    • RDB是将Redis的数据集快照保存到磁盘上。它通过fork一个子进程来进行持久化,保证了父进程和子进程之间的数据一致性。

    • RDB持久化适用于数据备份、灾难恢复、导出数据等场景。

    • 可以通过配置文件redis.conf中的save参数来定义触发RDB持久化的条件,例如在指定的时间间隔内,如果满足至少一个条件,则会进行RDB持久化操作。

    • RDB文件通常具有较小的体积,恢复速度较快。

  2. AOF持久化

    • AOF持久化是将Redis的操作日志以追加的方式保存到文件中。当Redis重启时,通过重新执行AOF文件中的写命令来恢复数据。

    • AOF持久化适用于数据持久化恢复的要求更高的场景。

    • 可以通过配置文件redis.conf中的appendonly参数来开启AOF持久化。

    • AOF文件通常比RDB文件更大,恢复速度相对较慢。

通常情况下,可以同时开启RDB和AOF持久化,以提供更高的数据安全性和可靠性。

另外,还可以使用AOF重写将AOF文件进行压缩和优化,以减少AOF文件的体积和提高恢复速度。

可以通过修改配置文件redis.conf来配置持久化方式和相关参数。

(2)持久化示例

以下是使用RDB和AOF持久化的示例:

  1. 开启RDB持久化: 在redis.conf配置文件中找到以下行,取消注释并设置保存条件:

save 900 1        # 在900秒(15分钟)内,至少有1个key发生变化时进行持久化
save 300 10       # 在300秒(5分钟)内,至少有10个key发生变化时进行持久化
save 60 10000     # 在60秒内,至少有10000个key发生变化时进行持久化

重启Redis服务器后,Redis会自动将数据集快照保存到磁盘上。

  1. 开启AOF持久化:redis.conf配置文件中找到以下行,取消注释并设置为yes

appendonly yes    # 开启AOF持久化

重启Redis服务器后,Redis会将所有写操作追加到AOF文件中。

  1. 使用AOF重写: 在Redis命令行中执行以下命令,手动执行AOF重写:

BGREWRITEAOF      # 异步执行AOF重写

执行后,Redis会在后台进行AOF重写,将AOF文件进行压缩和优化。

(3)RDB持久化原理

RDB(Redis Database)持久化是Redis的一种快照(Snapshot)方式的持久化方法。当启用RDB持久化时,Redis会定期或在满足一定条件时将数据集保存到磁盘上。

RDB持久化的过程如下:

  1. Redis主进程将数据集写入内存缓冲区。

  2. Redis主进程调用fork()系统调用,创建一个子进程。

  3. 子进程复制父进程的内存,生成一个快照文件。

  4. 子进程通过将快照文件写入临时文件来保存数据集。

  5. 子进程完成临时文件的写入后,替换原有的RDB文件。

在RDB持久化的过程中,Redis的主进程会将数据集写入内存缓冲区,而不是直接写入磁盘。这是因为直接写入磁盘的IO操作速度较慢,如果每次写操作都需要等待IO完成,会导致Redis性能下降。

通过fork()系统调用创建子进程后,子进程会复制父进程的内存,包括数据集和执行状态。这样做的好处是,在子进程中可以安全地生成快照文件,而不会受到主进程的写操作干扰。

子进程通过将快照文件写入临时文件来保存数据集。完成写入后,子进程将临时文件替换原有的RDB文件,从而完成数据的持久化。

RDB持久化方式的优点是快速且高效,适用于定期备份和数据恢复。缺点是可能会丢失最后一次快照与当前时间之间的写操作,同时在恢复大的RDB文件时可能会导致Redis的暂停。因此,根据业务需求可以选择合适的持久化方式

(4)AOF持久化原理

AOF(Append Only File)持久化是Redis的另一种持久化方式。与RDB持久化不同,AOF持久化是通过追加写入日志文件的方式来记录Redis服务器的写操作。

AOF持久化的过程如下:

  1. Redis主进程将写操作追加写入AOF缓冲区。

  2. Redis主进程根据配置的策略将AOF缓冲区中的写操作异步地写入AOF文件,或者根据每秒同步(everysec)或fsync策略将AOF缓冲区中的写操作同步地写入AOF文件。

  3. 当Redis重新启动时,根据AOF文件中的写操作恢复数据。

在AOF持久化的过程中,Redis主进程将写操作追加写入AOF缓冲区。由于AOF是以追加的方式写入文件,所以即使服务器崩溃或断电,已经写入AOF文件中的内容仍然得以保留。同时,Redis主进程可以继续执行其他操作,不需要等待AOF文件写入操作的完成,因此能够提高性能。

根据配置的策略,Redis会将AOF缓冲区中的写操作异步地或同步地写入AOF文件。异步写入方式可以提高性能,但可能会导致在服务器崩溃时丢失一部分数据。而同步写入方式可以保证数据的完整性,但会降低性能。

在Redis重新启动时,Redis根据AOF文件中的写操作来恢复数据。它会按照写操作的顺序依次执行,重建数据集的状态。

AOF持久化方式的优点是可以提供更高的数据安全和可恢复性,还可以防止数据丢失。缺点是相对于RDB持久化,AOF文件通常更大,恢复数据的时间较长。此外,频繁的写操作可能会导致AOF文件过大,影响性能,需要使用AOF重写进行优化。

根据业务需求,可以选择RDB持久化、AOF持久化或两者同时使用来保证数据的持久化和高可用性。

10.缓存雪崩

缓存雪崩(Cache Avalanche)是指在某个时间点,缓存中的大量数据同时过期失效或者缓存服务器宕机导致大量的请求直接落到了数据库上,从而导致数据库负载突然增加,甚至引起数据库崩溃。

缓存雪崩的解决方法有以下几种:

  1. 设置合理的过期时间:缓存的过期时间最好设置为随机值,避免大量的缓存同时过期。同时可以结合定期续期,定时刷新缓存的过期时间,保持缓存的有效性。

  2. 热点数据永不过期:对于一些热点数据,可以将其过期时间设置为永不过期,这样可以保证热点数据一直存在缓存中,减少对数据库的访问压力。

  3. 限流和降级:通过限制请求的并发数或者进行流量控制,避免缓存雪崩时的大量请求同时落到数据库上。同时可以通过降级策略,将一部分请求直接返回默认值或者空值,减少对数据库的访问。

  4. 多级缓存:在缓存层面引入多级缓存机制,如本地缓存和分布式缓存的结合使用。本地缓存一般用于频繁读取且数据量较小的数据,分布式缓存用于存储大量的数据。当本地缓存失效时,可以先从分布式缓存中获取数据,避免直接落到数据库上。

  5. 做好缓存预热:在系统启动时,可以预先将热点数据加载到缓存中,避免系统刚启动时大量的缓存穿透。

  6. 缓存数据异步更新:对于需要频繁更新的数据,可以将数据的更新操作异步化,通过消息队列等方式进行异步更新,避免对数据库和缓存的直接访问。

代码示例:

方法一:设置随机过期时间

public class CacheManager {
    private static final int TIMEOUT_SECONDS = 60;
    private static final int TIMEOUT_VARIANCE = 30;
​
    private Map<String, Object> cache = new ConcurrentHashMap<>();
​
    public Object get(String key) {
        Object value = cache.get(key);
        if (value == null) {
            // 从数据库获取数据
            value = getValueFromDatabase(key);
            if (value != null) {
                // 将数据添加到缓存,并设置随机的过期时间
                int timeout = TIMEOUT_SECONDS + new Random().nextInt(TIMEOUT_VARIANCE);
                cache.put(key, value, timeout, TimeUnit.SECONDS);
            }
        }
        return value;
    }
​
    private Object getValueFromDatabase(String key) {
        // 从数据库中获取数据的逻辑
    }
}

方法二:使用热点数据永不过期

public class CacheManager {
    private Map<String, Object> cache = new ConcurrentHashMap<>();
    private Set<String> hotKeys = new HashSet<>(); // 热点数据的键集合
​
    public Object get(String key) {
        Object value = cache.get(key);
        if (value == null) {
            if (hotKeys.contains(key)) {
                // 如果是热点数据,从数据库中获取数据,并永不过期
                value = getValueFromDatabase(key);
                if (value != null) {
                    cache.put(key, value);
                }
            } else {
                // 从数据库获取数据,并设置过期时间
                value = getValueFromDatabase(key);
                if (value != null) {
                    int timeout = 60; // 设置过期时间,单位为秒
                    cache.put(key, value, timeout, TimeUnit.SECONDS);
                }
            }
        }
        return value;
    }
​
    private Object getValueFromDatabase(String key) {
        // 从数据库中获取数据的逻辑
    }
}

11.缓存穿透

缓存穿透是指在使用缓存的情况下,某个请求经过缓存层无法获取到结果,于是继续向下层数据库请求,从而导致对底层数据库的压力增大。布隆过滤器可以用来解决缓存穿透的问题。

下面是一个示例:

  1. 假设有一个电商网站,用户可以通过商品ID查询商品的详细信息。

  2. 该网站使用Redis作为缓存,当用户查询商品信息时,首先尝试从Redis中获取商品信息,如果缓存中不存在该商品信息,则需要从数据库中查询。

  3. 恶意用户通过传入不存在的商品ID进行频繁的查询,导致大量的请求绕过缓存直接查询底层数据库。

  4. 为了解决这个问题,可以使用布隆过滤器来判断用户查询的商品ID是否在缓存中存在。

  5. 在每次用户查询商品信息之前,先将该商品ID进行布隆过滤器的判断。如果判断结果是不存在,则直接返回结果为空,不再继续请求底层数据库。

  6. 如果判断结果是存在,再尝试从Redis中获取商品信息。如果缓存中存在该商品信息,则返回缓存中的结果;否则,继续向下层数据库查询,并将查询结果缓存到Redis中。

使用Redis自带的布隆过滤器实现时,可以通过以下命令来进行创建、插入和查询操作:

  1. 创建布隆过滤器:BF.RESERVE

  2. 插入元素到布隆过滤器:BF.ADD

  3. 查询元素是否存在于布隆过滤器:BF.EXISTS

代码示例:

下面是在Spring Boot中实现缓存穿透问题的示例代码

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.SimpleKey;
import org.springframework.data.redis.core.BloomOperations;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
​
@SpringBootApplication
@EnableCaching
@RestController
public class CachePenetrationExample {
​
    @Autowired
    private CacheManager cacheManager;
​
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
​
    @GetMapping("/products/{productId}")
    public String getProductInfo(@PathVariable int productId) {
        // 使用布隆过滤器判断商品ID是否存在于缓存中
        boolean exists = bloomFilterExists(productId);
        if (!exists) {
            return "Product not found";
        }
​
        // 尝试从缓存中获取商品信息
        Cache cache = cacheManager.getCache("productCache");
        String cacheKey = generateCacheKey(productId);
        Cache.ValueWrapper valueWrapper = cache.get(cacheKey);
        if (valueWrapper != null) {
            return (String) valueWrapper.get();
        }
​
        // 从数据库中查询商品信息
        String dbProductInfo = getFromDatabase(productId);
        if (dbProductInfo != null) {
            // 将查询结果缓存到缓存中
            cache.put(cacheKey, dbProductInfo);
            return dbProductInfo;
        } else {
            return "Product not found in database";
        }
    }
​
    private boolean bloomFilterExists(int productId) {
        RedisOperations<String, Object> redisOperations = redisTemplate.opsForValue();
        redisOperations.setKeySerializer(new StringRedisSerializer());
        redisOperations.setValueSerializer(new StringRedisSerializer());
​
        BloomOperations<String, Object> bloomOperations = redisOperations.opsForHyperLogLog();
        return bloomOperations.add("product_bloom_filter", String.valueOf(productId));
    }
​
    private String generateCacheKey(int productId) {
        return String.valueOf(productId);
    }
​
    private String getFromDatabase(int productId) {
        // 模拟从数据库中查询商品信息的逻辑
        if (productId == 1) {
            return "Product 1 Info";
        } else if (productId == 2) {
            return "Product 2 Info";
        } else {
            return null;
        }
    }
​
    public static void main(String[] args) {
        SpringApplication.run(CachePenetrationExample.class, args);
    }
}

在上述代码中,我们通过@EnableCaching注解开启了缓存功能,并使用CachePenetrationExample类中的CacheManagerRedisTemplate对象来操作缓存和Redis数据库。

getProductInfo()方法中,我们首先使用布隆过滤器来判断请求的商品ID是否存在于缓存中。如果布隆过滤器判定请求的ID不在缓存中,就直接返回“Product not found”。如果ID可能在缓存中,我们尝试从缓存中获取商品信息,如果能获取到就直接返回缓存中的数据。

如果缓存中没有找到对应的商品信息,我们就从数据库中查询,并将查询结果缓存到缓存中。这样,当下次查询同样的商品时,就可以直接从缓存中获取,避免了缓存穿透问题。

12.缓存击穿

缓存击穿是指一个缓存中不存在但是数据库中存在的数据,当大量的并发请求同时查询这个数据时,缓存无法命中,导致请求都落到数据库上,给数据库带来了很大的压力。

为了解决缓存击穿问题,可以采用以下策略:

  1. 基于互斥锁:在缓存失效的时候,使用互斥锁来保证只有一个线程可以访问数据库,其他线程等待结果即可。

  2. 提前加载:在缓存过期之前,提前异步加载数据到缓存中,避免缓存失效时出现大量并发请求。

  3. 热点数据永不过期:对于一些非常热门的数据,可以设置其永不过期,保证一直可以从缓存中获取,避免缓存失效。

    代码示例:

(1)基于双重检查锁定的方式解决缓存击穿问题:

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
​
@Service
public class UserService {
​
    @Cacheable(value = "users", key = "#id", sync = true)
    public User getUser(String id) {
        // 从数据库中获取用户数据
        User user = userRepository.getUserById(id);
        return user;
    }
}

上面的示例中,使用了Spring Boot的注解@Cacheable来声明缓存方法,在请求访问该方法时,会先检查缓存中是否存在数据,如果存在,则直接返回缓存数据;如果不存在,则执行方法体逻辑,并将返回结果放入缓存中。通过设置sync = true,保证只有一个线程能够进入数据库查询,其他线程等待该线程完成查询后从缓存中获取数据,从而避免了缓存击穿。

(2)使用互斥锁解决缓存击穿问题:

import org.springframework.cache.Cache;
import org.springframework.cache.Cache.ValueWrapper;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
​
@Service
public class UserService {
​
    @Autowired
    private CacheManager cacheManager;
​
    public User getUser(String id) {
        Cache cache = cacheManager.getCache("users");
        ValueWrapper valueWrapper = cache.get(id);
​
        if (valueWrapper != null) {
            return (User) valueWrapper.get();
        }
​
        synchronized (this) {
            valueWrapper = cache.get(id);
            if (valueWrapper != null) {
                return (User) valueWrapper.get();
            }
​
            User user = userRepository.getUserById(id);
            cache.put(id, user);
​
            return user;
        }
    }
}

在上面的示例中,我们通过获取CacheManager来获取缓存对象,使用互斥锁的方式保证只有一个线程能够进入数据库查询。首先检查缓存中是否存在数据,如果存在,则直接返回缓存数据;如果不存在,则加锁后再次检查缓存中是否存在数据,避免多个线程同时进行数据库查询。如果缓存仍然不存在,则从数据库中加载数据,并放入缓存中。

13.缓存穿透和缓存击穿区别

  1. 缓存穿透:指的是一个请求查询一个数据,在缓存中不存在的情况下,每次查询都直接穿过缓存,直接访问数据库。这可能是因为缓存中没有对应的key,或者是针对该key的值为空值,从而导致每次查询都无法命中缓存,直接访问数据库。缓存穿透会导致大量的无效查询访问数据库,增加了数据库的负载。

解决方法:通常的解决方法是在查询缓存之前,先进行参数或者结果校验,如果参数无效或者结果为空,则将空结果进行缓存并设置一定的过期时间,这样下次相同的查询就可以直接从缓存中获取结果。

  1. 缓存击穿:指的是一个热点的数据在缓存中失效的瞬间,大量的请求直接访问数据库,导致数据库压力过大。例如,一个热门商品的信息存在缓存中,但是在某个时间点这个缓存过期了,这时候有大量的用户请求该商品的信息,每个请求都需要访问数据库来获取数据,导致数据库压力急剧增加。

解决方法:常见的解决方法是采用互斥锁策略或者队列等机制来保证只有一个线程去数据库查询数据,其他线程等待查询结果。在查询结果返回后,将结果更新到缓存中,从而避免大量的请求直接访问数据库。

三.Rdssion

1.概念:

Redisson是一个基于Redis的分布式Java对象存储和映射框架。它提供了许多分布式对象和服务,如分布式锁分布式集合分布式Map分布式计数器等。Redisson使得在Java应用程序中使用Redis更加方便和高效。

Redisson的主要特性包括:

  1. 分布式对象:Redisson提供了一系列的分布式对象,如List、Set、Queue、Deque、Lock、ReadWriteLock等,这些对象可以跨JVM、跨数据中心进行数据共享和访问。

  2. 分布式集合:Redisson提供了一系列的分布式集合对象,如SortedSet、LexSortedSet、ScoredSortedSet等,它们支持对集合的排序和范围查询。

  3. 分布式映射:Redisson提供了分布式Map对象,它使得在不同的Java应用程序之间可以共享和访问同一个Map。

  4. 分布式锁:Redisson提供了分布式锁的实现,可以用于控制多个应用程序之间的并发访问。

  5. 分布式计数器:Redisson提供了分布式计数器的实现,可以用于实现多个应用程序之间的计数共享。

  6. 异步执行:Redisson提供了一系列的异步执行操作,可以提升系统的吞吐量和响应性能。

通过使用Redisson,开发人员可以方便地使用Redis提供的功能,并且避免了手动处理Redis连接、序列化和反序列化等繁琐的细节。同时,Redisson支持在Spring和Spring Boot等常见的Java框架中使用,使得与现有的Java应用程序集成更加简单和快捷。

2.redisson在springboot的使用添加分布式锁示例

在Spring Boot中使用Redisson进行分布式锁的示例代码如下:

  1. 首先,在pom.xml文件中添加Redisson的依赖:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.16.3</version>
</dependency>
  1. 在配置文件中添加Redis的连接信息

spring.redis.host=127.0.0.1
spring.redis.port=6379
  1. 创建一个需要进行分布式锁保护的方法,使用@RedissonLock注解来添加分布式锁

@Service
public class MyService {
​
    @RedissonLock(name = "myLock")
    public void doSomething() {
        // 这里是需要进行分布式锁保护的代码逻辑
        // ...
    }
}
  1. 创建一个切面类,用于添加分布式锁的逻辑

@Aspect
@Component
public class RedissonLockAspect {
​
    @Autowired
    private RedissonClient redissonClient;
​
    @Around("@annotation(redissonLock)")
    public Object handleLock(ProceedingJoinPoint joinPoint, RedissonLock redissonLock) throws Throwable {
        RLock lock = redissonClient.getLock(redissonLock.name());
        try {
            lock.lock();
            return joinPoint.proceed();
        } finally {
            lock.unlock();
        }
    }
}
  1. 在启动类上添加@EnableAspectJAutoProxy注解来启用AOP的功能

@SpringBootApplication
@EnableAspectJAutoProxy
public class Application {
​
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

通过以上步骤,就可以在Spring Boot中使用Redisson进行分布式锁的保护了。在需要进行分布式锁保护的方法上添加@RedissonLock注解,然后RedissonLockAspect切面类会自动为该方法添加分布式锁的逻辑。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值