SpringBoot - 集成 Redis

发布于 2024-09-02 03:06:07 字数 34161 浏览 16 评论 0

Redis 介绍

Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker.

C 语言开发的、开源的、基于内存的数据结构存储器,可以用作数据库、缓存和消息中间件一种 NoSQL(not-only sql,泛指非关系型数据库)的数据库,性能优秀,数据在内存中,读写速度非常快,支持并发 10W QPS,Redis 中文网站 的应用场景包括:缓存系统(“热点”数据:高频读、低频写)、计数器、消息队列系统、排行榜、社交网络和实时系统。

为什么要使用 redis

性能

由于 MySql 数据存储在磁盘中,对于一些需要执行耗时非常长的,但结果不会频繁改动的 SQL 操作(经常是查询,如每日排行榜或者高频业务热数据),就适合将运行结果放到到 redis 中。
后面的请求优先去 redis 中获取,加快访问速度、提高性能

并发

mysql 支持并发访问的能力有限(当然现在一般会使用一些数据库连接池的来加强并发能力),当有大量的并发请求,直接访问数据库的话,mysql 会挂掉。所以可以使用 redis 作为缓冲,让请求先访问到 redis,而不是直接访问数据库,提高系统的并发能力。

当然 redis 是基于内存的,存储容量肯定要比磁盘少很多,要存储大量数据,需升级内存,造成在一些不需要高性能的地方是相对比较浪费的,所以建议在需要性能的地方使用 redis,在不需要高性能的地方使用 mysql。不要一味的什么数据都丢到 redis 中。

Redis 主要有 5 种数据类型,包括 String、list、Set、ZSet、Hash

数据类型可以存储的值操作应用场景Java 对应
String字符串、整数或者浮点数对整个字符串或者字符串的其中一部分执行操作;对整数和浮点数执行自增或者自减操作做简单的键值对缓存类似 Java 的 String
List列表从两端压入或者弹出元素;对单个或者多个元素进行修剪,只保留一个范围内的元素存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的数据类似 Java 的 LinkedList
Set无序集合添加、获取、移除单个元素;检查一个元素是否存在于集合中;计算交集、并集、差集;从集合里面随机获取元素交集、并集、差集的操作,比如交集,可以把两个人的粉丝列表整一个交集类似 Java 中的 HashSet
ZSet(Sorted sets)有序集合添加、获取、删除元素;根据分值范围或者成员来获取元素;计算一个键的排名去重但可以排序,如获取排名前几名的用户类似 Java 的 SortedSet 和 HashMap 的结合体
Hash包含键值对的无序散列表添加、获取、移除单个键值对;获取所有键值对;检查某个键是否存在结构化的数据,比如一个对象类似 Java 的 HashMap

集成方式

下载安装

windows

截至 2021 年 1 月,官方也没提供 windows 版本的下载,如果是为了学习需要,可以到 MicrosoftArchive Github 上下载(最后的更新时间为 2016 年)

下载后像常用的软件类似:双击 redis-server.exe 即可启动 redis 服务器, 可参考 Redis 下载及安装(windows 版)

此版本对应的是 redis 的 3.2.1 版本,而目前 redis 已经发展到了 6.x,故生产环境不建议使用,自己学习就好;
当然如果生产环境中的服务器恰好是 windows 的,小型项目使用也可以(毕竟没那么多并发量),但还是建议使用下面的 docker 方式安装,使用较新的稳定版本
另外还有一种方案就是到 github 上下载 redis 对应版本的源码自己编译(或者找找网络大神的编译好的版本)

linux

直接输入命令:

sudo apt-get install redis-server

安装完成后,Redis 服务器会自动启动。

使用

ps -aux|grep redis

可以看到服务器系统进程默认端口 6379

需要手动下载安装包并运行的话,可参考 Ubuntu 安装 Redis 及使用

docker

使用 docker

下载镜像
docker pull redis
准备 redis 的配置文件

因为需要 redis 的配置文件,这里最好去 redis 的 官方网站 去下载一个 redis 使用里面的配置文件即可

拿到 redis.conf 后放到指定目录(这个目录用于后面 docker 指定本地目录用,比如我放在 D:\Software\docker\env\redis\redis.conf)

修改 redis.conf 配置文件的以下配置:

  • 注释掉 bind 127.0.0.1 使 redis 可以外部访问,则注释掉这部分
  • 修改 protected-mode no 不限制只能本地访问
  • 修改 requirepass 123456 #给 redis 设置密码(如果需要)

