目录:
1.使用redis缓存来实现登录
2.使用redis缓存某些经常访问的信息
3.缓存问题
4.秒杀实现
5.点赞功能
6.关注功能
7.关注功能-消息推送
8.附近的人功能
9.签到功能
10.redis集群
11.哨兵机制
12.多级缓存
本篇文章仅是个人使用redis的理解,简单总结使用思路,并没有给出具体的实现步骤
登录两大步:
- 登录验证
- 请求验证
缓存穿透问题
缓存穿透问题其实就是前端访问数据从缓存中和数据库中都没有查到该数据,这样的话,如果大量请求该数据,就会使数据库压力增大,甚至宕机。
方法一:空值法
- 如果没有该数据,就往缓存中写入一个空值,并且返回错误信息
方法二:布隆过滤器
- 通过算法,如果访问没有的数据,直接将请求拦截在缓存之前
缓存雪崩问题
缓存雪崩是指大量的key过期或者redis宕机,造成所有的请求都到达了数据库,造成数据库压力增大,甚至宕机
方法一:
- 给key设置不同的过期时间
方法二:
- 采用集群,提高redis的高可用性
方法三:
- 给业务进行降级限流
方法四:
- 给业务添加多级缓存
缓存击穿问题
缓存击穿就是某个高并发且重载业务复杂的key过期,在重载的时候,很多的请求进入,直接到达数据库,数据库压力增大,甚至宕机
解决的办法就是在重载的时候只允许一个请求进入,其他请求等待。
方法一:互斥锁
- 在进行重载之前获取锁,获取倒锁的才能进入,否则失败
- 悲观锁和乐观锁
- 悲观锁就是认为随时都会发生读取到的和之前数据不一样,所以都上锁
- 乐观锁认为不会发生错读,用一个值来标识是否修改过表,在读取途中如果这个值改变了
该次读取就失败。
方法二:逻辑过期
- 在redis中维护数据设置一个单独的键 expire 作为逻辑过期时间
- 这个数据永远都不会过期,在重载数据的时候,获取锁,获取到锁的对象就进行重载
- 没有获取到锁的对象就直接返回原来的数据。
问题: 但是这个锁,只能在单个tomcat生效,如果多个tomcat的话,各自有自己的锁,这样话,就无法保证锁只能被一个人获取
采用分布式锁
采用redis的setnx实现,setnx只允许存储一次
- 在redis中维护一把锁,这把锁以随机uuid + 线程id 值
- 释放锁就直接将键删除
问题:但是如果获取到了锁,服务宕机了,这样的话锁无法释放,以及在删除锁的时候可能出现误删的情况
1. 给锁设置一个过期时间,这样的话,即使redis宕机了,也可以得到释放
2. 为了防止误删,将根据uuid和线程id 在删除锁之前进行判断,如果成立就删除
3. 如果不成立,证明锁已经被删除了。
对于锁,redis提供了一套完整框架来获取锁和释放锁,并且实现了可重入锁
引入依赖
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.16.8version>
dependency>
使用
// 获取锁
RLock lock = redissonClient.getLock("order");
boolean isLock = lock.tryLock();
// 释放锁
lock.unlock();
1. 请求进来,根据id获取对应的优惠券,如果优惠券不存在,返回优惠券不存在
2. 判断库存是否足够,如果不够就返回库存不足
3. 如果库存充足,就直接扣减
4. 生成订单存储数据库
问题:上述对于优惠券的抢购,在高并发的情况下,就存在超卖的情况
1. 在每个请求进来的时候,根据id获取优惠券
2. 根据优惠使用redisson获取锁,没有获取到锁的,直接返回不能 重复下单
3. 获取到了锁之后,就生成订单,存储到数据库中
一个用户一个订单
在原来高并发情况解决超卖问题的基础上,因为优惠券一般都是一个人只能抢购一张
思路
1. 在生成订单之前,判断数据库中的订单列表是否已经存在该订单如果存在直接返回 不能重复下单
2. 如果不存在在生成订单存入数据库中-
问题: 虽然已经实现了高并发超卖问题并且完成一人一单问题,但是高并发状况下,仍然是在查询数据库,数据库压力大
1. 在redis中维护一个优惠券数量和一个已经购买过集合(set数据类型)的信息
2. 在添加优惠券的时候,就往redis中写入一份
3. 在请求到达时候,在获取库存和是否下单时候,只需要查询redis数据库
4. 生成订单信息,写入数据库
提升:这里从redis中获取数据和判断操作,属于redis操作,可以将其写入lua脚本,保证原子性,同时写入数据库的操作很费时间,我们可以订单信息存入到一个阻塞队列中,在开启一个线程通过while(true)来监听队列是否有订单信息,这样的话就实现了异步处理订单写入数据库,大大提升速度。
1. 编写lua脚本
2. 执行lua脚本
3. 生成订单存入阻塞队列
4. 异步写入数据库
上述还可以有改进的地方,阻塞队列是存储在jvm机之中,如果服务宕机了,就失去了订单
1. 采用redis中的stream队列,结合stream组读取操作
2. 在redis中维护一个 stream队列
3. 在判断库存和单个订单的同时,将订单数据写入到redis里面
4. 开启一个线程来监听当前的队列,如果队列中有数据,就进行读取。
5. 因为这里有pending-list的存在,读取一个数据,都会进入pending-list中,只有确定完成后,才能从pending-list中一处,如果在写入的时候出现了问题
6. 我们就读取pending-list集合,如果有数据,就进行写入。没有就直接退出读取pending-list
点赞功能的实现,主要是要实现一个人只能点一次赞,如果第二次点赞就是取消点赞
思路:
1. 在redis中维护一个set集合
2. 当有用户点赞的时候,就将该用户的id存入的set集合中,并且修改数据库中的点赞值
3. 当同一个用户再次点赞的时候,就删除set集合中用户,并且减少数据库中的点赞数
4. 同时还需要一个值,来反应是否已经点赞,交给前端的,来处理是否已经点赞
5. 所以我们需要在每次查询所有blog的时候,将查询redis中的set集合,判断当前用户是否已经点赞该blog
新问题: 如果需要有一个点赞排行榜,记录所有的点赞人的先后顺序,这个时候set集合就无法满足条件了
1. 将set集合换成sortedset集合,拥有排序功能
2. 在点赞存入用户的时候,将当前时间作为分数存入到sortedset中
3. 当需要获取点赞排行榜的时候,只需要获取分数排名靠前几个人的值取查询用户数据即可
一个用户关注另一个用户,这样的需求很常见,我们创建一张表来存储这些关注信息
采用redis集合中的set集合来完成共同关注的功能
1. 关注请求到达,带着我想要关注的用户id,我作为关注者,它作为被关注者
2. 生成关注者对象,将我的id 和 被关注的id传入
3. 为了实现共同关注功能,我们可以将信息,被关注者为键,将关注id存入到set集合中
4. 当需要获得共同关注者的时候,直接使用redis的set集合中的关注者做交集获得信息即可
消息推送方式分为三种:拉消息,推消息,推拉结合
拉消息:每个关注者,只存储了关注的信息,需要查看的时候,跟关注者信息拉去他发送的博客放到自己的信息箱中,这就称为拉消息,反应速度慢。
推消息:在被关注者发送信息的同时,将博客直接推送到关注者信息箱里面,这种方式会造成如果关注者不读取信息就浪费了内存
推拉结合:推拉结合是根据用户的活跃程度来实现的,如果用户是非常活跃的用户,就采用推方式,这样可以提高用户体验感,但是对于长期不查看信息的关注者,我们采用拉方式,他要查看的时候就拉去信息到信息箱中。
采用推消息实现
1. 在redis中为每个关注者维护一个信箱
2. 在被关注者发送信息的同时,查询所有的关注者,并且把信息推送到redis中的对应信箱中,但一般都只是推送一些id方便查询
3. 当用户查看信息的时候,就可以读取信箱中的信息
问题: 查看的方式问题,对于查看的人来说,希望的是每次查看到的是最新发送的信息
两种方式实现:
第一种采用下标来实现分页查询,但是这样的方式,如果有新的数据到达,就会造成查询的数据混乱
第二种采用滚动方式查询,每次查询都传入上次查询的最后位置的下标,再下一次查询的时候,就从比这个下标的下一个位置开始查询,这样的话就可以实现滚动查询,
1. 采用sortset作为信箱存储信息
2. 在读取的时候,使用rangewtihsocre(key,min,max,offset,count) 方式来查询
3. 同时将max和offset返回,方便下次读取
附近的人可以采用redis中的位图来实现,位图可以存储坐标信息
1. 将坐标信息存入到redis中
locations.add(new RedisGeoCommands.GeoLocation<>(
shop.getId().toString(),
new Point(shop.getX(),shop.getY())
));
2. 在用户需要通过距离获取附近商铺的时候,拉去redis的数据
GeoResults> results = stringRedisTemplate.opsForGeo()
.search(key,
GeoReference.fromCoordinate(x, y),
new Distance(5000),
RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end));
3. 将数据封装成对象,返回给前端
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);
连续签到天数算法,每次让最后一位 和 1 做 与操作,得到的仍是它本身,只要是0 就退出
int count = 0;
while (true){
if ((bit & 1) == 0){
break;
}else{
count ++;
}
bit >>>= 1;
}
redis为了实现高可用性,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的时候
哨兵机制就是用来监控redis集群的健康状态,在redis节点发生故障的时候,修复,并且同个各个redis节点
1. 修改配置文件中的端口号
2. 告知需要监控的主节点信息,例ip地址、端口号、密码
3. 启动所有哨兵
Sentinel的三个作用
1. 监控
2. 故障转移
3. 通知
Sentinel 如何判断一个redis实例是否健康
- 每隔一秒钟发送一次ping命令,如果超过一定时间没有响应就认为主观下线
- 如果大多数Sentinel都认为某个节点主观下线了,那么就判定该节点服务下线
故障转移步骤有哪些?
1. 首先选定一个salve作为新的master节点,执行slaveof no one ,该节点就成为新的主节点
2. 然后向所有节点发送执行slaveof 新的master
3. 修改故障节点的配置,添加slaveof 新master,等它上线,就成为了新主节点的子节点
在Java中使用RedisTemplate来操作redis,如果是单个只需要配置ip地址、端口号、密码等,就可以使用了,但是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);
}
从请求发送可以实现多级缓存
- 最前面的是基于浏览器的缓存,在浏览器中存放经常需要加载的数据
- 可以使用nginx反向代理,负载均衡给多个nginx,再在nginx中采用lua语言来读取redis集群实现缓存
- 然后到达tomcat,在tomcat也可以实现集群提高可用性,这里可以做Jvm缓存
- 如果前面的缓存中都没有,最终才到达数据库。