文章目錄
- 一、關于三級目錄
- 二、使用nginx
- 三、JMeter壓測+JvisualVM監測+性能優化
- 四、分布式快取
- 1、Redis
- 2、快取擊穿、穿透、雪崩
- 3、 加鎖解決快取擊穿(本地鎖)
- 4、加鎖解決快取擊穿(分布式鎖)
- 5、Redisson基本介紹
- 6、將Redisson集成到專案里
- 7、如何保證快取和資料庫中的資料一致?
- 五、SpringCache
- 1.和快取有關的注解:
- 2.@Cacheable注解的使用,同時它存在的問題:
- 3.解決@Cacheable注解存在的問題
- 4.解決@Cacheable注解存在的問題
- 5.`@CacheEvict`、`@Cacheput`、`@chaching`注解的演示
- 6.在組態檔中,還可以指定一些快取的自定義配置
- 7.SpringCache的不足
- 階段總結:
- 六、ES檢索查詢
- 1.前臺功能
- 2.前臺傳回來的檢索條件
- 3.后臺回傳給前臺的資料
- 4.總體邏輯:
- 七、面包屑功能
- 八、 商品詳情頁
- 九、注冊頁面—驗證碼功能
- 八、注冊頁面—注冊功能
- 給密碼加密的方式
- P219 登陸頁面—用戶名密碼登錄
- 九、登陸頁面—完成微博登錄
- 十、SpringSession—session不共享、不跨域問題
- 1.session不能跨域問題
- 2.分布式下session共享問題
- 3.session共享問題的解決方案
- 4.總說:
- 5.修改微博登陸的代碼
- 6.修改賬號密碼登錄的代碼
- 十一、單點登錄
- 1.為什么要單點登錄?
- 2.單點登錄的原理?
- 十二、購物車(面試版)
- 1.添加商品到購物車
- 2.購物車種資料的存盤方式:
- 3.展示購物車:
- 4.更加細節的東西
- 十四、購物車(精細版)
- 1、關于攔截器
- 2、添加商品到購物車
- 7.測驗:
- 3、獲取購物車
- 4、選中購物車項
- 5、修改購物項數量
- 6、洗掉購物項
一、關于三級目錄
查詢資料中封裝了三級目錄的那張表,然后把里面的資料一次性全查出來封裝到物體類List0<CategoryEntity>里面;
從List0<CategoryEntity>物體類中查找parent_cid為0的就是1級分類;
查找List0<CategoryEntity>,里面parent_id為一級分類id的就是二級分類;
查找List0<CategoryEntity>,里面找parent_id為二級分類id的就是三級分類;
總之全程只查詢了"pms_category"表,沒有涉及到其他表,而且只查詢了一遍資料庫,
二、使用nginx
瀏覽器訪問gulimall.com,本地根據在windows的hosts檔案配置里找到gulimall.com映射的是虛擬機ip“192.168.56.106”,于是轉發到虛擬機,虛擬機會交給nginx,nginx有一處配置專門監聽gulimall.com,監聽到以后根據配置轉發到88埠的gulimall-gateway,網關根據斷言轉發到gulimall-product
三、JMeter壓測+JvisualVM監測+性能優化
1.中間件的影響
前臺請求先經過nginx,然后nginx交給gulimall-Gateway,然后才能到達具體的服務,中間兩個中間件nginx、Gateway會不會影響性能?
2.做了哪些壓測?
先壓測localhost:10000,因為它沒有使用中間件直接訪問到首頁,所以回應也挺快的;
然后壓測gulimall.com,因為它有了nginx,有了Gateway,查看它的回應時間;
3.關于資料的三次查詢
訪問首頁gulimall.com,首頁會訪問資料庫查詢資料庫的三級目錄,模板引擎需要將查詢到的資料轉交給thymeleaf然后渲染到頁面,你的業務代碼都會導致回應會慢很多,比如你查詢三級分類時查詢資料庫要盡量一次拿到pms_category的所有資料,而不是一級分類查一下資料庫、二級分類查一下資料庫、三級分類再查一下資料庫,
3.優化的方向:
1.優化JVM:測驗時JvisualVM發現伊甸園區記憶體只有32M,超小,所以垃圾回收次數非常多,所以如果伊甸園區調整的大一些就gc的時間就減少很多,那么吞吐量也就上去了,而且老年代也很小,導致幾乎就要爆滿了,給gulimall-product設定一Xmx1024m -xms1024m 一Xmn512m(記憶體最大占用1024M,初值也時1024M,相當于記憶體大小固定好了就是1024M,Xmn就是伊甸園區,給伊甸園區調大到512M)
2.業務代碼也很影響性能,①查詢資料庫次數問題,③模板的快取問題,你開發時經常在yml中有個配置就是thymeleaf.cache: false關閉thymeleaf快取便于除錯,到了實際上線后一定要開啟快取,④優化日志級別,以前是debug,現在改為error,也就是只列印錯誤日志,
3.使用Redis快取,將三級目錄的資料存放到快取里面,這就不用來一個查一個,
4.動靜分離:以前我們是動態請求、靜態請求都是先找nginx,然后找Gateway,然后找具體的微服務,所以我們可以把靜態資源上傳到nginx上面,這樣靜態請求只需要找到nginx就看拿到對應的資源了,
5.優化資料庫:優化資料庫,在查詢三級目錄時經常查詢parent_cid,由于parent_cid不是主鍵沒有索引導致查詢起來其實很慢,你如果查詢id那種主鍵、有索引的就會很快很快,所以給parent_cid加上索引(索引型別就是普通索引,不用選成主鍵索引),那么查詢速度就會快很多
串起來:一個請求先經過
nginx,然后經過業務代碼,代碼底層有JVM,另外你還優化了兩個資料庫(Redis和MySQL)
四、分布式快取
1、Redis
1.哪些資料適合放入鍰存?
- 即時性、資料一致性要求不高的
- 訪間量大且更新頻率不高的資料(讀多,寫少)
2.本地快取與分布式快取對比
本地快取存在的問題:
(1)快取不共享:每個服務都有一個快取,但是這個快取并不共享,
(2)快取一致性問題:在一臺設備上的快取更新后,其他設備上的快取可能還未更新,這樣當從其他設備上獲取資料的時候,得到的可能就是未給更新的資料,
分布式快取:
一個服務的不同副本可以共享同一個快取空間,可以是redis,必要時還可以使用redis集群,
2、快取擊穿、穿透、雪崩
前面我們將查詢三級分類資料的查詢進行了優化,將查詢結果放入到Redis中,當再次獲取到相同資料的時候,直接從快取中讀取,沒有則到資料庫中查詢,并將查詢結果放入到Redis快取中
1.快取穿透:
- 指查詢一個一定不存在的資料,由于快取是不命中,將去查詢資料庫,但是資料庫也無此記錄,我們沒有將這次查詢的null寫入快取,這將導致這個不存在的資料每次請求都是快取是不命中然后查詢資料庫然后資料庫也沒有,
- 解決辦法就是:從資料庫查詢到null以后寫入快取,以后凡是請求這個資源的讓快取直接回傳null,別再查詢資料了,(當然快取中存放的這個null肯定得有過期時間)
2.快取雪崩:
- 快取雪崩是指在我們設定快取時key采用了相同的過期時間,導致快取在某一時刻同時失效,請求全部轉發到DB,DB瞬時壓力過重雪崩,
- 解決:原有的失效時間基礎上增加一個隨機值,比如1-5分鐘隨機,這樣每一個快取的過期時間的重復率就會降低,就很難引發集體失效的事件,
3.快取擊穿
- 對于一些設定了過期時間的key,如果這些key可能會在某些時間點被超高并發地訪問,是一種非常“熱點”的資料,如果這個key在100萬請求同時進來前一秒正好失效,那么所有對這個key的資料查詢都落到db,我們稱為快取擊穿(還沒來得及寫入快取資料庫就被查了100來萬遍),
- 解決:加鎖,大量并發只讓一個去查,其他人等待,查到以后釋放鎖,其他人獲取到鎖,先查快取,就會有資料,不用去db
簡單來說:快取穿透是指查詢一個永不存在的資料(
巧計:真空下光線穿透);快取雪崩是值大面積key同時失效問題;快取擊穿是指高頻key失效問題;
3、 加鎖解決快取擊穿(本地鎖)
本地鎖在分布式情況下存在的問題
把gulimall-product復制四份,然后讓JMeter大并發去訪問gulimall.com,我們發現在分布式下的四個服務分別存在著四個快取未命中的情況,也就意味著會有四次查詢資料庫的操作,顯然我們的synchronize鎖未能實作限制其他服務實體進入臨界區,也就印證了在分布式情況下,本地鎖只能針對于當前的服務生效,
4、加鎖解決快取擊穿(分布式鎖)
在Redisson出現之前我們使用的就是去Redis中占坑的方式去獲得分布式鎖,我們占坑的方法
lock=setIfAbsent(key,value)就是如果這個key不存在的話就設定key-value,而且回傳true;如果存在了就設定不了key-value,回傳false
加了分布式鎖解決快取擊穿,但是分布式鎖存在很多需要考慮的因素:
- 萬一某個執行緒拿到分布式鎖后執行業務邏輯時拋出例外,此時會被該執行緒獨占這把鎖——解決辦法:給鎖要設定過期時間
- 加鎖和給鎖設定過期時間這兩行代碼存在時間差,萬一在這個時間差內出現斷電什么的也會被某個執行緒拿到沒有被設定過期時間的鎖——解決辦法:加鎖和設定程序時間弄成原子性操做,要么同時成功要么同時失敗,
- 刪鎖時發現你的鎖已經過期了,你的鎖已經被別的執行緒拿到了,別的執行緒就會進來,你此時如果刪鎖那么刪掉的是別人的鎖——解決辦法:給鎖設定uuid,每個人都不一樣,你就刪不了別人的鎖了,
- 上一步說到刪鎖時先比對uuid再刪鎖,期間存在時間差,萬一在你比對完發現這把鎖就是你的鎖,正要刪鎖時你的鎖過期了,被別的執行緒拿到了,你刪掉的就是別人的鎖,——解決辦法:比對uuid和刪鎖必須是原子操作,
- 一句話:加鎖保證原子性,解鎖保證原子性,
- 可以看到:手寫一個分布式鎖很麻煩,所以我們用Redisson,Redisson就是分布式鎖,而且不用再考慮上面的那些功能
5、Redisson基本介紹
1.SpringBoot整合Redisson
寫一個配置類,配置類中指明Redis的地址,回傳Redisson
@Configuration
public class MyRedisConfig {
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.137.14:6379");
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
然后直接使用
RLock lock = redisson.getLock("my-lock");
lock.lock();
lock.unlock();
2.Redisson的特性
設想一種情況,一個請求執行緒在執行業務方法的時候,突然發生了中斷,此時沒有來得及執行釋放鎖操作,那么同時等待的另外一個執行緒是否會發生死鎖,
在A服務在獲取鎖后,突然中斷它的運行;等待的B服務會很快就拿到鎖,不會因為A沒有釋放鎖而被卡死,通這是因為在Redisson中會為每個鎖加上“leaseTime”,默認是30秒,如果A服務宕機,到了時間就會自動釋放鎖,如果A服務沒有宕機,而且30秒不夠用,Redisson會自動給它續期,當然,人家默認的自動解鎖時間是30秒,如果你改為10秒,那么10秒后立刻釋放鎖,不會給鎖續期,但是這種自定義解鎖場景也很常用,你可以自定義300秒,如果一個業務300秒都沒有執行完肯定就有問題,而且我們還可以拿它評估一下業務的最大執行用時,
小結:redisson的lock具有如下特點
- (1)阻塞式等待,默認的鎖的時間是30s,
- (2)鎖定的制動續期,如果業務超長,運行期間會自動給鎖續上新的30s,無需擔心業務時間長,鎖自動被洗掉的問題,
- (3)加鎖的業務只要能夠運行完成,就不會給當前鎖續期,即使不手動解鎖,鎖默認在30s以后自動洗掉,
- (4)可以自定義解鎖時間,時間到了不會續期,但它可以評估一下業務的最大執行用時
3.Redisson的讀寫鎖
寫+讀:要等寫完才能讀
寫+寫:等前一個寫完后一個才能寫
讀+讀:相當于無鎖,大家都能讀
讀+寫:有讀鎖,寫必須等待
讀寫鎖適合經常讀、很少寫的情況,因為讀的時候相當于無鎖,
4.Redisson的閉鎖
走完五個人就鎖門,這就是閉鎖
5.Redisson的信號量
車庫停車,3個停車位,獲取到信號量才能進去停車,
以上演示的Redisson的讀寫鎖、閉鎖、信號量都是分布式下也適用的情況,
6、將Redisson集成到專案里
public Map<String, List<Catalog2Vo>> getCatalogJson() {
String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isEmpty(catalogJSON)) {
//快取中沒有,查詢資料庫
System.out.println("快取不命中,,,,將要查詢資料庫,,,,");
Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithRedisLock();
return catalogJsonFromDb;
}
System.out.println("快取命中,,,,直接回傳,,,,");
Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {});//轉為我們指定的物件
return result;
}
public Map<String, List<Catalogs2Vo>> getCatalogJsonFromDbWithRedissonLock() {
//注意鎖的粒度問題
RReadWriteLock lock = redissonClient.getReadWriteLock("catalogJson-lock");
lock.lock();
Map<String, List<Catalogs2Vo>> dataFromDb = null;
try {
dataFromDb = getCatalogJsonFromDB();//從資料庫中查詢三級目錄,查詢之前我們還要再去快取中確定一次,如果沒有才需要繼續查詢
} finally {
lock.unlock();
}
return dataFromDb;
}
3.使用Redsson時應該注意鎖的粒度問題
給鎖起名字要注意,不能都起一樣的名字,一樣的名字代表同一把鎖,獲取三級分類資料、獲取品牌、獲取屬性鎖到同一把鎖里面,那就導致粒度很粗,假如訪問三級分類是高并發的請求,訪問品牌是低并發的,他倆如果同一把鎖那么高并發的鎖住導致低并發的也訪問不到,
7、如何保證快取和資料庫中的資料一致?
-
如何保證快取和資料庫中的資料一致?
①雙寫模式(改了資料庫順帶著改了快取)
②失效模式(改了資料庫順帶刪了快取)
③雙寫模式/失效模式+讀寫鎖
④使用Canal(MySQL中一有什么變化就會同步到快取中來) -
各自的弊端:
①雙寫模式:A要把a改為1,然后B要把a改為2,最后資料庫a應該是2,但是A把a改為1本來順帶改一下快取結果卡頓了,導致B把a改為2順帶先改了快取,然后卡頓的A改了快取導致快取中a是1但資料庫中的a是2,高并發下快取不一致出現了,這就又又又得加鎖解決,
②失效模式:(A和B是寫操作,改了資料庫就要刪快取;C是讀操作,如果快取中讀不到就得去資料庫讀然后寫到快取中)現在有這么一個場景:A要把a改為1,然后B要把a改為2,然后C要讀取a,本來C讀到的a應該是2,但是A要把a改為1然順帶刪了快取;然后B要把a改為2結果B卡頓住了;C進來讀取快取發現快取沒有資料就讀資料庫讀到了a=1,因為快取中沒有資料所以C要把讀到的a寫到快取上,但是C寫快取之前也卡頓了一下;結果現在B變流暢了,它把資料庫a改為2,順帶要刪快取,結果發現快取中還沒有資料所以就不刪了;現在C開始了,它把讀到的a=1寫到快取中,又要加鎖,
③雙寫模式/失效模式+讀寫鎖:這個沒什么問題,但是代碼太復雜了吧,
④使用Canal:Canal是第三方的,使用起來非常方便,而且也沒什么問題,但是又加了一個中間件,還得自定義一些功能,我們這個小專案就不用了,
我們系統的一致性解決方案:
實時性、一致性要求高的那就去資料庫中查;
實時性、一致性要求不高的那就放到快取中,如果害怕出現臟資料,那就給快取加上過期時間,然后使用雙寫模式/失效模式+讀寫鎖,代碼很復雜,所以SpringCache應用而生
五、SpringCache
1.和快取有關的注解:
@Cacheable:觸發將資料保存到快取的操作
@CacheEvict:觸發將資料從快取中洗掉的操作
@CachePut:在不影響方法執行的情況下更新快取,
@Caching:組合以上多個操作
@CacheConfig: 在類級別上共享一些公共的與快取相關的設定,
2.@Cacheable注解的使用,同時它存在的問題:
- 默認的過期時間是無限時間
- 默認的資料保存方式不是json的
- 默認的key不好,我們要自定義存到快取里面的key
//這是以前撰寫的前臺訪問/index時獲取一級目錄的方法,我們只需要在上面添加@Cacheable注解就表示如果快取中有就不用執行下面的方法,快取中沒有就執行下面的方法查出資料并且放入快取
@Cacheable({"catagory"})
@Override
public List<CategoryEntity> getLevel1Categorys() {
System.out.println("getLevel1Categorys......");
long l = System.currentTimeMillis();
return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
}
3.解決@Cacheable注解存在的問題
@Cacheable注解里面有些默認配置不合理,我們要自定義
- 自定義過期時間
- 自定義存到快取里面的key
#在yml中指定過期時間
spring.cache.redis.time-to-live=3600000
//因為spel動態取值,所有需要額外加''表示字串
@Cacheable(value = {"catagory"},key = "'Level1Categorys'")
@Override
public List<CategoryEntity> getLevel1Categorys() {
System.out.println("getLevel1Categorys......");
long l = System.currentTimeMillis();
return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
}
4.解決@Cacheable注解存在的問題
@Cacheable注解里面有些默認配置不合理,我們要自定義
- 自定義資料保存方式是json,寫個配置類就行了
5.@CacheEvict、@Cacheput、@chaching注解的演示
說明:
getLevel1Categorys()是從資料庫中讀取一級分類資料,getCatalogJson()是從資料庫中讀取三級分類資料,;
updateCascade()是更新資料庫中的三級分類資料,一旦資料庫中三級分類資料被更新,那么那么一級目錄的資料和三級目錄的資料都變了,所以需要清除getLevel1Categorys()和getCatalogJson()里面的快取資料,
1.清除快取的方法一
@Caching(evict = {
@CacheEvict(value = "category",key = "'getLevel1Categorys'"),
@CacheEvict(value = "category",key = "'getCatalogJson'")
})
@Transactional(rollbackFor = Exception.class)
@Override
public void updateCascade(CategoryEntity category) {
...................
}
@Cacheable(value = {"category"},key = "#root.method.name")
@Override
public List<CategoryEntity> getLevel1Categorys() {
...................
}
@Cacheable(value = {"catagory"},key = "#root.method.name")
@Override
public Map<String, List<Catalog2Vo>> getCatalogJson() {
...................
}
1.清除快取的方法二
@CacheEvict(value = "category",allEntries = true) //洗掉某個磁區下的所有資料
@Transactional(rollbackFor = Exception.class)
@Override
public void updateCascade(CategoryEntity category) {
//更新資料庫同時修改快取中的資料,
}
6.在組態檔中,還可以指定一些快取的自定義配置
spring.cache.type=redis
#設定超時時間,默認是毫秒
spring.cache.redis.time-to-live=3600000
#設定Key的前綴,如果指定了前綴,則使用我們定義的前綴,否則使用快取的名字作為前綴
spring.cache.redis.key-prefix=CACHE_
spring.cache.redis.use-key-prefix=true
#是否快取空值,防止快取穿透
spring.cache.redis.cache-null-values=true
7.SpringCache的不足
p155 快取擊穿、穿透、雪崩問題能用SpringCache解決掉嗎?
(總說)先明確什么是讀模式什么是寫模式:
getLevel1Categorys()是從資料庫中讀取一級分類資料,getCatalogJson()是從資料庫中讀取三級分類資料,它們都是讀模式,讀模式就是從資料中讀取資料,然后使用@Cacheable將資料放入快取,- updateCascade()是更新資料庫中的三級分類資料,他就是寫模式,寫模式就是更新資料庫資料,資料庫資料一旦被更新就需要使用@CacheEvict()注解清除快取中的舊資料,
1)、讀模式
- 快取穿透:并發查詢一個null資料就會產生快取穿透,SpringCache可以解決,解決方案:快取空資料,可通過yml中的
spring.cache.redis.cache-null-values=true配置來實作 - 快取擊穿:大量并發進來同時查詢一個正好過期的資料,解決方案:需要加鎖,使用
@Cacheable(sync = true)來解決擊穿問題, - 快取雪崩:大量的key同時過期,解決:加隨機時間,
- 也就是說在讀模式中SpringCache能夠解決掉所有問題,
2)、寫模式:(快取與資料庫一致)
- 讀寫加鎖,
- 引入Canal,感知到MySQL的更新去更新Redis
- 讀多寫多,直接去資料庫查詢就行
3)、總結:
常規資料(讀多寫少,即時性,一致性要求不高的資料,完全可以使用Spring-Cache):
寫模式(只要快取的資料有過期時間就足夠了,過期了讓它自己更新就可以了)
特殊資料:你還想加快取,還想保證資料庫和快取的一致性,那就需要結合Redisson來使用
階段總結:
p136到p138是搭建了首頁;
p139和p140是讓我們借助nginx來通過域名訪問這個首頁(假如nginx肯定會使訪問路線更加曲折,從而影響性能);
p141-p147是通過Jmeter、JvisualVM來分析加入Gateway、nginx這些中間件帶來的性能損失,
p148-p150是進行性能優化—動靜分離、JVM記憶體優化、代碼優化
p151--p154是進行性能優化—使用Redis
p155—p158是要解決快取擊穿就需要加鎖,而加本地鎖不行,只能加分布式鎖,但是加分布式鎖又要考慮一堆分布式并發問題,于是就有了Redisson;
p159—p166是給你介紹了Redisson分布式鎖的用法,但是用上Redisson后還要考慮快取和資料庫一致性問題,于是SpringCache應用而生,
p167—p172有了SpringCache,常規資料的快取你可以不用Redisson,因為SpringCache已經考慮到快取雪崩、擊穿、穿透問題了,它里面可以加鎖,可以設定過期時間等等,
從P173開始,完整的筆記全部參考這位博主寫的筆記:谷粒商城-個人筆記(高級篇二)
| 分割線 |
六、ES檢索查詢
1.前臺功能
2.前臺傳回來的檢索條件
下面的內容必須背誦,你至少得知道你的ES檢索是可以檢索哪些東西啊,比如可以根據銷量檢索,可以根據價格檢索等等
完整查詢引數 keyword=小米&catalog3Id=1&brandId=1&hasStock=0/1&skuPrice=400_1900&at trs=1_3G:4G:5G&attrs=2_驍龍845&attrs=4_高清屏&sort=saleCount_desc/asc
@Data
public class SearchParam {
private String keyword;//頁面傳遞過來的全文匹配關鍵字
//sort=saleCount_asc/desc銷量
//sort=skuPrice_asc/desc價格
//sort=hotScore_asc/desc熱度分
private String sort;//排序條件
//hasStock=0/1
private Integer hasStock;//是否只顯示有貨
//skuPrice=1_500
private String skuPrice;//價格區間查詢
//brandId=2&brandId=3
private List<Long> brandId;//按照品牌進行查詢,可以多選
//catelog3Id=1
private Long catalog3Id;//三級分類id
//attr=1_3G:4G:5G;attrs=2_驍龍
private List<String> attrs;//按照屬性進行篩選
private Integer pageNum = 1;//頁碼
}
3.后臺回傳給前臺的資料
@Data
public class SearchResult {
//查詢到的商品資訊
private List<SkuEsModel> products;
//分頁資訊
private Integer pageNum;//當前頁碼
private Long total;//總記錄數
private Integer totalPages;//總頁碼
//所有涉及到的品牌
private List<BrandVo> brands;
//所有涉及到的分類
private List<CatalogVo> catalogs;
//所有涉及到的屬性
private List<AttrVo> attrs;
}
4.總體邏輯:
前臺把查詢的條件封裝到SearchParam里面,后臺根據SearchParam查詢ElastiSearch,后臺寫的Java代碼事實上就是動態的DSL陳述句,用DSL陳述句查詢ElasticSearch,把查詢到的結果從DSL陳述句中提取出來,封裝到SearchResult里面回傳給前臺,
@GetMapping("/list.html")
public String listPage(SearchParam searchParam, Model model) {
SearchResult result = mallSearchService.search(searchParam);
System.out.println("===================="+result);
model.addAttribute("result", result);
return "list";
}
七、面包屑功能
做法很簡單,我們之前前臺給后臺傳回去的SearchParam不變,但是后臺回傳給前臺的SearchResult里面再添加一個新的欄位List<NavVo> navs,在NavVo里面有navName,navValue,link這三個欄位;
假如你點擊了一個屬性是“高清屏”,那么前臺傳給后臺就有attrs=4_高清屏,對于NavVo里的navValue其實就是"高清屏";對于NavVo里的navName其實就是根據attrId呼叫gulimall-product查詢屬性表得到attr_name,attrId不就是4嘛;對于NavVo里的link其實就是沒點面包屑之前的url,點了面包屑不就是在原先url基礎上多拼裝了一個attrs=4_高清屏嘛,所以你從前端拿到現在的url(也就是點了面包屑以后的url)然后切割一下就行了,
需要注意的三個點:
①因為遠程呼叫gulimall-product所以可以在被呼叫的gulimall-product的那個方法上添加快取@cacheable(value = "attr",key = " 'attrInfo'+#root.args[0]")
②如何從前端拿到現在的url?
③通過以上方法拿到的前端的url是被URL編碼的結果&attrs=%257B%2522request%255Fid%2522%253A%25,不是你想要的url,所以你需要先解碼,
| 分割線 |
八、 商品詳情頁
前臺傳回來的只有skuid,然后查詢對應的表得到對應的封裝資訊
九、注冊頁面—驗證碼功能
總說:
完成用戶在注冊頁面的發送驗證碼的操做:前臺發送
/sms/sendcode的請求給后臺的gulimall-auth-server,然后gulimall-auth-server會先驗證一下驗證碼是否在60秒前發送過(介面防刷),如果沒有就使用OpenFeign遠程呼叫gulimall-thrid-party的sendCode方法完成第三方服務的發送驗證碼功能,
關于介面防刷
gulimall-auth-server如何校驗驗證碼是否在60秒前發送過?當前臺帶著手機號發送
/sms/sendcode的請求給后臺,后臺先到redis中根據key為(“sms:code:”+phone)嘗試獲取這段redis資訊,如果獲取不到,后臺會在redis中存盤(key為"sms:code:"+phone,value為"驗證碼_當前時間",過期時間是10分鐘)的一段資訊,然后遠程呼叫發送驗證碼方法;假如60秒內前臺帶著該手機號再次發送/sms/sendcode的請求給后臺,后臺先到redis中根據key為(“sms:code:”+phone)嘗試獲取這段redis資訊,如果能夠獲取到這段資訊就判斷時間差是否小于60s,如果是就不進行發送驗證碼操做,
八、注冊頁面—注冊功能
用戶會填好驗證碼和個人的注冊資訊封裝到UserRegistVo后發送給gulimall-auth-server
然后gulimall-auth-server首先進行JSR303校驗,若JSR303校驗未通過,則通過BindingResult封裝錯誤資訊,并重定向至注冊頁面;
若通過JSR303校驗,則需要從redis中取值判斷驗證碼是否正確,正確的話遠程呼叫會員服務注冊;
會員服務呼叫成功則重定向至登錄頁,否則封裝遠程服務回傳的錯誤資訊回傳至注冊頁面,
撰寫UserRegistVo類,代碼如下:
@Data
public class UserRegistVo {
@NotEmpty(message = "用戶名必須提交")
@Length(min = 6, max = 18, message = "用戶名必須是6-18位字符")
private String userName;
@NotEmpty(message = "密碼必須填寫")
@Length(min = 6, max = 18, message = "密碼必須是6-18位字符")
private String password;
@NotEmpty(message = "手機號必須填寫")
@Pattern(regexp = "^[1]([3-9])[0-9]{9}$",message = "手機號格式不正確")
private String phone;
@NotEmpty(message = "驗證碼必須填寫")
private String code;
}
撰寫LoginController類,下面的注釋一定一定要好好看!!!
/**
* 下面的代碼可以說相當重要,regist()方法一共有三個引數,UserRegistVo是封裝前臺傳過來的資料,BindingResult封裝JSR303校驗錯誤資訊
* RedirectAttributes是重定向攜帶資料,轉發的時候session共享資料,重定向的時候如何共享資料呢?
* 使用RedirectAttributes,它利用session原理,將資料放在session中,只要跳到下一個頁面,取出資料以后,session里面的資料就會刪掉,
*/
@PostMapping("/regist")
public String regist(@Valid UserRegistVo vo, BindingResult result,
RedirectAttributes redirectAttributes){
if (result.hasErrors()){
//如果校驗不通過,則封裝校驗結果,將錯誤資訊封裝到redirectAttributes中
Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
redirectAttributes.addFlashAttribute("errors",errors);
//使用return "reg"; 轉發會出現重復提交的問題,不要以轉發的方式
//使用 return "forward:/reg.html"; 會出現問題:Request method 'POST' not supported的問題(原因:用戶注冊-> /regist[post] ------>轉發/reg.html (路徑映射默認都是get方式訪問的))
//使用重定向 解決重復提交的問題,但面臨著資料不能攜帶的問題,就用RedirectAttributes,
return "redirect:http://auth.gulimall.com/reg.html";
}
//1、校驗驗證碼
String code = vo.getCode();
String s = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
if (!StringUtils.isEmpty(s)) {
if (code.equals(s.split("_")[0])) {
//驗證碼通過,洗掉快取中的驗證碼;令牌機制
stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
//真正注冊呼叫遠程服務注冊
R r = memberFeignService.regist(vo);
if (r.getCode() == 0) {
//成功
return "redirect:http://auth.gulimall.com/login.html";
} else {
Map<String, String> errors = new HashMap<>();
errors.put("msg", r.getData(new TypeReference<String>() {
}));
redirectAttributes.addFlashAttribute("errors", errors);
}
} else {
Map<String, String> errors = new HashMap<>();
errors.put("code", "驗證碼錯誤");
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
} else {
Map<String, String> errors = new HashMap<>();
errors.put("code", "驗證碼錯誤");
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimall.com/reg.html";//校驗出錯重定向到注冊頁
}
//注冊成功回到登錄頁
return "redirect:http://auth.gulimall.com/login.html";
}
遠程呼叫會員服務,會員服務干了什么?會查詢ums_member表的phone的數量是否大于0,如果大于0說明手機號已經存在,回傳錯誤資訊;查詢ums_member表的username的數量是否大于0,如果大于0說明用戶名已經存在,回傳錯誤資訊;如果ums_member表中手機號數量問0、用戶名資料為0,那就把(用戶名、手機號、密碼)一并存入ums_member表,其中密碼加密適用了BCrypt加密方式
給密碼加密的方式
給用戶密碼加密的三種方式對比:MD5加密、鹽值加密、BCrypt加密
可逆加密:知道了加密演算法后通過密文可以推算出原來的明文
不可逆加密:即使知道了加密演算法通過密文也不可以推算出原來的明文
①MD5加密:知道了密文可以推算出原來的明文,網上隨處可找MD5破解
String s = DigestUtils.md5Hex("123456");
System.out.println(s);//e10adc3949ba59abbe56e057f20f883e
②MD5加鹽(鹽值加密)
可以給隨機鹽也可以給指定鹽值,反正就是對“密碼+鹽值”進行MD5加密,你只能把鹽值保存起來然后下一次對“密碼+鹽值”進行再加密然后比對密文是否一致來判斷用戶名密碼正確與否
String s = Md5Crypt.md5Crypt("123456".getBytes(),"$1$qqqqqqqq"); //$1$qqqqqqqq就是你指定的鹽值
System.out.println(s); //$1$qqqqqqqq$AZofg3QwurbxV3KEOzwuI1
③BCrypt加密
Spring家的BCrypt加密,即使明文一樣,每次加密的密文都不一樣,但是你可以匹配明文和密文,人家就會告訴你這兩個匹配與否
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encode = bCryptPasswordEncoder.encode("123456");//$2a$10$GT0TjB5YK5Vx77Y.2N7hkuYZtYAjZjMlE6NWGE2Aar/7pk/Rmhf8S
boolean matches = bCryptPasswordEncoder.matches("123456", "$2a$10$GT0TjB5YK5Vx77Y.2N7hkuYZtYAjZjMlE6NWGE2Aar/7pk/Rmhf8S");
System.out.println(matches);//true
P219 登陸頁面—用戶名密碼登錄
總說:
前臺發送 /login到后臺的gulimall-auth-server模塊中,然后gulimall-auth-server會使用OpenFeign遠程呼叫gulimall-member的login()方法,在該方法中根據用戶名查詢ums_member表拿到MemberEntity然后進行用戶名密碼比對完成登錄,登錄成功重定向到首頁,登陸失敗重定向到登錄頁
九、登陸頁面—完成微博登錄
1.微博登陸的流程
2.有兩個地址很重要
(1)是“ 在登錄頁引導用戶至授權頁”的地址:
這一步是前臺完成的,前臺html中的url要寫成
Get
https://api.weibo.com/oauth2/authorize?client_id=1917008757&response_type=code&redirect_uri=http://gulimall.com/oauth2.0/weibo/success
client_id:是你創建網站應用時的app key,
redirect_uri是用戶使用微博登錄后重定向到哪里去,
我們指定redirect_uri=http://gulimall.com/oauth2.0/weibo/success也就是說用戶用戶使用微博登錄后,相當于發送 /oauth2.0/weibo/success到后臺的gulimall-auth-server模塊中,那么gulimall-auth-server會使用code換取token,這就涉及到換取token的url:
(2)是換取token的url
這一步是后臺完成的,后臺發送這樣的url才能獲取到token
POST
https://api.weibo.com/oauth2/access_token?client_id=1917008757&client_secret=94d9cc62c60d5f9f3d0c62389593024f&grant_type=authorization_code&redirect_uri=http://auth.gulimall.com/oauth2.0/weibo/success&code=CODE
client_id: 創建網站應用時的app key;
client_secret: 創建網站應用時的app secret
redirect_uri: 認證完成后的跳轉鏈接(需要和平臺高級設定一致);
code:換取令牌的認證碼
后臺發送這么個請求就可以根據用戶授權回傳的code換取token(換回來的不僅僅是token,還有uid用戶id、expires_in令牌的過期時間等等,這些被封裝到SocialUser中),拿到SocialUser中的token就可以向微博官方發送別的請求換取用戶資訊
3.微博登陸的具體流程
4.編碼總說:
①前臺帶著code發送
/oauth2.0/weibo/success請求到后臺的gulimall-auth-server模塊中,然后gulimall-auth-server會先使用code換取SocialUser,然后拿著SocialUser到OpenFeign遠程呼叫gulimall-member的oauth2Login()方法,在該方法中會先用SocialUser的uid查詢資料庫來判斷用戶是否是第一次用微博登錄,如果是第一次的話我們就得給該用戶注冊(拿著token到微博里面查詢該用戶的基本資訊,然后insert到咱們的資料庫里面);如果該用戶之前已經用微博登陸過,那就到資料庫中更新一下token,
如果一切順利,gulimall-member就會帶著MemberEntity(封裝著用戶的所有資訊)回傳到gulimall-auth-server,然后gulimall-auth-server會把MemberEntity設定到RedirectAttributes然后重定向到http://gulimall.com;如果不順利就把error資訊回傳到gulimall-auth-server,然后gulimall-auth-server會把error資訊封裝到RedirectAttributes然后重定向到http://auth.gulimall.com/login.html
②前臺用戶用微博登錄后我們會拿到用戶的code,后臺用code到微博里面換取token這樣才能用token訪問到用戶基本資訊;用戶每登陸一次訪問微博的token就會變一次,所以當用戶下次用微博登陸時我們需要到資料庫更新一下token
十、SpringSession—session不共享、不跨域問題
之前我們學過解決跨域問題,
現在是解決的是不同域名下沒辦法共享session問題,共享session干什么?用戶一登陸就被存在session里面了,共享session就可以實作一次登錄處處生效,
我們在auth.gulimall.com登陸成功后把用戶資訊存到session里面,但是登陸成功會跳轉到gulimall.com不是同一個微服務,我們每個微服務都有自己的域名,它們域名不一樣,就沒辦法共享session,
1.session不能跨域問題
2.分布式下session共享問題
多臺服務器都有會員服務,你在A服務器上把用戶資訊保存到記憶體上了,下次如果落在B服務器上,即使瀏覽器帶著cookie來了,由于B服務器記憶體肯定沒有存盤用戶資訊,這也是問題,
3.session共享問題的解決方案
-
session復制
用戶登錄后,A服務器得到session后,把session也復制到別的機器上,顯然這種處理很不好 -
客戶端存盤
把session存盤到瀏覽器上,肯定相當不安全 -
hash一致性
根據用戶,到指定的機器上登錄,但是遠程呼叫還是不好解決 -
redis統一存盤
最終的選擇方案,把session放到redis中,這樣每個微服務都可以獲取到session
4.總說:
瀏覽器會在
auth.gulimall.com里面登錄成功,auth.gulimall.com會將登陸成功的用戶的從資料庫查到的用戶相關資訊存到session里面,而且存session時不是存到自己的記憶體里面而是存到redis里面,然后auth.gulimall.com給瀏覽器發cookie,而且發的cookie的作用域不能僅僅是auth.gulimall.com而是要放大服務到.gulimall.com,此時瀏覽器訪問其它任何服務都會帶上這個cookie,
如果你把redis里面的session清空,那就是把登陸過的用戶資訊清空,雖然前臺的瀏覽器訪問后臺時攜帶了cookie資訊,但是到redis里面查不到用戶資訊,所以你就得重新登陸,而且我們設定了redis里面的session默認30分鐘過期,也就是30分鐘后redis里面的用戶資訊就沒有了
5.修改微博登陸的代碼
①修改sprinsession的存盤型別是redis(這很重要·,以后存到session中就是存到redis中)
spring:
session:
store-type: redis
②增加一個配置類,由于默認使用jdk進行序列化,通過匯入RedisSerializer修改為json序列化,并且通過修改CookieSerializer擴大session的作用域至**.gulimall.com
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("gulimall.com");
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
}
③修改gulimall-auth-server的Controller
以前的邏輯是:
這是微博登陸的代碼,前臺帶著code發送
/oauth2.0/weibo/success請求到后臺的gulimall-auth-server模塊中,然后gulimall-auth-server會先使用code換取SocialUser,然后拿著SocialUser到OpenFeign遠程呼叫gulimall-member的oauth2Login()方法,在該方法中如果是第一次的話我們就得給該用戶注冊;如果該用戶之前已經用微博登陸過,那就到資料庫中更新一下token,
如果一切順利,gulimall-member就會帶著MemberEntity(封裝著用戶的所有資訊)回傳到gulimall-auth-server,然后gulimall-auth-server會把MemberEntity設定到RedirectAttributes然后重定向到http://gulimall.com;如果不順利就把error資訊回傳到gulimall-auth-server,然后gulimall-auth-server會把error資訊封裝到RedirectAttributes然后重定向到http://auth.gulimall.com/login.html
現在的邏輯就是:
gulimall-member就會帶著MemberEntity(封裝著用戶的所有資訊)回傳到gulimall-auth-server,然后gulimall-auth-server會把MemberEntity設定到SpringSession中然后重定向到http://gulimall.com;如果不順利就把error資訊回傳到gulimall-auth-server,然后gulimall-auth-server會把error資訊封裝到RedirectAttributes然后重定向到http://auth.gulimall.com/login.html
6.修改賬號密碼登錄的代碼
修改.auth.gulimall.com的LoginController,目的是賬號密碼登陸的用戶也要存到session里面(我們原來做的作業只是把微博登陸的用戶存到sesson里面)
十一、單點登錄
1.為什么要單點登錄?
在但系統服務中,springsession把auth.gulimall.com作用域放大到gulimall.com,放大作用域就能共享session,但要是多系統情況下,域名完全不一樣,沒辦法通過放大作用域的方式來共享session,這就需要用登錄解決,
共享session干什么?用戶一登陸就被存在session里面了,共享session就可以實作一次登錄處處生效
你在新浪微博(https://weibo.com/)里面注冊登錄了,同時就要保證在新浪體育(https://sports.com/)、新浪新聞(https://news.com/)里面全都可以拿到session資料
2.單點登錄的原理?
兩個域名不一樣的服務端client1和client2,還有一個負責登錄的ssoserver,還有一個瀏覽器,它們四個之間的故事
先說明一下這個路徑的含義:
http://ssoserver.com:8080/login.html?redirect_url=http:I/client1.com:8081/employees的含義就是讓你訪問http://ssoserver.com:8080/login.html登陸頁面,而redirect_url=http:I/client1.com:8081/employees的含義是當你完成登陸后會重定向到http:I/client1.com:8081/employees的位置
第1-11步的決議:只有登陸了才能查看員工資訊,一開始瀏覽器訪問client1.com的員工資訊
http:I/client1.com:8081/employees,client1會根據這個url有沒有token引數判斷是否登錄,由于沒有token引數也就是沒有登陸,服務端會命令瀏覽器重定向到ssoserver.com的登陸頁面http:I/ssoserver.com:8080/login.html?redirect_url=http:I/client1.com:8081/employees,ssoserver.com會判斷是否登陸過,沒有登陸過就展示這個登陸頁面,用戶會輸入賬號密碼進行登錄,提交登陸請求http:/ssoserver.com:8080/doLogin?usermame,password,redirect_url給ssoserver.com,那么ssoserver.com會保存用戶狀態到redis,同時ssoserver.com會命令重定向到http: /lclient1.com:8081/employees?token=dadadadsdeuieu(瀏覽器訪問路徑),同時ssoserver.com會命令瀏覽器保存sso_token=dadadadsdeuieu這樣式的cookie,瀏覽器這次就可以訪問員工資訊了,他的訪問路徑是剛剛提到的http://lclient1.com:8081/employees?token=dadadadsdeuieu比一開始訪問員工資訊的http:I/client1.com:8081/employees多了token=dadadadsdeuieu,這就回到第2步了,client1會根據有沒有token引數判斷是否登錄,這次client1會覺得它登陸過了就可以訪問員工資訊了,
第12-19步決議:這次瀏覽器要訪問客戶端2的boss資訊
http:I/client2.com:8081/boss,client2會根據有沒有token引數判斷是否登錄,由于沒有token引數也就是沒有登陸,服務端會命令瀏覽器重定向到ssoserver.com的登陸頁面http:I/ssoserver.com:8080/login.html?redirect_url=http:I/client2.com:8081/boss,ssoserver.com會判斷是否登陸過,由于瀏覽器有sso_token=dadadadsdeuieu這樣式的cookie,而且從redis能查到,說明它之前在client1或者client2登陸過,ssoserver.com會命令重定向到http:/lclient2.com:8082/boss?token=dadadadsdeuieu,所以瀏覽器就會訪問http://lclient2.com:8082/boss?token=dadadadsdeuieu,這就回到了第2步,client2會根據有沒有token引數判斷是否登錄,登陸過就回應頁面,
所以說,以后瀏覽器無論訪問client1還是client2,由于瀏覽器中保存了cookie,所以ssoserver.com就會判定它登陸過,所以以后都不用登陸,
演示
代碼用的網友的,截屏用到老師的,網友喜歡自己起名字,把token改為redisKey什么的,不要計較細節上的不同
十二、購物車(面試版)
1.添加商品到購物車
1)如果用戶沒有登錄:
情況1:第一次來,那就在瀏覽器種創建一個cookie(user-key),設定cookie的作用域、過期時間
情況2:之前來過,從瀏覽器種獲取到cookie(user-key)
不論是情況1還是情況2,現在都有了user-key了,然后將用戶購物車資訊存到redis中
2)如果用戶已經完成登錄:
按照userId來存到redis中
2.購物車種資料的存盤方式:
本節內容就是說明了用戶購物車里的資訊應該使用哪個資料庫存盤(MySQL還是Redis?),以及使用了Redis后是用List存盤這些資訊呢還是使用Hash存盤這些資訊?以及購物車VO、購物項VO的撰寫
3.展示購物車:
1)如果用戶沒有登錄:
從cookie中獲取user-key,使用user-key從Redis獲取購物車資料
2)如果用戶已經完成登錄:
使用userId從Redis獲取購物車資料,并嘗試從瀏覽器中拿到user-key、查詢Redis中對應臨時購物車資料與用戶購物車資料合并,并洗掉臨時購物車
4.更加細節的東西
面試官要是沒有問道太細節的東西,你也就不用解釋user-key是什么,userId是什么,只會顯得更加亂,他要是問道更加細節的東西,那就用下面的知識:
登錄攔截器:
(登陸攔截器設計也是一個重點,后面得講)
在購物車的所有Controller執行之前,我們先執行一個攔截器,在攔截器里需要區分用戶的三種狀態:1.用戶已登錄 2.用戶未登錄,而且還是第一次來 3.用戶未登錄,而且前兩天已經來過了
“用戶未登錄,而且還是第一次來”這種情況的用戶就需要在瀏覽器中保存一段cookie(user-key),并且設定cookie的過期時間什么的
如果用戶沒有登錄,UserInfoTo中的userId是空的,但userKey不是空的
如果用戶已經登錄,UserInfoTo中的userId不是空的,但userKey是空的
如果UserInfoTo中的userId不是空的,UserInfoTo中的userKey也不是空的,那就說明用戶現在登錄了,但是他之前還沒有登錄的時候也訪問過京東購物車;那么就需要合并購物車了
攔截器執行完后,UserInfoTo中的userId不是空的表示賬號用戶,反之為臨時用戶 ,然后決定用臨時購物車還是用戶購物車,將用戶購物車資訊存到redis中,redis中肯定需要鍵值對,賬號用戶的購物車的redis中的key是
gulimall:cart:1(1是userId);臨時用戶的redis中的key是gulimall:cart:uuid其中uuid就是user-key,
添加到購物車:
添加新商品到購物車,第一步先看redis里面能不能查到skuid,查不到說明購物車里面之前沒有添加過此商品,那就需要遠程查詢此商品的一系列資訊;能查到說明購物車有此商品,將資料取出修改數量即可,
展示購物車:
若用戶未登錄,則直接使用user-key獲取購物車資料;否則使用userId獲取購物車資料,并將user-key對應臨時購物車資料與用戶購物車資料合并,并洗掉臨時購物車
十四、購物車(精細版)
1、關于攔截器
在購物車的所有Controller執行之前,我們先執行一個攔截器,在攔截器里判斷用戶是否登錄,以及用戶是第幾次登錄
一個用戶進來我們執行的 “ 攔截器—Controller—Service—Dao ” 這一套流程讓同一個執行緒執行,這就使用了ThreadLocal技術,ThreadLocal是同一個執行緒共享資料,這個執行緒里面的資料會共享,使用程序就是:
ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();//創建一個threadLocal
...
userInfoTo.setTempUser(true);//標注該用戶是臨時用戶
userInfoTo.setuserkey(uuid);//設定user-key
threadLocal.set(userInfoTo);//把要共享的資料userInfoTo設定進threadlocal里面
....
UserInfoTo userInfoTo = threadLocal.get();//后期就可以獲取到這個共享的資料
UserInfoTo如下:
@ToString
@Data
public class UserInfoTo {
private Long userId;
private String userKey;
private boolean tempUser = false; //這個相當重要,我們會根據tempUser是true還是false來決定有沒有執行postHandle()方法
}
登陸攔截器如下:
/**
* @Description: 在執行目標方法之前,判斷用戶的登錄狀態,并封裝傳遞給目標請求
*/
public class CartInterceptor implements HandlerInterceptor {
//ThreadLocal同一個執行緒共享資料
public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();
/**
* 在目標方法執行之前攔截
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
UserInfoTo userInfoTo = new UserInfoTo();
HttpSession session = request.getSession();
MemberResponseVO member = (MemberResponseVO) session.getAttribute(AuthServerConstant.LOGIN_USER);
if (member != null){
//用戶登錄
userInfoTo.setUserId(member.getId());
}
//用戶沒有登陸:
Cookie[] cookies = request.getCookies();
if (cookies!=null && cookies.length >0){
//有臨時用戶資訊
for (Cookie cookie : cookies) {
String name = cookie.getName();
if (name.equals(CartConstant.TEMP_USER_COOKIE_NAME)){
userInfoTo.setUserKey(cookie.getValue());
userInfoTo.setTempUser(true);
}
}
}
//用戶沒有登陸,而且沒有臨時用戶資訊,一定保存一個臨時用戶
if (StringUtils.isEmpty(userInfoTo.getUserKey())){
String uuid = UUID.randomUUID().toString();
userInfoTo.setUserKey(uuid);
}
//userInfoTo存到threadLocal中
threadLocal.set(userInfoTo);
return true;
}
/**
* 業務執行之后 分配臨時用戶,讓瀏覽器保存
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
UserInfoTo userInfoTo = threadLocal.get();
//如果沒有臨時用戶,第一次訪問購物車就添加臨時用戶
if (!userInfoTo.isTempUser()){
//持續的延長用戶的過期時間
Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
cookie.setDomain("gulimall.com");
cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
response.addCookie(cookie);
}
}
}
請你假設一下以下三種情況在攔截器中會發生什么:
如果用戶沒有登錄,而且瀏覽器中沒有用戶的臨時資訊:UserInfoTo中的userId是空的,但userKey不是空的
如果用戶沒有登錄,但是瀏覽器中有用戶的臨時資訊:UserInfoTo中的userId是空的,但userKey不是空的
如果用戶已經登錄,UserInfoTo中的userId不是空的,但userKey是空的
登錄Controller如下:
@Controller
public class CartController {
/**
* 登錄 session有
* 沒登錄,按照cookie里面帶來的user-key來做
* 第一次,如果沒有臨時用戶,幫忙創建一個臨時用戶
*/
@GetMapping("/cart.html")
public String cartListPage(){
//快速得到用戶資訊,id,user-key
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
System.out.println(userInfoTo);
return "cartList";
}
}
2、添加商品到購物車
getCartOps()方法里面邏輯:
因為是攔截器先執行的,所以先得到攔截器ThreadLocal的回傳結果
UserInfoTo userInfoTo = threadLocal.get(),如果userInfoTo.getUserId()不為空表示賬號用戶,反之為臨時用戶 ,然后決定用臨時購物車還是用戶購物車,將用戶購物車資訊存到redis中,redis中肯定需要鍵值對,賬號用戶的購物車的redis中的key是gulimall:cart:1(1是用戶id,表示1號用戶的購物車);臨時用戶的redis中的key是gulimall:cart:uuid其中uuid就是我們攔截器里存下的user-key, redisTemplate.boundHashOps(cartKey)是說以后所有對redis的增刪改查都是針對redia中key為cartKey的增刪改查,
addToCart()方法里面的邏輯:
添加新商品到購物車,第一步先看redis里面能不能查到skuid,查不到說明購物車里面之前沒有添加過此商品,那就需要遠程查詢此商品的一系列資訊;能查到說明購物車有此商品,將資料取出修改數量即可,
7.測驗:
3、獲取購物車
若用戶未登錄,則直接使用user-key獲取購物車資料;否則使用userId獲取購物車資料,并將user-key對應臨時購物車資料與用戶購物車資料合并,并洗掉臨時購物車
4、選中購物車項
5、修改購物項數量
6、洗掉購物項
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/300715.html
標籤:其他










