运行

# docker run -p 6379:6379 --name redis -v D:\Software\docker\env\redis\redis.conf:/etc/redis/redis.conf  -v D:\Software\docker\env\redis\data:/data -d redis redis-server /etc/redis/redis.conf --appendonly yes
docker run -p 6379:6379 --name redis --restart=always -v /d/Software/docker/env/redis/redis.conf:/etc/redis/redis.conf -v /d/Software/docker/env/redis/data:/data -d redis redis-server /etc/redis/redis.conf --appendonly yes
# /d/ Windows 的 D 盘

参数解释:

  • -p 6379:6379:把容器内的 6379 端口映射到宿主机 6379 端口
  • -v /d/Software/docker/env/redis.conf:/etc/redis/redis.conf:把宿主机配置好的 redis.conf 放到容器内的这个位置中
  • -v /d/Software/docker/env/redis/data:/data:把 redis 持久化的数据在宿主机内显示,做数据备份
  • redis-server /etc/redis/redis.conf:这个是关键配置,让 redis 不是无配置启动,而是按照这个 redis.conf 的配置启动
  • appendonly yes:redis 启动后数据持久化

其他定制配置可参考 hub.docker

Redis 可视化客户端

Redis 的可视化客户端目前较流行的有:

  • Redis Desktop Manager: 基于 Qt5 的跨平台 Redis 桌面管理软件, 下载地址 , 不过 0.9.3 版本之后就开始付费使用了,只能下载 0.9.3 的版本。

redis-desktop-manager

个人推荐 Another Redis DeskTop Manager,作为替代方案

引入

访问 Redis,直接引入 spring-boot-starter-data-redis 依赖即可(它实际上是 Spring Data 的一个子项目—— Spring Data Redis )

在 pom.xml 中加入

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- lettuce pool 缓存连接池 如果不需要在 yml 中自定义 pool 配置, 则不需要引用-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.5.0</version>
</dependency>
</dependencies>

Springboot2 以后,底层访问 redis 已经不再是 jedis 了,而是默认 lettuce

  • 使用 jedis:当多线程使用同一个连接时,是线程不安全的。所以要使用连接池,为每个 jedis 实例分配一个连接。
  • 使用 Lettuce:当多线程使用同一连接实例时,是线程安全的。是采用 netty 连接 redis server,实例可以在多个线程间共享,不存在线程不安全的情况,这样可以减少线程数量

所以如果要继续使用 jedis 的话需要改为:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
</dependencies>

推荐使用 lettuce

配置

如果使用默认 lettuce 的话,直接在 application.yml 配置 redis 服务连接基本参数即可(spring-boot-starter-xx 的好处之一):

spring:
## Redis 配置
redis:
## Redis 数据库索引(默认为 0)
database: 1
## Redis 服务器地址
host: 127.0.0.1
## Redis 服务器连接端口
port: 6379
## Redis 服务器连接密码(默认为空)
password:

如果需要配置连接池的参数的话:

  • 使用 lettuce:
lettuce:
pool:
## 连接池最大连接数(使用负值表示没有限制) 默认 8
max-active: 500
## 连接池中的最小空闲连接 默认 0
min-idle: 0
## 连接池中的最大空闲连接 默认 8
max-idle: 500
##连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: 1000
  • 使用 jedis
jedis:
pool:
## 连接池最大连接数(使用负值表示没有限制)
#spring.redis.pool.max-active=8
max-active: 8
## 连接池最大阻塞等待时间(使用负值表示没有限制)
#spring.redis.pool.max-wait=-1
max-wait: -1
## 连接池中的最大空闲连接
#spring.redis.pool.max-idle=8
max-idle: 8
## 连接池中的最小空闲连接
#spring.redis.pool.min-idle=0
min-idle: 0

使用

编写公共配置

