柚子快報邀請碼778899分享:數(shù)據(jù)庫 緩存 Redis實戰(zhàn)
柚子快報邀請碼778899分享:數(shù)據(jù)庫 緩存 Redis實戰(zhàn)
短信登錄功能
發(fā)送短信驗證碼實現(xiàn)流程
提交手機(jī)號校驗手機(jī)號生成驗證碼,并保存保存驗證碼到redis發(fā)送驗證碼
@Override
public Result sendCode(String phone) {
//校驗手機(jī)號
if(RegexUtils.isPhoneInvalid(phone)){
return Result.fail(UserErrorConstant.PHONE_FORMAT_ERROR);
}
//生成驗證碼
String code = RandomUtil.randomNumbers(6);
//保存驗證碼(有效期:5min)
stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY +phone,code,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
//發(fā)送驗證碼
log.debug("發(fā)送短信驗證碼成功,驗證碼:{}",code);
return Result.ok();
}
短信驗證碼登錄注冊實現(xiàn)流程
提交手機(jī)號和驗證碼驗證驗證碼根據(jù)手機(jī)號查詢用戶用戶是否存在(如果不存在創(chuàng)建新用戶并保存用戶到數(shù)據(jù)庫)保存用戶到redis
@Override
public Result loginByCode(LoginFormDTO loginFormDTO) {
//校驗手機(jī)號
String phone = loginFormDTO.getPhone();
if(RegexUtils.isPhoneInvalid(phone)){
return Result.fail(UserErrorConstant.PHONE_FORMAT_ERROR);
}
//從redis獲取驗證碼并驗證
String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY+phone);
String code = loginFormDTO.getCode();
if(cacheCode==null||!cacheCode.equals(code)){
return Result.fail(UserErrorConstant.VERIFICATION_CODE_ERROR);
}
//根據(jù)手機(jī)號查詢用戶
User user = query().eq("mobile",phone).one();
//判斷用戶是否存在
if(user==null){
user = createUserWithPhone(phone);
}
//使用jwt令牌
String token = UUID.randomUUID().toString();
//將user對象轉(zhuǎn)成HashMap存儲(由于使用的是stringRedisTemplate,所以要把id轉(zhuǎn)為string)
UserDTO userDTO = BeanUtil.copyProperties(user,UserDTO.class);
userDTO.setToken(token);
Map
CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue == null ? "" : fieldValue.toString()));
stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY+token,userMap);
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token,RedisConstants.LOGIN_USER_TTL,TimeUnit.HOURS);
//返回token
return Result.ok(userDTO);
}
校驗登錄狀態(tài)實現(xiàn)流程
請求并攜帶cookie從redis獲取用戶判斷用戶是否存在(如果不存在則攔截)如果存在則保存用戶到threadlocal
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//從請求頭中獲取token
String token = request.getHeader("authorization");
if (StringUtils.isEmpty(token)) {
//不存在token
return true;
}
//從redis中獲取用戶
Map
stringRedisTemplate.opsForHash()
.entries(RedisConstants.LOGIN_USER_KEY + token);
//用戶不存在
if (userMap.isEmpty()) {
return true;
}
//hash轉(zhuǎn)UserDTO存入ThreadLocal
UserHolder.saveUser(BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false));
//token續(xù)命
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
緩存功能
緩存就是數(shù)據(jù)交換的緩沖區(qū)(Cache),是存儲數(shù)據(jù)的臨時地方,一般讀寫性能較高。
作用:
降低后端負(fù)載。提高讀寫效率,降低響應(yīng)時間。(解決高并發(fā)問題)
成本:
數(shù)據(jù)一致性成本代碼維護(hù)成本(緩存擊穿)運(yùn)維成本
緩存更新策略
內(nèi)存淘汰:利用redis的內(nèi)存淘汰機(jī)制,內(nèi)存不足時自動淘汰部分?jǐn)?shù)據(jù)。
超時剔除:添加ttl時間,到期自動刪除緩存(兜底)。
主動更新:在修改數(shù)據(jù)庫的同時,更新緩存。
方案1:由緩存的調(diào)用者,在更新數(shù)據(jù)庫的同時更新緩存。方案2:緩存與數(shù)據(jù)庫整合為一個服務(wù),由服務(wù)來維護(hù)一致性。方案3:調(diào)用者只操作緩存,由其他線程異步的將緩存數(shù)據(jù)持久化到數(shù)據(jù)庫,保證最終一致。
如何保證緩存與數(shù)據(jù)庫的操作的同時成功或失?。ㄔ硬僮鲉栴})?
單體系統(tǒng),將緩存與數(shù)據(jù)庫操作放在一個事務(wù)。分布式系統(tǒng),利用tcc等分布式事務(wù)方案。
先操作緩存還是先操作數(shù)據(jù)庫(線程安全)?
先操作數(shù)據(jù)庫再刪除緩存的操作更保險,因為數(shù)據(jù)庫的操作時間一般要比緩存慢。
緩存穿透
緩存穿透是指客戶端請求的數(shù)據(jù)在緩存中和數(shù)據(jù)庫中都不存在,這樣緩存永遠(yuǎn)不會生效,這些請求都會達(dá)到數(shù)據(jù)庫。
緩存空對象(被動方案)
優(yōu)點:實現(xiàn)簡單,維護(hù)方便
缺點:額外的內(nèi)存消耗(緩存垃圾,設(shè)置ttl刪除)、可能造成短期的不一致(插入時更新緩存)
布隆過濾(被動方案)
在客戶端和redis之間加入布隆過濾器,如果不存在則拒絕,存在則放行。(用byte數(shù)組存儲,hashcode二進(jìn)制hash對應(yīng)位置,若為1存在,若為0不存在)
優(yōu)點:內(nèi)存占用少,沒有多余key
缺點:實現(xiàn)復(fù)雜,存在誤判可能
主動方案
增加id的復(fù)雜度,避免被猜測id規(guī)律(雪花算法)做好數(shù)據(jù)的基礎(chǔ)格式校驗加強(qiáng)用戶權(quán)限校驗做好熱點參數(shù)的限流
/**
* 緩存穿透
* @param id
* @return
*/
public Result queryWithPassThrough(int id){
String key = RedisConstants.CACHE_SHOP_KEY+id;
//從redis查詢企業(yè)用戶信息緩存
String userJson = stringRedisTemplate.opsForValue().get(key);
//判斷是否存在
if(StrUtil.isNotBlank(userJson)){
User user = JSONUtil.toBean(userJson,User.class);
return Result.ok(user);
}
//判斷命中的是否是空值
if(userJson!=null){
//返回錯誤信息
return Result.fail(UserErrorConstant.USER_NOEXIST_ERROR);
}
//不存在,根據(jù)id查詢數(shù)據(jù)庫
User user = userMapper.queryUserById(id);
//不存在,返回錯誤
if(user==null){
//緩存穿透-》將空值寫入redis
stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
return Result.fail(UserErrorConstant.USER_NOEXIST_ERROR);
}
//存儲到redis中,設(shè)置了超時時間
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(user),RedisConstants.CACHE_SHOP_TTL,TimeUnit.HOURS);
return Result.ok(user);
}
緩存雪崩
緩存雪崩是指同一時段大量的緩存key同時失效或者redis服務(wù)宕機(jī),導(dǎo)致大量請求到達(dá)數(shù)據(jù)庫,帶來巨大壓力。
解決方案
給不同的key的ttl添加隨機(jī)值(同一時間段大量key失效,有時候在做緩存的預(yù)熱時會在同一時間批量的數(shù)據(jù)導(dǎo)入)利用redis集群提高服務(wù)的可用性(主宕機(jī),從結(jié)點上還有數(shù)據(jù))給緩存業(yè)務(wù)添加降級限流策略(提前做好服務(wù)降級,比如快速失敗拒絕服務(wù))給業(yè)務(wù)添加多級緩存
緩存擊穿
緩存擊穿問題也叫熱點key問題,就是一個被高并發(fā)訪問并且緩存重建業(yè)務(wù)較復(fù)雜的key突然失效了,無數(shù)的請求訪問會在瞬間給數(shù)據(jù)庫帶來巨大的沖擊。(無數(shù)請求緩存重建)
解決方案
互斥鎖
未命中需要獲取鎖才能查詢數(shù)據(jù)庫重建緩存數(shù)據(jù),寫入緩存后再釋放鎖。
優(yōu)點:沒有額外內(nèi)存消耗,保證數(shù)據(jù)的一致性,實現(xiàn)簡單。
缺點:線程需要等待,性能受影響,可能有死鎖風(fēng)險。
/**
* 緩存擊穿->互斥鎖
* @param id
* @return
*/
public Result queryWithPassMutex(int id) {
String key = RedisConstants.CACHE_SHOP_KEY+id;
//從redis查詢企業(yè)用戶信息緩存
String userJson = stringRedisTemplate.opsForValue().get(key);
//判斷是否存在
if(StrUtil.isNotBlank(userJson)){
User user = JSONUtil.toBean(userJson,User.class);
return Result.ok(user);
}
//判斷命中的是否是空值
if(userJson!=null){
//返回錯誤信息
return Result.fail(UserErrorConstant.USER_NOEXIST_ERROR);
}
//緩存重建
String lockKey = RedisConstants.LOCK_SHOP_KEY+id;
User user =null;
try {
boolean isLock = tryLock(lockKey);
if(!isLock){
Thread.sleep(50);
//風(fēng)險!改成while(true)輪詢就好
return queryWithPassMutex(id);
}
//不存在,根據(jù)id查詢數(shù)據(jù)庫
user = userMapper.queryUserById(id);
//不存在,返回錯誤
if(user==null){
//緩存穿透-》將空值寫入redis
stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
return Result.fail(UserErrorConstant.USER_NOEXIST_ERROR);
}
//存儲到redis中,設(shè)置了超時時間
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(user),RedisConstants.CACHE_SHOP_TTL,TimeUnit.HOURS);
} catch (InterruptedException e){
throw new RuntimeException(e);
} finally {
//釋放互斥鎖
unlock(lockKey);
}
return Result.ok(user);
}
邏輯過期
發(fā)現(xiàn)邏輯時間已過期,未命中獲取互斥鎖,返回過期數(shù)據(jù),開啟新線程去做緩存重建并釋放鎖,如果有新線程來的時候還是會返回新數(shù)據(jù)。
優(yōu)點:線程無需等待,性能較好。
缺點:不保證一致性,有額外內(nèi)存消耗,實現(xiàn)復(fù)雜。
/**
* 緩存穿透
* @param id
* @return
*/
public Result queryWithPassThrough(int id){
String key = RedisConstants.CACHE_SHOP_KEY+id;
//從redis查詢企業(yè)用戶信息緩存
String userJson = stringRedisTemplate.opsForValue().get(key);
//判斷是否存在
if(StrUtil.isNotBlank(userJson)){
User user = JSONUtil.toBean(userJson,User.class);
return Result.ok(user);
}
//判斷命中的是否是空值
if(userJson!=null){
//返回錯誤信息
return Result.fail(UserErrorConstant.USER_NOEXIST_ERROR);
}
//不存在,根據(jù)id查詢數(shù)據(jù)庫
User user = userMapper.queryUserById(id);
//不存在,返回錯誤
if(user==null){
//緩存穿透-》將空值寫入redis
stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
return Result.fail(UserErrorConstant.USER_NOEXIST_ERROR);
}
//存儲到redis中,設(shè)置了超時時間
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(user),RedisConstants.CACHE_SHOP_TTL,TimeUnit.HOURS);
return Result.ok(user);
}
柚子快報邀請碼778899分享:數(shù)據(jù)庫 緩存 Redis實戰(zhàn)
推薦鏈接
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點和立場。
轉(zhuǎn)載請注明,如有侵權(quán),聯(lián)系刪除。