柚子快報邀請碼778899分享:面試項目準(zhǔn)備:黑馬點評項目總結(jié)
面試項目準(zhǔn)備:黑馬點評項目總結(jié)
1. 項目介紹1.1 項目使用的技術(shù)棧1.2 項目架構(gòu)Nginx作用:
2. 各個功能模塊2.1 登錄模塊短信登錄功能(基于session)基于redis的短信登錄
2.2 用戶查詢緩存模塊2.3 優(yōu)惠券秒殺功能2.4 好友關(guān)注功能
3. 總結(jié)
1. 項目介紹
??黑馬點評項目是一個前后端分離項目,類似于大眾點評,實現(xiàn)了發(fā)布查看商家,達人探店,點贊,關(guān)注等功能,業(yè)務(wù)可以幫助商家引流,增加曝光度,也可以為用戶提供查看提供附近消費場所,主要。用來配合學(xué)習(xí)Redis的知識。 ??基于 Redis + Springboot的點評APP ,實現(xiàn)了短信驗證碼登錄、查找店鋪、秒殺優(yōu)惠券、發(fā)表點評、關(guān)注推送的完 整業(yè)務(wù)流程。
1.1 項目使用的技術(shù)棧
??SpringBoot+Mysql+Lombok+MyBatis-Plus+Hutool+Redis
1.2 項目架構(gòu)
后端部署在Tomcat上,前端部署在Nginx。
Nginx作用:
1. 反向代理Tomcat服務(wù)器,解決多臺服務(wù)器,session不共享問題,隱藏真實服務(wù)地址。 2. 負載均衡降低服務(wù)器壓力。三種負載均衡方式:輪詢法(默認方法)、weight權(quán)重模式(加權(quán)輪詢)、ip_hash Nginx的靜態(tài)處理能力很強,但是動態(tài)處理能力不足,因此,在企業(yè)中常用動靜分離技術(shù)。
2. 各個功能模塊
2.1 登錄模塊
短信登錄功能(基于session)
發(fā)送驗證碼 校驗手機號、判斷格式是否正確、正確生成驗證碼、發(fā)送驗證碼。校驗手機號和驗證碼 校驗手機號、校驗驗證碼、查找用戶、如果沒有創(chuàng)建用戶,保存用戶到session。
以上完成的兩步把用戶信息保存到session中了。然而有許多頁面都需要用戶信息和校驗登錄狀態(tài)。
校驗登錄狀態(tài) ?訪問不通的后端控制器,要獲取數(shù)據(jù)之前需要校驗登錄狀態(tài),用攔截器實現(xiàn)最好,減少代碼冗余。 ?攔截器實現(xiàn),訪問之前從session中獲取用戶,如果用戶存在放行,并且把用戶保存到ThreadLocal中去,不同的線程互不干擾。訪問之后,把ThreadLocal保存的信息刪除。 ?配置攔截器生效,選擇要攔截的請求或是排除不攔截的。
基于redis的短信登錄
?session共享問題:多臺Tomcat并不共享session存儲空間,當(dāng)請求切換到不同服務(wù)器會導(dǎo)致數(shù)據(jù)丟失問題。(可以用Tomcat間的數(shù)據(jù)同步解決,但還是會出現(xiàn)數(shù)據(jù)不一致和占用內(nèi)存問題) ?session代替方案應(yīng)該滿足:
數(shù)據(jù)共享內(nèi)存存儲key,value結(jié)構(gòu) 使用redis代替session是完全可以的
問題:是訪問不攔截的頁面,token不會刷新。而session是訪問哪個頁面都會刷新。 優(yōu)化:再加一個攔截器,攔截所有請求并且有token的話就刷新,第二個則判斷用戶是否在ThreadLocal中存在。這樣就不會出現(xiàn)不刷新的現(xiàn)象。
2.2 用戶查詢緩存模塊
?什么是緩存? 數(shù)據(jù)交換的緩沖區(qū)(cache),是貯存數(shù)據(jù)的臨時地方,一般讀寫性能高。 ?緩存的作用: 1. 降低后端負載。 2. 提高讀寫效率,降低響應(yīng)時間。 ?緩存的成本: 1. 數(shù)據(jù)一致性成本 2. 代碼維護成本 3. 運維成本
添加Redis緩存 根據(jù)id查詢店鋪緩存的流程 主動更新策略
?先刪除緩存,在操作數(shù)據(jù)庫
在線程1 刪除緩存后,線程2查詢緩存未命中,然后去查詢數(shù)據(jù)庫,最后把查詢結(jié)果寫入緩存。但此時,線程1更新數(shù)據(jù)庫的操作還沒有完成,線程2查到的是舊的值,寫入了緩存。當(dāng)線程1更新完數(shù)據(jù)庫之后,就會造成數(shù)據(jù)庫和緩存數(shù)據(jù)不一致問題。
?先操作數(shù)據(jù)庫,再刪除緩存
要想并發(fā)問題發(fā)生,首先要線程1查詢緩存,剛好緩存失效,然后去查詢數(shù)據(jù)庫。此時線程2要去更新數(shù)據(jù)庫,然后去刪除緩存。如果線程2在線程1的寫入緩存之前更新完數(shù)據(jù)庫和刪除完緩存,南無就會造成數(shù)據(jù)不一致問題。但畢竟緩存的操作速度快和線程1查詢時緩存剛好失效并且線程2要去更新數(shù)據(jù)庫。這些事情發(fā)生的概率極小。
所以選擇先操作數(shù)據(jù)庫,再刪除緩存。(給數(shù)據(jù)庫操作加鎖的話,應(yīng)該可以解決并發(fā)問題)
緩存穿透
是指用戶要查詢的數(shù)據(jù)在緩存和數(shù)據(jù)庫中都沒有,這樣緩存永遠不會生效,所有的請求都會到達數(shù)據(jù)庫,造成數(shù)據(jù)庫巨大的壓力。
常見的解決方案有兩種
緩存空對象
優(yōu)點:實現(xiàn)簡單,維護方便。缺點:內(nèi)存的消耗、短期的數(shù)據(jù)不一致。 布隆過濾器
優(yōu)點:內(nèi)存占用少,沒有多余的key.缺點:實現(xiàn)復(fù)雜、存在誤差。
查詢店鋪的緩存穿透解決(使用緩存空對象方式) 代碼如下:
public Shop queryWithPassThrough(Long id) {
String key = CACHE_SHOP_KEY + id;
//1 從Redis查詢商鋪信息
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2 判斷是否存在,isNotBlank(null," ", "")
if (StrUtil.isNotBlank(shopJson)) {
//3 存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
if (shopJson != null) { //因為等于null是沒有查到緩存,其他的""、" "是緩存的空對象直接返回
//返回錯誤信息
return null;
}
//4 不存在,根據(jù)id查詢數(shù)據(jù)庫
Shop shop = getById(id);
//5 不存在,返回錯誤
if (shop == null) {
//將空值寫入Redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); //設(shè)置ttl
return null;
}
//6 存在,寫入Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
//7 返回
return shop;
}
null是指沒有這個對象,空值(空字符串)是有這個對象,但是里面的內(nèi)容為空
緩存穿透的解決方案還有哪些?
緩存null值布隆過濾增強id的復(fù)雜度,避免被猜測id規(guī)律做好數(shù)據(jù)的基礎(chǔ)格式校驗加強用戶權(quán)限校驗做好熱點參數(shù)限流
緩存雪崩 ?緩存雪崩是指在一段時間大量的key過期或者Redis宕機,導(dǎo)致大量的請求打到數(shù)據(jù)庫,給數(shù)據(jù)庫造成巨大的壓力。 解決方案
給不同可以的TTL添加隨機值利用Redis集群提高服務(wù)的可用性給業(yè)務(wù)添加降級限流策略給業(yè)務(wù)添加多級緩存(瀏覽器緩存、Nginx緩存、Tomcat緩存等)
緩存擊穿 ?緩存擊穿問題也叫熱點key問題,就是一個被高并發(fā)訪問并且緩存業(yè)務(wù)重建復(fù)雜的key突然失效了,無數(shù)的請求訪問會在瞬間給數(shù)據(jù)庫造成巨大的沖擊。
?常見的解決方案有兩種
互斥鎖邏輯過期 在業(yè)務(wù)中經(jīng)常會遇到一致性和可用性的選擇。 需求:修改根據(jù)id查詢商鋪的業(yè)務(wù),基于邏輯過期方式來解決緩存擊穿問題。 代碼如下:
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
//緩存擊穿 邏輯過期
public Shop queryWithLogicalExpire(Long id) {
String key = CACHE_SHOP_KEY + id;
//1 從Redis查詢商鋪信息
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2 判斷是否存在
if (StrUtil.isBlank(shopJson)) {
//3 存在,直接返回
return null;
}
// 4.命中,需要先把json反序列化為對象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判斷是否過期
if (expireTime.isAfter(LocalDateTime.now())) {
// 5.1未過期,直接返回店鋪信息
return null;
}
// 5.2已過期,需要緩存重建
//6.緩存重建
//6.1獲取互斥鎖
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
//6.2判斷獲取鎖是否成功
if (isLock){
if(!expireTime.isAfter(LocalDateTime.now())) {
//6.3成功開啟獨立線程
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
this.saveShop2Redis(id, 20L);
} finally {
unlock(lockKey);
}
});
}
}
//7 返回
return shop;
}
緩存工具的封裝 ?代碼如下:
//緩存穿透 緩存空對象
public
String keyPrefix, ID id, Class
String key = keyPrefix + id;
//1 從Redis查詢商鋪信息
String json = stringRedisTemplate.opsForValue().get(key);
//2 判斷是否存在
if (StrUtil.isNotBlank(json)) {
//3 存在,直接返回
return JSONUtil.toBean(json, type);
}
if (json != null) {
//返回錯誤信息
return null;
}
//4 不存在,根據(jù)id查詢數(shù)據(jù)庫
R r = dbFallback.apply(id);
//5 不存在,返回錯誤
if (r == null) {
//將空值寫入Redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//6 存在,寫入Redis
this.set(key, r, time, unit);
//7 返回
return r;
}
//緩存擊穿,設(shè)置邏輯過期時間
public
String key = keyPrefix + id;
//1 從Redis查詢商鋪信息
String json = stringRedisTemplate.opsForValue().get(key);
//2 判斷是否存在
if (StrUtil.isBlank(json)) {
//3 存在,直接返回
return null;
}
// 4.命中,需要先把json反序列化為對象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判斷是否過期
if (expireTime.isAfter(LocalDateTime.now())) {
// 5.1未過期,直接返回店鋪信息
return null;
}
// 5.2已過期,需要緩存重建
//6.緩存重建
//6.1獲取互斥鎖
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
//6.2判斷獲取鎖是否成功
if (isLock){
if(!expireTime.isAfter(LocalDateTime.now())) {
//6.3成功開啟獨立線程
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
R r1 = dbFallback.apply(id);
this.setWithLogicalExpire(key, r1, time, unit);
} finally {
unlock(lockKey);
}
});
}
}
//7 返回
return r;
}
使用方式
//一行解決緩存穿透,封裝了方法
Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, id2 -> getById(id2), CACHE_SHOP_TTL, TimeUnit.MINUTES);
2.3 優(yōu)惠券秒殺功能
全局唯一ID 代碼如下:
public long nextId(String keyPrefix) {
//1. 生成時間戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timeStamp = nowSecond - BEGIN_TIMESTAMP;
//2. 生成序列號
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd")); //方便統(tǒng)計年月日
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);//一天下單的量,拼接日期,還有統(tǒng)計效果
//3. 拼接并返回
return timeStamp << COUNT_BITS | count;
}
全局唯一ID生成策略:
UUIDRedis自增snowflake算法數(shù)據(jù)庫自增
Redis自增ID策略:
每天一個key,方便統(tǒng)計訂單量ID構(gòu)造是時間戳+計數(shù)器
實現(xiàn)優(yōu)惠劵秒殺的下單功能 ?下單時需要判斷兩點:
秒殺是否開始或結(jié)束,如果尚未開始或已結(jié)束則無法下單庫存是否充足,不足則無法下單
更新庫存和查詢版本是數(shù)據(jù)庫自帶命令,是原子操作,不會有線程安全問題。 如果字段不是庫存,需要加版本號,可以通過分段鎖提高成功率,例如currentHashMap中的分段鎖。
一人一單:同一個優(yōu)惠劵,一人只能下一單。 分布式鎖 在集群模式下,普通的鎖還會出現(xiàn)問題,因為不同jvm有不同的鎖監(jiān)視器。
代碼如下:
public class SimpleRedisLock implements ILock{
private String name;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
private static final DefaultRedisScript
static {
UNLOCK_SCRIP = new DefaultRedisScript<>();
UNLOCK_SCRIP.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIP.setResultType(Long.class);
}
@Override
public boolean tryLock(long timeoutSec) {
String threadId = ID_PREFIX + Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId , timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
//調(diào)用lua腳本, 判斷和釋放在一行代碼執(zhí)行,滿足原子性。
stringRedisTemplate.execute(
UNLOCK_SCRIP,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
分布式鎖基于Redis的極端情況,誤刪情況 極端情況下依然會出現(xiàn)線程誤刪,釋放業(yè)務(wù)阻塞,以判斷完畢。 獲取鎖標(biāo)識并判斷要和釋放鎖是原子操作 Redisson入門 Redis三種消息隊列
2.4 好友關(guān)注功能
基于Set集合的關(guān)注、取關(guān)、共同關(guān)注、消息推送等功能 實現(xiàn)分頁查詢
3. 總結(jié)
使用 Redis 解決了在集群模式下的 Session共享問題,使用攔截器實現(xiàn)用戶的登錄校驗和權(quán)限刷新 基于Cache Aside模式解決數(shù)據(jù)庫與緩存的一致性問題 使用 Redis 對高頻訪問的信息進行緩存 ,降低了數(shù)據(jù)庫查詢的壓力 ,解決了緩存穿透、雪崩、擊穿問題使用 Redis + Lua腳 本實現(xiàn)對用戶秒殺資格的預(yù)檢 ,同時用樂觀鎖解決秒殺產(chǎn)生的超賣問題 使用Redis分布式鎖解決了在集群模式下一人一單的線程安全問題 基于stream結(jié)構(gòu)作為消息隊列,實現(xiàn)異步秒殺下單 使用Redis的 ZSet 數(shù)據(jù)結(jié)構(gòu)實現(xiàn)了點贊排行榜功能,使用Set 集合實現(xiàn)關(guān)注、共同關(guān)注功能
柚子快報邀請碼778899分享:面試項目準(zhǔn)備:黑馬點評項目總結(jié)
好文推薦
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點和立場。
轉(zhuǎn)載請注明,如有侵權(quán),聯(lián)系刪除。