@EnableCaching
@Configuration("cache")
public class RedisConfig extends CachingConfigurerSupport {

@Autowired
RedisConnectionFactory redisConnectionFactory;

@Bean
public RedisTemplate<String, Object> objectRedisTemplate() {
return configRedisTemplate(Object.class, redisConnectionFactory);
}

/**
* 可以根据自己实际项目需要,定制多个 CacheManager,注解的地方可以指定使用哪个 CacheManager
* @param objectRedisTemplate
* @return
*/
@Bean
public CacheManager cacheManager(RedisTemplate<String, Object> objectRedisTemplate) {
//如果支持使用 objectRedisTemplate,没有用注解或者 cacheManager 方式的话,则此配置不生效,即 key 相关的规则,需使用者自己定义
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration
.defaultCacheConfig()
// .entryTtl(Duration.ofDays(1))
.disableCachingNullValues()
.computePrefixWith(cacheName -> "spring-redis".concat(":").concat(cacheName).concat(":"))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(objectRedisTemplate.getStringSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(objectRedisTemplate.getValueSerializer()));

Set<String> cacheNames = new HashSet<>();
cacheNames.add("user");


// 对每个缓存空间应用不同的配置
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
configMap.put("user", cacheConfiguration.entryTtl(Duration.ofSeconds(120)));

return RedisCacheManager.builder(Objects.requireNonNull(objectRedisTemplate.getConnectionFactory()))
.cacheDefaults(cacheConfiguration)
.initialCacheNames(cacheNames)
.withInitialCacheConfigurations(configMap)
//在 spring 事务正常提交时才缓存数据
.transactionAware()
.build();
}

private <T> RedisTemplate<String, T> configRedisTemplate(Class<T> clazz, RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, T> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);

Jackson2JsonRedisSerializer<T> j2jrs = new Jackson2JsonRedisSerializer<>(clazz);
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get 和 set,以及修饰符范围,ANY 是都有包括 private 和 public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 解决 jackson2 无法反序列化 LocalDateTime 的问题
om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
om.registerModule(new JavaTimeModule());

// 指定序列化输入的类型,类必须是非 final 修饰的,final 修饰的类,比如 String,Integer 等会跑出异常
// om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
j2jrs.setObjectMapper(om);

redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(j2jrs);
redisTemplate.setHashValueSerializer(j2jrs);
redisTemplate.afterPropertiesSet();

return redisTemplate;
}

}

service 中使用

注意推荐使用 SpringCache 中的注解和类,这样就算后期不使用 redis,改用 mongodb 或者其他缓存中间件时,业务代码都不需要变更,这里体现了 Java 中的门面模式(外观模式)

使用 @CacheConfig 相关注解

项目中一定要加上 @EnableCaching

@Service("cacheAnnotationUserService")
@CacheConfig(cacheNames = "user")
public class CacheAnnotationUserService implements UserService {

private final Logger logger = LoggerFactory.getLogger(getClass());

/**
* Cacheable[] cacheable() default {}; //声明多个 @Cacheable
* CachePut[] put() default {}; //声明多个 @CachePut
* CacheEvict[] evict() default {}; //声明多个 @CacheEvict
* 插入用户
*/
@Caching(put = {@CachePut(key = "#user.id")})
@Override
public User saveUser(User user) {
logger.info("插入用户: {}", user.getUsername());
return user;
}

/**
* --@Cacheable 注解会先查询是否已经有缓存,有会使用缓存,没有则会执行方法并缓存
* 命名空间:@Cacheable 的 value 会替换 @CacheConfig 的 cacheNames(两者必须有一个)
* --key 是[命名空间]::[@Cacheable 的 key 或者 KeyGenerator 生成的 key](@Cacheable 的 key 优先级高,KeyGenerator 不配置走默认 KeyGenerator SimpleKey [])
* 使用 sync = true 保证只有一个线程访问数据库,避免缓存击穿 ,注意 sync = true 不能与 unless="#result == null"一起使用
*/
@Cacheable(key = "#userId", sync = true)
@Override
public User findUser(Long userId) {
logger.info("查找用户: {}", userId);
return null;
}

/**
* --@CachePut 注解的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,
* 和 @Cacheable 不同的是,它每次都会触发真实方法的调用
* 简单来说就是用户更新缓存数据。但需要注意的是该注解的 value 和 key 必须与要更新的缓存相同,也就是与 @Cacheable 相同
* 默认先执行数据库更新再执行缓存更新
* 注意返回值必须是要修改后的数据
*/
@Override
@CachePut(key = "#user.id")
public User updateUser(User user) {
logger.info("更新用户:{}", user.getId());
return user;
}

@Override
@CacheEvict(key = "#userId")
public User deleteById(Long userId) {
logger.info("删除用户:{}", userId);
return null;
}

/**
* --@CachEvict 的作用 主要针对方法配置,能够根据一定的条件对缓存进行清空
* 触发缓存清除
* 默认先执行数据库删除再执行缓存删除
*/
@Override
@CacheEvict(allEntries = true)
public void clear() {
logger.info("清除所有");
}

}

