1、系统架构

2、基于session登录

用户的 session 是由服务器(如 Tomcat)自动管理和维护的,每个用户在访问 Web 应用时都会拥有一个独立的 session 对象。这个对象是通过浏览器和服务器之间的 HTTP 协议自动绑定的。
1. 如何区分不同用户的 Session?
每个用户的 session 是通过 Cookie 中的 JSESSIONID 来区分的。
当用户第一次访问服务器时,服务器会创建一个唯一的 HttpSession 对象,并生成一个唯一的标识符 JSESSIONID。
这个 JSESSIONID 会被写入到客户端的 Cookie 中。
后续每次请求中,客户端会将 JSESSIONID 发送到服务器端,服务器根据这个 ID 找到对应的 HttpSession 实例。
示例流程:
- 用户 A 第一次请求 /user/code:
- 服务器创建 HttpSession 实例 A,并分配 JSESSIONID=abc123。
- 将 JSESSIONID=abc123 写入用户 A 的 Cookie。
- 用户 B 第一次请求 /user/code:
- 服务器创建另一个 HttpSession 实例 B,并分配 JSESSIONID=xyz456。
- 将 JSESSIONID=xyz456 写入用户 B 的 Cookie。
- 用户 A 再次请求 /user/code:
- 浏览器自动携带 Cookie 中的 JSESSIONID=abc123。
- 服务器找到对应的 HttpSession 实例 A 并使用它处理请求。
如下登录代码,校验验证码:
通过取出该用户对应session中保存的验证码和用户传递的验证码对比进行校验。其中session中的验证码实在给用户发送短信的同时保存在session中的。
校验通过,登录成功。服务器将用户信息存储在该session中。之后每次用户请求,将会通过cookie辨认对应的session,然后得知该用户已登录。

拦截器实现登录校验

集群session共享问题

Redis解决集群的session问题


一半选择使用hashmap来存储用户信息(因为用户信息往往是一个对象)
登录逻辑

校验逻辑
前端每次请求会在请求头携带token

拦截器的使用

3、缓存
是什么?

缓存作用模型

缓存更新策略

主动更新策略
一般选择方案1。后两个方案都不成熟


先删除缓存还是先操作数据库?
方案二发生概率更低,因为查询数据库之后写入缓存之间间隔往往很小(微秒级别);而更新数据库并更新所需要的时间比较长。因而方案二发生数据不一致的概率比较小。

小结

缓存穿透
布隆过滤器里面就是一些二进制位。数据库中的数据通过哈希算法映射到布隆过滤器中的某个位置上。

方法一:缓存空对象

缓存穿透的解决方案有哪些?

缓存雪崩
如果大量缓存的key的过期时间一样,会导致他们同时失效;解决方案为下图第一种。

缓存击穿

解决方案


互斥锁解决

使用redis的setnx命令实现逻辑上的自定义互斥锁。

为了避免获取锁的进程出现故障而导致锁无法被该进程释放,一般还会为这个锁设置有效期。
redis实现互斥锁

逻辑过期解决
使用这种方法需要进行缓存预热,也就是提前将热点信息加入缓存当中。

redis里面设置逻辑过期字段可以参考以下代码:

封装缓存工具类

java
@Slf4j
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
//普通插入redis
public void set(String key, Object value, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
}
//带逻辑过期时间插入redis
public void setWithLogicalExpire(String key,Object value,Long time,TimeUnit unit){
//设置逻辑过期
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
//写入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
}
//解决缓存穿透的查询(返回空值)
public <R,ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time,TimeUnit unit){
String key=keyPrefix+id;
//1.尝试从Redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
//2.判断缓存是否存在
if(StrUtil.isNotBlank(json)) { //判断字符串既不为null,也不是空字符串(""),且也不是空白字符
//3.存在,返回商铺信息
return JSONUtil.toBean(json, type);
}
//判断是否为空值
if(json!=null){
return null;
}
//4.不存在,根据id查询数据库
R r = dbFallback.apply(id);
//5.判断数据库中是否存在
if(r==null){
//6.不存在,返回错误状态码
stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
return null;
}
//7.存在,写入redis,返回商铺信息
this.set(key,r,time,unit);
return r;
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);
//解决缓存击穿的查询(逻辑过期)
public <R,ID> R queryWithLogicalExpire(
String keyPrefix,ID id,Class<R> type,Function<ID,R> dbFallback,Long time,TimeUnit unit){
String key=keyPrefix+id;
//1.尝试从Redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
//2.判断缓存是否存在
if(StrUtil.isBlank(json)) { //判断字符串既不为null,也不是空字符串(""),且也不是空白字符
//3.不存在,返回商铺信息
return null;
}
//4.存在,将json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R shop = JSONUtil.toBean((JSONObject) redisData.getData(),type);
LocalDateTime expireTime = redisData.getExpireTime();
//5.判断是否过期
if(expireTime.isAfter(LocalDateTime.now())) {
//5.1.未过期,直接返回店铺信息
return shop;
}
//5.2.已过期,需要返回缓存重建
//6.缓存重建
//6.1.获取互斥锁
String lockKey=RedisConstants.LOCK_SHOP_KEY+id;
boolean isLock = tryLock(lockKey);
//6.2.判断是否获取锁成功
if(isLock){
// 6.3.成功,开启独立线程实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
//查询数据库
R r1= dbFallback.apply(id);
//写入redis
this.setWithLogicalExpire(key,r1,time,unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
//释放锁
unLock(key);
}
});
}
//6.4.返回过期的商铺信息
return shop;
}
/**
* 创建锁
* @param key
* @return
*/
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
/**
* 封闭锁
* @param key
*/
private void unLock(String key){
stringRedisTemplate.delete(key);
}
}
4、JMeter-模拟高并发的工具


