回顾Redis 的使用
Redis 是一种 key-value 的 nosql 数据库,基于内存
使用场景:
- 缓存
- 分布式锁
- 分布式 session
- 消息队列
- 计数器
- 社交网络
- 排行榜
缓存使用过程
一、Redis 的并发问题
问题 | 说明 | 解决方法 |
雪崩 | 1.大量热点key同时失效,导致大量请求直接访问数据库,导致数据库宕机 2. Redis服务器重启或出现问题 | 1. 将热点的key过期时间设置随机的 2. 搭建Redis集群 |
击穿 | 大量线程并发访问,没等前面线程访问后存到Redis,其它线程就直接访问数据库 | 使用双检锁机制,保存读取缓存,读取数据和保存缓存原子执行 |
穿透 | 大量线程查询数据库中没有的数据,Redis不能保存,数据库压力过大 | 1. 在Redis保存空对象,设置过期时间 2. 使用布隆过滤器排除数据库不存在的数据 |
解决 Redis 并发代码
@Service
public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> implements StudentService {
// 缓存中学生信息的key前缀
public static final String PREFIX = "Student:";
@Autowired
private StudentMapper studentMapper;
// Redis操作魔板,用于操作缓存
// RedisTemplate 它封装了 Redis 连接的创建、资源管理、以及对 Redis 数据结构的操作
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// Redis布隆过滤器,用于判断数据是否存在于缓存
@Autowired
private RBloomFilter bloomFilter;
@Override
public Student getStudentById(Long id) {
ValueOperations<String, Object> ops = redisTemplate.opsForValue();
Student student = (Student) ops.get(PREFIX + id);
if (student == null) {
synchronized (this) {
student = (Student) ops.get(PREFIX + id);
if (student == null) {
System.err.println("Redis为空,访问数据库:" + Thread.currentThread().getName());
Student selectById = studentMapper.selectById(id);
if (selectById != null) {
System.err.println("数据库不为空,保存到数据库:" + Thread.currentThread().getName());
ops.set(PREFIX + id, selectById);
return selectById;
} else {
System.err.println("数据库为空,返回空值:" + Thread.currentThread().getName());
ops.set(PREFIX + id, new Student(), 10, TimeUnit.SECONDS);
}
return null;
}
}
}
System.err.println("Redis存在就直接访问数据:" + Thread.currentThread().getName());
return student;
}
}
二、布隆过滤器
布隆过滤器:一系列二进制数列和哈希函数组成
是很长的数组(向量),每个元素就是0或1。
用于判断某个数据是否存在于某个集合中,向集合添加数据后,会将数据进行一系列哈希运算,将计算出的位置上的值设置1。
进行对象判断时,重复进行哈希运算,判断该位置上是否都是1。
存在的问题:能准确判断数据在集合中是否不存在,判断数据存在一定的误差(向量越长,哈希运算越多,误差率越低,内存消耗越大)。
实现步骤:
1) 使用Redis实现布隆过滤器(Redisson)
2) 将数据库中所有数据的id保存到布隆过滤器中
3) 数据库查询之前,使用布隆过滤器排除掉不存在的id
1)Redisson 依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.0</version>
</dependency>
2)配置类返回布隆过滤器
@Configuration
public class RedissonConfig {
@Bean
//创建并配置Redisson布隆过滤器的 Bean RBloomFilter 是Redisson框架中提供的布隆过滤器的接口
public RBloomFilter<String> bloomFilter() {
//创建Redisson配置对象
Config config = new Config();
// 设置IO模式 NIO表示非阻塞IO
config.setTransportMode(TransportMode.NIO);
// 设置单服务器配置
SingleServerConfig singleServerConfig = config.useSingleServer();
// 配置 Redis 服务器地址,可以有“rediss://”来启用SSL连接
singleServerConfig.setAddress("redis://127.0.0.1:6379");
// 创建redisson客户端
RedissonClient redisson = Redisson.create(config);
// 获取或创建名为"student-filter" 的布隆过滤器
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("student-filter");
// 初始化布隆过滤器,参数1为向量长度,参数2为错误率
bloomFilter.tryInit(10000000L, 0.03);
// 返回配置好的布隆过滤器 Bean
return bloomFilter;
}
}
3)将存在的数据存入布隆过滤器
@RestController
public class StudentController {
@Autowired
private StudentService studentService;
@Autowired
private RBloomFilter<String> bloomFilter;
@GetMapping("init-bloomfilter")
// 初始化布隆过滤器
public ResponseResult<String> initBloomFilter() {
// 查询所有学生的ID列表
List<Student> list = studentService.list();
// 将学生ID添加到布隆过滤器中
list.forEach(student -> {
bloomFilter.add(StudentServiceImpl.PREFIX + student.getId());
});
// 返回初始化布隆过滤器操作的响应结果
return ResponseResult.ok("ok");
}
}
4)查询数据库之前,用布隆过滤器过滤掉不存在的key
@Service
public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> implements StudentService {
// 缓存中学生信息的key前缀
public static final String PREFIX = "Student:";
@Autowired
private StudentMapper studentMapper;
// Redis操作魔板,用于操作缓存
// RedisTemplate 它封装了 Redis 连接的创建、资源管理、以及对 Redis 数据结构的操作
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// Redis布隆过滤器,用于判断数据是否存在于缓存
@Autowired
private RBloomFilter bloomFilter;
@Override
public Student getStudentById(Long id) {
// 获取操作字符串类型数据的操作对象
ValueOperations<String, Object> ops = redisTemplate.opsForValue();
// 尝试从Redis缓存中获取指定ID对应的学生信息
Student student = (Student) ops.get(PREFIX + id);
// 使用DCL双检锁提升性能
if (student == null) {
synchronized (this) {
// 再次查询,防止多个线程同时通过第一次判断
student = (Student) ops.get(PREFIX + id);
if (student == null) {
System.err.println("Redis查询Student为空,现在访问数据库:" + Thread.currentThread().getName());
// 使用布隆过滤器判断数据库没有的id
if (!bloomFilter.contains(PREFIX + id)) {
System.err.println("数据库为空,返回空值 " + Thread.currentThread().getName());
return null;
}
// 从数据库查询ID对应的学生信息
Student selectById = studentMapper.selectById(id);
//查询数据库不为空,就保存到redis中
if (selectById != null) {
System.err.println("数据库不为空,保存到数据库:" + Thread.currentThread().getName());
ops.set(PREFIX + id, selectById);
return selectById;
}
return null;
}
}
}
System.err.println("redis里面的Student存在,直接返回数据" + Thread.currentThread().getName());
return student;
}
}
三、Redis 的集群
Redis 集群提高 Redis 的可以性,不会因为单点故障导致 Redis 不可用
Redis 集群的基础:主从架构
一台 Redis 服务器做主机(Master),两台 Redis 做从机(Slave)
读写分离:主机负责写,从机负责读
主从复制:主机数据修改,会将数据同步到所有的从机上
为什么最少上台机器?
涉及 Master 选举,必须达到半数以上
常见集群方案:
1)哨兵架构
由多个主从架构和哨兵机器组成,哨兵监视主从架构运行的情况,在主机出现问题时,让从机成为主机,保证集群的正常运行
优势:可用性高
缺点:哨兵资源有一定浪费
2)集群架构
由 N 台 Redis 服务器组成,把所有的机器分为 16384 个槽 (slot),每个 key 保存时进行哈希运算,存入槽中,每个槽再路由到集群中的每一台机器。
优势:资源利用和可用性都比较高
搭建伪集群
创建集群出现问题:
1) xxx not empty ....
停掉每个Redis服务
到每一个redis01~redis06/src 删除 appendonly.aof 和 dump.rdb
再启动start.sh
2) slot not cover .. 槽没有覆盖完整
修复集群
redis-cli --cluster fix IP地址:端口
四 、Redis 和数据库的同步问题
几种同步方式:
1)SpringCache 申明式注解
@CachePut @CachEvit
代码侵入性比较低
2)Canal 主键监听 Mysql 的修改,进行同步
3)手动完成同步,延迟双删
一般情况下有两种方法:
1、先删缓存,再更新数据库
问题:
2、选更新数据库,在删缓存
问题:
延迟双删解决上面存在的问题,更新数据库执行删除一次,更新数据库,更新后再删除一次缓存。
延迟多久?
一般不会超过 2s,看网络和业务复杂情况