使用 CacheManager

注解方式适合逻辑不是很复杂的情况,当业务逻辑需要更加灵活的控制缓存处理时,可使用 CacheManager 来管理

@Service("cacheManagerUserService")
public class CacheManagerUserService implements UserService {

private final Logger logger = LoggerFactory.getLogger(getClass());

@Autowired
private CacheManager cacheManager;

@Override
public User saveUser(User user) {
getUserCache().put(user.getId(), user);
logger.info("保存用户: {}", user);
return user;
}

@Override
public User findUser(Long userId) {
return getUserCache().get(userId, User.class);
}

@Override
public User updateUser(User user) {
getUserCache().put(user.getId(), user);
return user;
}

@Override
public User deleteById(Long userId) {
getUserCache().evict(userId);
return null;
}

@Override
public void clear() {
getUserCache().clear();
}

private Cache getUserCache() {
return cacheManager.getCache("user");
}
}

通过 @RedisHash 注解存储实体到 redis

参考 Introduction to Spring Data Redis 可以像其他数据库一样继承 CrudRepository 来操作对象

如果需要将某个属性标识为唯一 id,添加 @Id 注解即可

如果需要在 redis 存储中拥有生命周期,添加 @TimeToLive 注解;以秒为单位,可根据需要设置其失效时间:

@RedisHash("Student")
public class Student implements Serializable {

@Id
private String id;
private String name;
private Gender gender;
private int grade;

/**
* 以秒为单位,失效时间
*/
@TimeToLive
private Long time;

public enum Gender {
MALE, FEMALE
}

//.......

}

当然如果对一个类想要整体设置过期时间,可以使用 @RedisHash(value = “Student”, timeToLive = 20L)

直接使用 RedisTemplate

当然如果直接使用 RedisTemplate 也是可以的,不过需要注意的是一旦直接使用了 RedisTemplate,则 cacheManager 相关的配置将不会生效,包含 CachingConfigurerSupport 相关的也不会生效,如发生异常时将不会回调 CacheErrorHandler

@Service("redisTemplateUserService")
public class RedisTemplateUserService implements UserService {

private final Logger logger = LoggerFactory.getLogger(getClass());

public final String PREFIX_CACHE_REDIS_KEY_USER = "spring-redis:user:";

@Autowired
RedisTemplate<String, User> userRedisTemplate;

@Override
public User saveUser(User user) {
userRedisTemplate.opsForValue().set(getRealKeyById(user.getId()), user);
return null;
}

@Override
public User findUser(Long userId) {
return userRedisTemplate.opsForValue().get(getRealKeyById(userId));
}

@Override
public User updateUser(User user) {
userRedisTemplate.opsForValue().set(getRealKeyById(user.getId()), user);
return null;
}

@Override
public User deleteById(Long userId) {
Boolean result = userRedisTemplate.delete(getRealKeyById(userId));
logger.info("删除结果: {}", result);
return null;
}

@Override
public void clear() {
Set<String> keys = userRedisTemplate.keys(PREFIX_CACHE_REDIS_KEY_USER + "*");
if (keys != null) {
userRedisTemplate.delete(keys);
}
}

/**
* 获取真实 key
*
* @param userId
* @return
*/
private String getRealKeyById(Long userId) {
return PREFIX_CACHE_REDIS_KEY_USER + userId;
}
}

key 需要自己定义前缀,当然之间使用 RedisTemplate 可以直接控制更加底层的 api

分布式锁实现

单机情况下使用 jvm 提供的锁机制即可

  • 方式一: 直接在方法上加上 synchronized(或者在关键代码上使用),缺点购买多个商品时效率会较低(当然因为 java8 对于 synchronized 做了很多优化,效率也不会有多差相较于 lock)
  • 方式二: 使用 lock, 缺点如果逻辑上出现异常(未捕获的) 导致锁未及时释放的话,会导致后面的请求都会由于获取不到锁而失败,一个商品抢购还好,如果好多商品,会因为某一个异常导致所有商品都失败
  • 方式三(推荐):使用 ConcurrentHashMap,一个商品一个锁(或者一段数量一个锁),这样可以在某个商品秒杀出现异常时,不影响其他商品

如果是单机环境的话,使用 ConcurrentHashMap 是不错的选择,不过如果是集群情况下(部署到多个服务器上),使用 jvm 的锁机制就满足不了需求了

