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