SpringBoot - 集成 Redis
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 的可视化客户端目前较流行的有:
个人推荐 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
- 拉取最新 redis 镜像
docker pull redis
- 从官方下载最新的 redis, 找到 redis.conf 文件,拷贝到本地某个文件夹下,如:D:\env\docker\redis\config\redis.conf
- 配置并运行主服务器
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 服务并制定配置文件(容器中的路径)>
- 使用 Redis Desktop Manager 测试是否连接成功
- 配置从服务器 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
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
- 按照类似步骤配置并运行从服务器 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
从官方下载最新的 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
- 哨兵 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
- 哨兵 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 查看信息
关闭主服务器,等待 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 为什么快呢?
redis 的速度非常的快,单机的 redis 就可以支撑每秒 10 几万的并发,相对于 mysql 来说,性能是 mysql 的几十倍。速度快的原因主要有几点:
- 完全基于内存操作
- C 语言实现,优化过的数据结构,基于几种基础的数据结构,redis 做了大量的优化,性能极高
- 使用单线程,无上下文的切换成本
- 基于非阻塞的 IO 多路复用机制
那为什么 Redis6.0 之后又改用多线程呢?
redis 使用多线程并非是完全摒弃单线程,redis 还是使用单线程模型来处理客户端的请求,只是使用多线程来处理数据的读写和协议解析,执行命令还是使用单线程。
这样做的目的是因为 redis 的性能瓶颈在于网络 IO 而非 CPU,使用多线程能提升 IO 读写的效率,从而整体提高 redis 的性能。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论