使用 redis 实现

可参考 使用 Spring Boot AOP 实现 Web 日志处理和分布式锁

@Component
public class RedisLockUtils {

@Autowired
RedisTemplate<String, Object> redisTemplate;

private final Logger logger = LoggerFactory.getLogger(getClass());

public String getLock(String key, long timeout, TimeUnit timeUnit) {
try {
String value = UUID.randomUUID().toString();
Boolean lockStat = redisTemplate.execute((RedisCallback< Boolean>) connection ->
connection.set(key.getBytes(StandardCharsets.UTF_8), value.getBytes(StandardCharsets.UTF_8),
Expiration.from(timeout, timeUnit), RedisStringCommands.SetOption.SET_IF_ABSENT));
if (!lockStat) {
// 获取锁失败。
return null;
}
return value;
} catch (Exception e) {
logger.error("获取分布式锁失败,key={}", key, e);
return null;
}
}

public void unLock(String key, String value) {
try {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
boolean unLockStat = redisTemplate.execute((RedisCallback< Boolean>)connection ->
connection.eval(script.getBytes(), ReturnType.BOOLEAN, 1,
key.getBytes(StandardCharsets.UTF_8), value.getBytes(StandardCharsets.UTF_8)));
if (!unLockStat) {
logger.error("释放分布式锁失败,key={},已自动超时,其他线程可能已经重新获取锁", key);
}
} catch (Exception e) {
logger.error("释放分布式锁失败,key={}", key, e);
}
}

}

使用 redisson

使用 redis 做分布式锁时容易发生死锁等未知情况,实际项目还是推荐使用 redisson 来实现 分布式分段锁

pom.xml 中引入

<dependencies>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.14.0</version>
<exclusions>
<exclusion>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-data-23</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-data-24</artifactId>
<version>3.14.0</version>
</dependency>
</dependencies>

redisson-spring-data-24, 取决于项目中使用的 SpringBoot 版本,因为我使用了 SpringBoot 2.4.2,故引用 24,而 redisson-spring-boot-starter 的 3.14.0 版本默认引用的是 23,故需排除

resource 下新建 redisson-single.yml(单机版):

ngleServerConfig:
# 连接空闲超时,单位:毫秒
idleConnectionTimeout: 10000
# 连接超时,单位:毫秒
connectTimeout: 10000
# 命令等待超时,单位:毫秒
timeout: 3000
# 命令失败重试次数,如果尝试达到 retryAttempts(命令失败重试次数) 仍然不能将命令发送至某个指定的节点时,将抛出错误。
# 如果尝试在此限制之内发送成功,则开始启用 timeout(命令等待超时) 计时。
retryAttempts: 3
# 命令重试发送时间间隔,单位:毫秒
retryInterval: 1500
# # 重新连接时间间隔,单位:毫秒
# reconnectionTimeout: 3000
# # 执行失败最大次数
# failedAttempts: 3
# 密码
password:
# 单个连接最大订阅数量
subscriptionsPerConnection: 5
# 客户端名称
clientName: null
# # 节点地址
address: "redis://127.0.0.1:6379"
# 发布和订阅连接的最小空闲连接数
subscriptionConnectionMinimumIdleSize: 1
# 发布和订阅连接池大小
subscriptionConnectionPoolSize: 50
# 最小空闲连接数
connectionMinimumIdleSize: 32
# 连接池大小
connectionPoolSize: 64
# 数据库编号
database: 2
# DNS 监测时间间隔,单位:毫秒
dnsMonitoringInterval: 5000
# 线程池数量,默认值: 当前处理核数量 * 2
threads: 0
# Netty 线程池数量,默认值: 当前处理核数量 * 2
nettyThreads: 0
# 编码
codec: !<org.redisson.codec.JsonJacksonCodec> {}
# 传输模式
transportMode : "NIO"

在 application.yml 中加入:

spring:
## Redis 配置
redis:

# redisson 相关配置, 会导致 redis 原有配置失效,使用时需注意
redisson:
file: "classpath:redisson-single.yml"

这样的好处在于,不想引用 redisson 的话,只要去除 pom 中的引用并去除 spring.redis.redisson 配置即可

即可使用 RedissonClient 获取分布式锁:

