redis缓存常见使用和问题分析
目录:
1.使用redis缓存来实现登录
2.使用redis缓存某些经常访问的信息
3.缓存问题
4.秒杀实现
5.点赞功能
6.关注功能
7.关注功能-消息推送
8.附近的人功能
9.签到功能
10.redis集群
11.哨兵机制
12.多级缓存
本篇文章仅是个人使用redis的理解,简单总结使用思路,并没有给出具体的实现步骤
1. 使用redis缓存来实现登录
- 首相我们通过发送验证码的方式,来获取验证码
- 然后进行登录验证,登录验证的时候,从数据库中查询信息
- 如果存在用户,就获取用户信息和生成token,并且把信息和token封装,以token作为键存入redis,并且返回给前端
a. 以map形式存入,能够更方便获取值 - 为了能够使当前线程内的任何类都可以访问到这个user对象,采用ThreadLocal类来存储
- 有了token,就需要每次访问的时候,对token进行验证,这个验证就放在前端拦截器之中
a. 一个拦截器拦截所有请求,对token进行判断,如果有token,就可以根据token拿到用户信息存入到threadLocal中
b. 第二个拦截器,只需要拦截需要验证登录的请求,只需要判断threadLocal中是否有user对象即可 - 完成完整的登录流程
2. 使用redis缓存某些经常访问的信息
- 首先发来请求,先从redis中获取信息,如果redis中没有该商品信息
- 从数据库中获取,将获取到的数据返回给前端,再将数据缓存到redis中,这样下次访问就是访问redis中的了
3. 缓存问题
缓存穿透问题
- 解决办法
方法一:空值法
- 如果没有该数据,就往缓存中写入一个空值,并且返回错误信息
方法二:布隆过滤器
- 通过算法,如果访问没有的数据,直接将请求拦截在缓存之前
缓存雪崩问题
- 解决办法
方法一:
- 给key设置不同的过期时间
方法二:
- 采用集群,提高redis的高可用性
方法三:
- 给业务进行降级限流
方法四:
- 给业务添加多级缓存
缓存击穿问题
- 解决方法:
解决的办法就是在重载的时候只允许一个请求进入,其他请求等待。
方法一:互斥锁
- 在进行重载之前获取锁,获取倒锁的才能进入,否则失败
- 悲观锁和乐观锁
- 悲观锁就是认为随时都会发生读取到的和之前数据不一样,所以都上锁
- 乐观锁认为不会发生错读,用一个值来标识是否修改过表,在读取途中如果这个值改变了
该次读取就失败。
方法二:逻辑过期
- 在redis中维护数据设置一个单独的键 expire 作为逻辑过期时间
- 这个数据永远都不会过期,在重载数据的时候,获取锁,获取到锁的对象就进行重载
- 没有获取到锁的对象就直接返回原来的数据。
- 解决办法:
采用分布式锁
采用redis的setnx实现,setnx只允许存储一次
- 在redis中维护一把锁,这把锁以随机uuid + 线程id 值
- 释放锁就直接将键删除
- 解决办法:
1. 给锁设置一个过期时间,这样的话,即使redis宕机了,也可以得到释放
2. 为了防止误删,将根据uuid和线程id 在删除锁之前进行判断,如果成立就删除
3. 如果不成立,证明锁已经被删除了。
- 采用redisson 实现
引入依赖
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.8</version>
</dependency>
使用
// 获取锁
RLock lock = redissonClient.getLock("order");
boolean isLock = lock.tryLock();
// 释放锁
lock.unlock();
4. 秒杀实现
1. 请求进来,根据id获取对应的优惠券,如果优惠券不存在,返回优惠券不存在
2. 判断库存是否足够,如果不够就返回库存不足
3. 如果库存充足,就直接扣减
4. 生成订单存储数据库
- 解决办法:
1. 在每个请求进来的时候,根据id获取优惠券
2. 根据优惠使用redisson获取锁,没有获取到锁的,直接返回不能 重复下单
3. 获取到了锁之后,就生成订单,存储到数据库中
一个用户一个订单
1. 在生成订单之前,判断数据库中的订单列表是否已经存在该订单如果存在直接返回 不能重复下单
2. 如果不存在在生成订单存入数据库中-
- 解决办法
1. 在redis中维护一个优惠券数量和一个已经购买过集合(set数据类型)的信息
2. 在添加优惠券的时候,就往redis中写入一份
3. 在请求到达时候,在获取库存和是否下单时候,只需要查询redis数据库
4. 生成订单信息,写入数据库
1. 编写lua脚本
2. 执行lua脚本
3. 生成订单存入阻塞队列
4. 异步写入数据库
- 解决办法
1. 采用redis中的stream队列,结合stream组读取操作
2. 在redis中维护一个 stream队列
3. 在判断库存和单个订单的同时,将订单数据写入到redis里面
4. 开启一个线程来监听当前的队列,如果队列中有数据,就进行读取。
5. 因为这里有pending-list的存在,读取一个数据,都会进入pending-list中,只有确定完成后,才能从pending-list中一处,如果在写入的时候出现了问题
6. 我们就读取pending-list集合,如果有数据,就进行写入。没有就直接退出读取pending-list
5. 点赞功能
思路:
1. 在redis中维护一个set集合
2. 当有用户点赞的时候,就将该用户的id存入的set集合中,并且修改数据库中的点赞值
3. 当同一个用户再次点赞的时候,就删除set集合中用户,并且减少数据库中的点赞数
4. 同时还需要一个值,来反应是否已经点赞,交给前端的,来处理是否已经点赞
5. 所以我们需要在每次查询所有blog的时候,将查询redis中的set集合,判断当前用户是否已经点赞该blog
1. 将set集合换成sortedset集合,拥有排序功能
2. 在点赞存入用户的时候,将当前时间作为分数存入到sortedset中
3. 当需要获取点赞排行榜的时候,只需要获取分数排名靠前几个人的值取查询用户数据即可
6. 关注功能
1. 关注请求到达,带着我想要关注的用户id,我作为关注者,它作为被关注者
2. 生成关注者对象,将我的id 和 被关注的id传入
3. 为了实现共同关注功能,我们可以将信息,被关注者为键,将关注id存入到set集合中
4. 当需要获得共同关注者的时候,直接使用redis的set集合中的关注者做交集获得信息即可
7. 关注功能-消息推送
采用推消息实现
1. 在redis中为每个关注者维护一个信箱
2. 在被关注者发送信息的同时,查询所有的关注者,并且把信息推送到redis中的对应信箱中,但一般都只是推送一些id方便查询
3. 当用户查看信息的时候,就可以读取信箱中的信息
问题: 查看的方式问题,对于查看的人来说,希望的是每次查看到的是最新发送的信息
两种方式实现:
-
第一种采用下标来实现分页查询,但是这样的方式,如果有新的数据到达,就会造成查询的数据混乱
-
第二种采用滚动方式查询,每次查询都传入上次查询的最后位置的下标,再下一次查询的时候,就从比这个下标的下一个位置开始查询,这样的话就可以实现滚动查询,
1. 采用sortset作为信箱存储信息
2. 在读取的时候,使用rangewtihsocre(key,min,max,offset,count) 方式来查询
3. 同时将max和offset返回,方便下次读取
8. 附近的人功能
1. 将坐标信息存入到redis中
locations.add(new RedisGeoCommands.GeoLocation<>(
shop.getId().toString(),
new Point(shop.getX(),shop.getY())
));
2. 在用户需要通过距离获取附近商铺的时候,拉去redis的数据
GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
.search(key,
GeoReference.fromCoordinate(x, y),
new Distance(5000),
RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end));
3. 将数据封装成对象,返回给前端
9. 签到功能
1. 在redis中,根据当前用户id 和 当前年月 作为键, 以0-31bit位为作为值,来存储签到次数
2. 从redis中拉去当前月的签到信息
3. 通过解析bit位来统计签到次数以及连续签到次数
具体代码实现:
// 存储键值
stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);
// key 键
// dayOfMonth-1 偏移量,也就是今天
// 获取键值
List<Long> keys = stringRedisTemplate.opsForValue().bitField(
key,
BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0)
);
// bitfield 可以进行多个操作,每个操作的结果存入到集合中
// 3. 解析键值
Long bit = keys.get(0);
int count = 0;
while (true){
if ((bit & 1) == 0){
break;
}else{
count ++;
}
bit >>>= 1;
}
10. redis集群
搭建集群
1. 修改配置文件中端口号,rdb的保存位置
2. 启动所有的redis
3. 在从节点中执行 slaveof ip地址 端口号
主从同步
- 全量同步
1. slave 节点请求全量同步
2. master节点判断replid 发现不一致,拒绝增量同步
3. master节点将完整的内存数据生成rdb文件,发送rdb给slave节点
4. slave清空本地数据,加载master的rdb文件
5. master将rdb期间的命令记录在repl_baklog,并持续将log中的命令发送给slave
6. slave执行接受到的命令,保持与master之间的同步
- 全量同步:master将完整的数据生成rdb文件,发送rdb文件给slave节点,并且将期间的写操作记录在repl_baklog中,逐个的将命令发送给slave节点。
- 增量同步:slave提交自己的offset到master,master从repl_baklog中offset位置开始,将后续命令发送给slave
- slave节点第一次链接master时候
- slave节点断开太久,repl_baklog中的offset已经被覆盖时
- slave节点断开恢复,并且从repl_baklog中能找到offset的时候
11. 哨兵机制
搭建哨兵集群
1. 修改配置文件中的端口号
2. 告知需要监控的主节点信息,例ip地址、端口号、密码
3. 启动所有哨兵
1. 监控
2. 故障转移
3. 通知
- 每隔一秒钟发送一次ping命令,如果超过一定时间没有响应就认为主观下线
- 如果大多数Sentinel都认为某个节点主观下线了,那么就判定该节点服务下线
1. 首先选定一个salve作为新的master节点,执行slaveof no one ,该节点就成为新的主节点
2. 然后向所有节点发送执行slaveof 新的master
3. 修改故障节点的配置,添加slaveof 新master,等它上线,就成为了新主节点的子节点
RedisTemplate使用redis集群
Sentinel配置
spring:
redis:
sentinel:
master: mymaster
nodes:
- 127.0.0.1:27001
- 127.0.0.1:27002
- 127.0.0.1:27003
读写分离配置
@Bean
public LettuceClientConfigurationBuilderCustomizer configurationBuilderCustomizer(){
return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}
12. 多级缓存
从请求发送可以实现多级缓存
- 最前面的是基于浏览器的缓存,在浏览器中存放经常需要加载的数据
- 可以使用nginx反向代理,负载均衡给多个nginx,再在nginx中采用lua语言来读取redis集群实现缓存
- 然后到达tomcat,在tomcat也可以实现集群提高可用性,这里可以做Jvm缓存
- 如果前面的缓存中都没有,最终才到达数据库。