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里面設置邏輯過期字段可以參考以下代碼:
封裝緩存工具類
@Slf4j
@Component
public class CacheClient {private final StringRedisTemplate stringRedisTemplate;public CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}//普通插入redispublic void set(String key, Object value, Long time, TimeUnit unit){stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);}//帶邏輯過期時間插入redispublic 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)));//寫入redisstringRedisTemplate.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);//寫入redisthis.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
重復插入的元素只記錄一次