@Service
class TestService {

@Autowired(required = false)
private RedissonClient redissonClient;

private boolean buyWithRedissonLock(Long productId) {
if(redissonClient == null) {
logger.error("未配置 RedissonClient");
return false;
}
String key = "xxx_lock_" + productId;
RLock lock = redissonClient.getLock(key);
try {
if(lock.tryLock(2, TimeUnit.SECONDS)) {
return normalBuy(productId);
}
logger.error("获取不到锁");
return false;
} catch (Exception e) {
e.printStackTrace();
logger.error("获取锁异常", e);
return false;
} finally {
lock.unlock();
}
}
}

再进阶的话就可以再封装一层读写锁

另外 redis 和 redisson 也支持发布和订阅的功能 convertAndSend,有兴趣可以了解下

注意要点

缓存的处理策略一般是:前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果。所以可能就会存在以下问题:

缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为 id 为“-1”的数据或 id 为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大

如何解决?

  • 接口层增加校验,如用户鉴权校验,id 做基础校验,id<=0 的直接拦截
  • 从缓存取不到的数据,在数据库中也没有取到,这时也可以将 key-value 对写为 key-null,缓存有效时间可以设置短点,如 30 秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个 id 暴力攻击
  • 加一层布隆过滤器

当然一般情况下,在接口层增加校验即可,真正业务发展大了,存在攻击了,所采取的策略不会这么简单

缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力

如何解决?

  • 加锁
  • 将过期时间组合写在 value 中,通过异步的方式不断的刷新过期时间,防止此类现象
  • 设置热点数据永远不过期
//使用 lock 解决缓存击穿问题(粗颗粒度锁)
private Lock lock = new ReentrantLock();

//使用 ConcurrentHashMap 解决缓存击穿问题(细颗粒度锁-推荐)
private ConcurrentHashMap<String, Lock> lockConcurrentHashMap = new ConcurrentHashMap<>();

缓存雪崩

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至 down 机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库

如何解决?

  • 针对不同 key 设置不同的过期时间,过期时间设置随机,防止同一时间大量数据过期现象发生。
  • 限流,如果 redis 宕机,可以限流,避免同时刻大量请求打崩 DB
  • 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
  • 加入二级缓存,提前加载热 key 数据到内存中,如果 redis 宕机,走内存查询
  • 设置热点数据永远不过期

进阶: Redis 部署模式

standalone(单机) 模式

部署在一台服务器中,并发需求不太高时

master/slaver(主从复制) 模式

部署在多台服务器中,一个主节点,多个从节点

优点

  • 数据备份: 当一个节点损坏(指不可恢复的硬件损坏)时,数据因为有备份,可以方便恢复
  • 负载均衡:所有客户端都访问一个节点肯定会影响 Redis 工作效率,有了主从以后,可做读写分离,查询操作就可以通过查询从节点来完成

缺点

master 节点挂了以后,redis 就不能对外提供写服务了,因为剩下的 slave 不能成为 master

搭建方式

这里就演示 windows docker desktop 下的搭建方式, 以下命令都用 cmd 或者 powerShell

  1. 拉取最新 redis 镜像 docker pull redis
  2. 从官方下载最新的 redis, 找到 redis.conf 文件,拷贝到本地某个文件夹下,如:D:\env\docker\redis\config\redis.conf
  3. 配置并运行主服务器

redis.conf 中找到下面的配置并修改:

# bind 127.0.0.1
requirepass 123456 #给 redis 设置密码
appendonly yes #redis 持久化  默认是 no

dir /usr/local/etc/redis/redis-master/data/ #db 等相关目录位置(根据自己需要配置)

bind 配置可以修改为 bind 0.0.0.0, 或者指定 ip, 或者直接注释掉(这里我们选择直接注释掉,允许所有来自于可用网络接口的连接),appendonly 开启后,Redis 会把每次写入的数据在接收后都写入 appendonly.aof 文件,每次启动时 Redis 都会先把这个文件的数据读入内存里

实际项目中可能还需要记录日志,可配置 logfile 并映射本地文件即可

docker run --name redis -p 6379:6379 -v /d/env/docker/redis/conf/redis.conf:/usr/local/etc/redis/redis-master/redis.conf -v /d/env/docker/redis/data/:/usr/local/etc/redis/redis-master/data/ -d redis redis-server /usr/local/etc/redis/redis-master/redis.conf