5、全局唯一ID

要求递增性是为了数据库查询的高效。

6、秒杀优惠券
业务流程

超卖问题

解决办法
其中使用悲观锁解决会导致性能严重下降。

乐观锁-版本号法

由于stock肯定是递减的,因此将它作为"版本号"不会导致ABA问题的发生。就有如下解法:

一人一单业务

7、集群模式下的秒杀-分布式锁
动机

分布式锁



redis实现分布式锁
使用set命令同时设置互斥的(NX)key和它的过期时间

误删别人锁的问题

解决办法
key对应value标识当前获取锁的对象



Lua脚本解决多条命令原子性问题
极端情况
假如某线程在刚刚判断完当前锁是不是自己的锁之后,线程发生堵塞,则该线程在被唤醒之后还是有可能误删其他线程的锁。
->要保证判断锁和释放锁这两个动作共同形成一个原子操作。

Redis的Lua脚本




8、Redisson

事实上分布式锁无需自己实现,会使用框架即可,如Redisson

快速入门


Redisson可重入锁的原理
获取锁

释放锁

整体流程

Resson的multiLock


9、redis优化秒杀

redis判断秒杀资格
优化后,下单的核心流程仅仅是以下:可见变得非常短且执行速度会非常快

异步下单-阻塞队列
10、异步下单-消息队列

redis消息队列
redis消息队列-list

下图缺点"无法避免消息丢失",因为有可能刚刚pop出来还没处理就宕机了。

redis消息队列-PubSub

"不支持数据持久化"------发布者发布消息的瞬间如果没有任何订阅者,这条消息就会消失。
"消息堆积有上限"------订阅者有一个消息缓存区,存放收到了但是还没来得及处理的消息。

redis消息队列-Stream
写消息

读消息


"消息可回溯"------消息永远会保存在队列中

消费者组-基于stream


一个消费者组里面,消息被任意一个消费者读取过就算被消费了。



小结对比

11、点赞功能
以博客id为key,点赞用户的set集合为value存储。

点赞排行榜
显示最新点赞的n个用户。使用SortedSet,score值设置为时间戳。

12、共同关注
以某个用户为key,该用户关注的所有用户的set为value,存到redis。
共同关注取交集即可。
但是关注信息在mysql数据库中也有一个表维护。(树树:不然怎么查看粉丝数......)
13、关注推送-feed流


拉模式

只有赵六准备读取的时候才从他关注的up主的发件箱读取消息------延时高
推模式
up主不保存信息,他一旦发布就将信息保存在所有粉丝的收信箱------保存多个副本,如有大v有几千万粉丝,则内存消耗巨大

推拉结合


新博客推送给粉丝-推模式
用户发送博客,查询粉丝列表,将新博客id存到所有粉丝的收件箱。
滚动分页
【豆包】
滚动分页(Scroll Pagination)是一种基于用户滚动行为的分页加载方式,常见于移动端应用、网页信息流等场景,核心特点是当用户滚动到页面底部时,自动加载下一页数据,无需手动点击页码或 "加载更多" 按钮,从而实现 "无限滚动" 的连续浏览体验。
与传统分页的区别:
传统分页(如页码分页)需要用户通过点击 "上一页 / 下一页" 或直接输入页码跳转,每次加载固定页数的数据(如第 1 页 10 条、第 2 页 10 条),依赖 "页码" 作为分页标识。
而滚动分页不依赖页码,而是通过记录 "上一页最后一条数据的标记"(如 ID、时间戳、游标等)作为下一页的查询起点,动态加载后续内容。例如:
- 首次加载第 1-10 条数据,记录第 10 条的 ID 为
lastId=10
;- 用户滚动到底部时,自动请求
lastId=10
之后的 11-20 条数据,再记录第 20 条的 ID 为lastId=20
,以此类推。
传统分页的弊端
如果在查询的同时有人插入了数据,就会发生重复读取情况。

使用滚动分页,每次记住上一次查询到哪一条数据,下次查询从这条数据往后查。

实现方法:
每次从用户邮箱读取的时候,记住max,并计算出offset,传递给前端。前端下次查询会携带这两个数据。


优化点?
b站弹幕:我觉得可以用户在线推到redis,不在线就不推了,用户在线下拉刷新是去读redis,向上滑读mysql
树树:这么做是因为会导致redis内存压力太大?我感觉可以这样:用户上线的时候/刷新的时候将数据库所有关注的人的笔记加载到redis(当然还要设置一个过期时间);然后后面再......
14、附近商铺
GEO-地理坐标

添加坐标点

计算两个点之间的距离

计算一个圆内所有点,圆心是给定的

上面这些命令中,g1是key,代表一个存放了若干个地理坐标点的数据集。
实现

不同类型的商铺分开存。redis存的只有商户id和对应经纬度,而没有全部具体的商铺信息。

15、用户签到-BitMap


实现


位运算遍历每个bit位。
16、UV统计
UV、PV是什么?

HyperLogLog

重复插入的元素只记录一次