#docker run -p <容器端口>:<主机端口> –name <容器名> -v <本地配置文件映射容器配置文件> -v <本地文件夹挂载到容器文件夹> -d(表示以守护进程方式启动容器) <启动 redis 服务并制定配置文件(容器中的路径)>

  1. 使用 Redis Desktop Manager 测试是否连接成功
  2. 配置从服务器 1,拷贝新的 redis.conf 并重命名为 redis-slave-1.conf,找到下面的配置并编辑:
port 6380
# bind 127.0.0.1
requirepass 123456 #给 redis 设置密码
appendonly yes #redis 持久化  默认是 no

masterauth 123456 #主服务器密码
# replicaof <master ip> <master port>
replicaof 192.168.31.13 6379 #Redis 主机(Master)IP 端口

replicaof 为主服务器的 ip+端口(在 redis5.x 的主从配置中,从机配置要配置 replicaof 参数。而早期版本,要配置的是 slaveof 参数),如果主服务器设置了密码则需配置 masterauth

  1. 运行从服务器 1
docker run --name redis-slave-1 -p 6380:6380 -v /d/env/docker/redis/conf/redis-slave-1.conf:/usr/local/etc/redis/redis-slave-1/redis.conf -d redis redis-server /usr/local/etc/redis/redis-slave-1/redis.conf
  1. 按照类似步骤配置并运行从服务器 2

为了方便,直接拷贝 redis-slave-1.conf 并修改 port 即可

redis-slave-2.conf

port 6381

运行

docker run --name redis-slave-2 -p 6381:6381 -v /d/env/docker/redis/conf/redis-slave-2.conf:/usr/local/etc/redis/redis-slave-2/redis.conf -d redis redis-server /usr/local/etc/redis/redis-slave-2/redis.conf

以上便可以搭建 1 主 2 从的 master/slaver(主从复制) 模式, 通过向主服务器写入数据,两个从服务器即会自动同步数据

如果是本机 dockers,可以在每个 redis-xx.conf 中修改,以便显示声明物理机的 ip 与 port,如 redis-slave-1.conf

replica-announce-ip 192.168.31.13 # 这里写自己的 ip 地址
replica-announce-port 6380 # 这里写绑定的 redis 服务端口

sentinel(哨兵) 模式

Sentinel 其实是运行在特殊模式下的 redis server, 部署在多台服务器中, 心跳机制+投票裁决,是建立在主从模式的基础上,这也是目前的主流方案, 可参考 官方文章-Redis Sentinel 文档

优点

有效解决主从模式主库异常手动主从切换的问题

缺点

当数据量过大到一台服务器存放不下的情况时,主从模式或 sentinel 模式就不能满足需求了,这个时候需要对存储的数据进行分片,将数据存储到多个 Redis 实例中

搭建方式

先搭建 1 个主服务器和两个从服务器,搭建方式同上面的 master/slaver(主从复制) 模式,我们还是通过 windows docker desktop 的方式

由于 Sentinel 启动,故障切换,日志文件创建 等情况均需要修改配置文件,因此一定要给文件读写权限,因此启动前先 chmod 777 -R /data/redis/ 给所有文件夹配置好权限

下面再搭建 1 个哨兵

  1. 哨兵 1

从官方下载最新的 redis, 找到 sentinel.conf 文件(windows 版的是没有这个文件的,需要自己新建或者官网下载 linux 版本),拷贝到本地某个文件夹下,如:D:\env\docker\redis\config\sentinel-1.conf

编辑文件

# 禁止保护模式
protected-mode no
# 配置监听的主服务器,这里 sentinel monitor 代表监控,mymaster 代表服务器的名称,可以自定义,192.168.11.128 代表监控的主服务器,6379 代表端口,2 代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行 failover 操作。
sentinel monitor mymaster 192.168.11.128 6379 1
# sentinel author-pass 定义服务的密码,mymaster 是服务名称,123456 是 Redis 服务器密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster 123456
logfile "./sentinel_log.log"

运行

docker run --name sentinel-1 -p 26379:26379 -v  /d/env/docker/redis/conf/sentinel-1.conf:/usr/local/etc/redis/sentinel-1.conf -d redis redis-sentinel /usr/local/etc/redis/sentinel-1.conf

实际环境中对于哨兵也会有多个,一个哨兵进程对 Redis 服务器进行监控,可能会出现问题,为此,需要使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式

需先修改 sentinel-1.conf 中的 sentinel monitor


# 配置监听的主服务器,这里 sentinel monitor 代表监控,mymaster 代表服务器的名称,可以自定义,192.168.11.128 代表监控的主服务器,6379 代表端口,2 代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行 failover 操作。
sentinel monitor mymaster 192.168.11.128 6379 2
  1. 哨兵 2
    he
    拷贝一份 sentinel-1.conf, 重命名为 sentinel-2.conf,修改端口号即可
port 26380

运行

docker run --name sentinel-2 -p 26380:26380 -v  /d/env/docker/redis/conf/sentinel-2.conf:/usr/local/etc/redis/sentinel-2.conf -d redis redis-sentinel /usr/local/etc/redis/sentinel-2.conf
  1. 哨兵 3

同样拷贝一份 sentinel-1.conf, 重命名为 sentinel-3.conf,修改端口号即可

port 26381

运行

docker run --name sentinel-3 -p 26381:26381 -v  /d/env/docker/redis/conf/sentinel-3.conf:/usr/local/etc/redis/sentinel-3.conf -d redis redis-sentinel /usr/local/etc/redis/sentinel-3.conf

如果指定了新的 dir, 如

#Sentinel 服务运行时使用的临时文件夹
dir /usr/local/etc/redis

docker run --name sentinel-3 -p 26384:26384  -v  /d/env/docker/redis/conf/sentinel-3.conf:/usr/local/etc/redis-sentinel/sentinel.conf  -v  /d/tmp:/usr/local/etc/redis -d redis redis-sentinel /usr/local/etc/redis-sentinel/sentinel.conf

注意启动的顺序: 首先是主机的 Redis 服务进程,然后启动从机的服务进程,最后启动 3 个哨兵的服务进程

测试

使用 redis-cli –p 26379 查看信息

sentinel-info

关闭主服务器,等待 30 秒, 可以看到已经切换到某个从服务器中了

如果是本机 dockers,可以在每个 sentinel-xx.conf 中修改,以便显示声明物理机的 ip 与 port,如 sentinel-1.conf

sentinel announce-ip <ip> # 这里写自己的 ip 地址
sentinel announce-port <port> # 这里写绑定的 redis-sentinel 服务端口

Springboot 整合哨兵模式

application.yml

spring:
redis:
database: 0
password: 12345
sentinel:
master: mymaster ## master 名称
## 哨兵节点的 ip 和端口好,哨兵会托管主从的架构
nodes: 127.0.0.1:26379

cluster(集群) 模式

部署在多台服务器中,3.0 版本开始正式引入,cluster 的出现是为了解决单机 Redis 容量有限的问题,将 Redis 的数据根据一定的规则分配到多台机器,可以理解为是哨兵和主从模式的结合体

这种模式适合数据量巨大的缓存要求,当数据量不是很大使用 sentinel 即可

IBM 开发者文章-了解 Redis 并在 Spring Boot 项目中使用 Redis

Spring Boot 2.4.0「新增 RedisCacheMetrics」:用于监控使用 redis 时的 puts、gets、deletes 以及缓存命中率等信息
此指标信息默认不开启,需你增加配置 spring.cache.redis.enable-statistics = true

笔者使用了某云服务器部署了一些测试项目,结果竟然被爆出对外存在攻击行为,最后发现是被人攻占了 redis 的端口 6379,所以部署 redis 建议修改默认端口或者限制 IP 访问,开启密码认证功能,并使用强密码。

Redis vs MongoDB

本文示例源码

面试常问

Redis 为什么快呢?

redis 的速度非常的快,单机的 redis 就可以支撑每秒 10 几万的并发,相对于 mysql 来说,性能是 mysql 的几十倍。速度快的原因主要有几点:

  • 完全基于内存操作
  • C 语言实现,优化过的数据结构,基于几种基础的数据结构,redis 做了大量的优化,性能极高
  • 使用单线程,无上下文的切换成本
  • 基于非阻塞的 IO 多路复用机制

那为什么 Redis6.0 之后又改用多线程呢?

redis 使用多线程并非是完全摒弃单线程,redis 还是使用单线程模型来处理客户端的请求,只是使用多线程来处理数据的读写和协议解析,执行命令还是使用单线程。

这样做的目的是因为 redis 的性能瓶颈在于网络 IO 而非 CPU,使用多线程能提升 IO 读写的效率,从而整体提高 redis 的性能。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

东走西顾

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

玍銹的英雄夢

文章 0 评论 0

我不会写诗

文章 0 评论 0

十六岁半

文章 0 评论 0

浸婚纱

文章 0 评论 0

qq_kJ6XkX

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文