国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】

這篇具有很好參考價(jià)值的文章主要介紹了仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問(wèn)。

? 作者主頁(yè):歡迎來(lái)到我的技術(shù)博客??
? 個(gè)人介紹:大家好,本人熱衷于Java后端開(kāi)發(fā),歡迎來(lái)交流學(xué)習(xí)哦!( ̄▽?zhuān)?~*
?? 如果文章對(duì)您有幫助,記得關(guān)注、點(diǎn)贊收藏、評(píng)論??????
?? 您的支持將是我創(chuàng)作的動(dòng)力,讓我們一起加油進(jìn)步吧?。?!????

一、短信登錄

1.1 基于Session實(shí)現(xiàn)登錄流程

發(fā)送驗(yàn)證碼:

用戶(hù)在提交手機(jī)號(hào)后,會(huì)校驗(yàn)手機(jī)號(hào)是否合法,如果不合法,則要求用戶(hù)重新輸入手機(jī)號(hào)。

如果手機(jī)號(hào)合法,后臺(tái)此時(shí)生成對(duì)應(yīng)的驗(yàn)證碼,同時(shí)將驗(yàn)證碼進(jìn)行保存,然后再通過(guò)短信的方式將驗(yàn)證碼發(fā)送給用戶(hù)。

短信驗(yàn)證碼登錄、注冊(cè):

用戶(hù)將驗(yàn)證碼和手機(jī)號(hào)進(jìn)行輸入,后臺(tái)從session中拿到當(dāng)前驗(yàn)證碼,然后和用戶(hù)輸入的驗(yàn)證碼進(jìn)行校驗(yàn),如果不一致,則無(wú)法通過(guò)校驗(yàn),如果一致,則后臺(tái)根據(jù)手機(jī)號(hào)查詢(xún)用戶(hù),如果用戶(hù)不存在,則為用戶(hù)創(chuàng)建賬號(hào)信息,保存到數(shù)據(jù)庫(kù),無(wú)論是否存在,都會(huì)將用戶(hù)信息保存到session中,方便后續(xù)獲得當(dāng)前登錄信息。

校驗(yàn)登錄狀態(tài):

用戶(hù)在請(qǐng)求時(shí)候,會(huì)從cookie中攜帶者JsessionId到后臺(tái),后臺(tái)通過(guò)JsessionId從session中拿到用戶(hù)信息,如果沒(méi)有session信息,則進(jìn)行攔截,如果有session信息,則將用戶(hù)信息保存到threadLocal中,并且放行。
仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略


1.2 實(shí)現(xiàn)發(fā)送短信驗(yàn)證功能

頁(yè)面流程
仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略

  • 發(fā)送驗(yàn)證碼
    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1.校驗(yàn)手機(jī)號(hào)
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回錯(cuò)誤信息
            return Result.fail("手機(jī)號(hào)格式錯(cuò)誤!");
        }
        // 3.符合,生成驗(yàn)證碼
        String code = RandomUtil.randomNumbers(6);

        // 4.保存驗(yàn)證碼到 session
        session.setAttribute("code",code);
        // 5.發(fā)送驗(yàn)證碼
        log.debug("發(fā)送短信驗(yàn)證碼成功,驗(yàn)證碼:{}", code);
        // 返回ok
        return Result.ok();
    }
  • 登錄
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1.校驗(yàn)手機(jī)號(hào)
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回錯(cuò)誤信息
            return Result.fail("手機(jī)號(hào)格式錯(cuò)誤!");
        }
        // 3.校驗(yàn)驗(yàn)證碼
        Object cacheCode = session.getAttribute("code");
        String code = loginForm.getCode();
        if(cacheCode == null || !cacheCode.toString().equals(code)){
             //3.不一致,報(bào)錯(cuò)
            return Result.fail("驗(yàn)證碼錯(cuò)誤");
        }
        //一致,根據(jù)手機(jī)號(hào)查詢(xún)用戶(hù)
        User user = query().eq("phone", phone).one();

        //5.判斷用戶(hù)是否存在
        if(user == null){
            //不存在,則創(chuàng)建
            user =  createUserWithPhone(phone);
        }
        //7.保存用戶(hù)信息到session中
        session.setAttribute("user",user);

        return Result.ok();
    }

1.3 實(shí)現(xiàn)登錄攔截功能

仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略

  • 攔截器代碼
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
       //1.獲取session
        HttpSession session = request.getSession();
        //2.獲取session中的用戶(hù)
        Object user = session.getAttribute("user");
        //3.判斷用戶(hù)是否存在
        if(user == null){
              //4.不存在,攔截,返回401狀態(tài)碼
              response.setStatus(401);
              return false;
        }
        //5.存在,保存用戶(hù)信息到Threadlocal
        UserHolder.saveUser((User)user);
        //6.放行
        return true;
    }
}
  • 讓攔截器生效
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登錄攔截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
        // token刷新的攔截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

1.4 隱藏用戶(hù)敏感信息

我們通過(guò)瀏覽器觀察到此時(shí)用戶(hù)的全部信息都在,這樣極為不靠譜,所以我們應(yīng)當(dāng)在返回用戶(hù)信息之前,將用戶(hù)的敏感信息進(jìn)行隱藏,采用的核心思路就是書(shū)寫(xiě)一個(gè)UserDto對(duì)象,這個(gè)UserDto對(duì)象就沒(méi)有敏感信息了,我們?cè)诜祷厍埃瑢⒂杏脩?hù)敏感信息的User對(duì)象轉(zhuǎn)化成沒(méi)有敏感信息的UserDto對(duì)象,那么就能夠避免這個(gè)尷尬的問(wèn)題了。

在登錄方法處修改

//7.保存用戶(hù)信息到session中
session.setAttribute("user", BeanUtils.copyProperties(user,UserDTO.class));

在攔截器處:

//5.存在,保存用戶(hù)信息到Threadlocal
UserHolder.saveUser((UserDTO) user);

在UserHolder處:將user對(duì)象換成UserDTO

public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    public static UserDTO getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}

1.5 session共享問(wèn)題

每個(gè)tomcat中都有一份屬于自己的session,假設(shè)用戶(hù)第一次訪(fǎng)問(wèn)第一臺(tái)tomcat,并且把自己的信息存放到第一臺(tái)服務(wù)器的session中,但是第二次這個(gè)用戶(hù)訪(fǎng)問(wèn)到了第二臺(tái)tomcat,那么在第二臺(tái)服務(wù)器上,肯定沒(méi)有第一臺(tái)服務(wù)器存放的session,所以此時(shí) 整個(gè)登錄攔截功能就會(huì)出現(xiàn)問(wèn)題,我們能如何解決這個(gè)問(wèn)題呢?早期的方案是session拷貝,就是說(shuō)雖然每個(gè)tomcat上都有不同的session,但是每當(dāng)任意一臺(tái)服務(wù)器的session修改時(shí),都會(huì)同步給其他的Tomcat服務(wù)器的session,這樣的話(huà),就可以實(shí)現(xiàn)session的共享了。

但是這種方案具有兩個(gè)大問(wèn)題:

  1. 每臺(tái)服務(wù)器中都有完整的一份session數(shù)據(jù),服務(wù)器壓力過(guò)大。

  2. session拷貝數(shù)據(jù)時(shí),可能會(huì)出現(xiàn)延遲。

所以咱們后來(lái)采用的方案都是基于redis來(lái)完成,我們把session換成redis,redis數(shù)據(jù)本身就是共享的,就可以避免session共享的問(wèn)題了。

仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略


1.6 Redis代替session的業(yè)務(wù)流程

1.6.1 設(shè)計(jì)key的結(jié)構(gòu)

首先我們要思考一下利用redis來(lái)存儲(chǔ)數(shù)據(jù),那么到底使用哪種結(jié)構(gòu)呢?由于存入的數(shù)據(jù)比較簡(jiǎn)單,我們可以考慮使用String,或者是使用哈希,如下圖,如果使用String,同學(xué)們注意他的value,用多占用一點(diǎn)空間,如果使用哈希,則他的value中只會(huì)存儲(chǔ)他數(shù)據(jù)本身,如果不是特別在意內(nèi)存,其實(shí)使用String就可以啦。

仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略


1.6.2 設(shè)計(jì)key的具體細(xì)節(jié)

所以我們可以使用String結(jié)構(gòu),就是一個(gè)簡(jiǎn)單的key,value鍵值對(duì)的方式,但是關(guān)于key的處理,session他是每個(gè)用戶(hù)都有自己的session,但是redis的key是共享的,咱們就不能使用code了。

在設(shè)計(jì)這個(gè)key的時(shí)候,我們之前講過(guò)需要滿(mǎn)足兩點(diǎn):

  • key要具有唯一性

  • key要方便攜帶

如果我們采用phone:手機(jī)號(hào)這個(gè)的數(shù)據(jù)來(lái)存儲(chǔ)當(dāng)然是可以的,但是如果把這樣的敏感數(shù)據(jù)存儲(chǔ)到redis中并且從頁(yè)面中帶過(guò)來(lái)畢竟不太合適,所以我們?cè)诤笈_(tái)生成一個(gè)隨機(jī)串token,然后讓前端帶來(lái)這個(gè)token就能完成我們的整體邏輯了。


1.6.3 整體訪(fǎng)問(wèn)流程

當(dāng)注冊(cè)完成后,用戶(hù)去登錄會(huì)去校驗(yàn)用戶(hù)提交的手機(jī)號(hào)和驗(yàn)證碼,是否一致,如果一致,則根據(jù)手機(jī)號(hào)查詢(xún)用戶(hù)信息,不存在則新建,最后將用戶(hù)數(shù)據(jù)保存到redis,并且生成token作為redis的key,當(dāng)我們校驗(yàn)用戶(hù)是否登錄時(shí),會(huì)去攜帶著token進(jìn)行訪(fǎng)問(wèn),從redis中取出token對(duì)應(yīng)的value,判斷是否存在這個(gè)數(shù)據(jù),如果沒(méi)有則攔截,如果存在則將其保存到threadLocal中,并且放行。

仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略


1.7 基于Redis實(shí)現(xiàn)短信登錄

UserServiceImpl代碼

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1.校驗(yàn)手機(jī)號(hào)
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 2.如果不符合,返回錯(cuò)誤信息
        return Result.fail("手機(jī)號(hào)格式錯(cuò)誤!");
    }
    // 3.從redis獲取驗(yàn)證碼并校驗(yàn)
    String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
    String code = loginForm.getCode();
    if (cacheCode == null || !cacheCode.equals(code)) {
        // 不一致,報(bào)錯(cuò)
        return Result.fail("驗(yàn)證碼錯(cuò)誤");
    }

    // 4.一致,根據(jù)手機(jī)號(hào)查詢(xún)用戶(hù) select * from tb_user where phone = ?
    User user = query().eq("phone", phone).one();

    // 5.判斷用戶(hù)是否存在
    if (user == null) {
        // 6.不存在,創(chuàng)建新用戶(hù)并保存
        user = createUserWithPhone(phone);
    }

    // 7.保存用戶(hù)信息到 redis中
    // 7.1.隨機(jī)生成token,作為登錄令牌
    String token = UUID.randomUUID().toString(true);
    // 7.2.將User對(duì)象轉(zhuǎn)為HashMap存儲(chǔ)
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
            CopyOptions.create()
                    .setIgnoreNullValue(true)
                    .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
    // 7.3.存儲(chǔ)
    String tokenKey = LOGIN_USER_KEY + token;
    stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
    // 7.4.設(shè)置token有效期
    stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);

    // 8.返回token
    return Result.ok(token);
}

1.8 解決狀態(tài)登錄刷新問(wèn)題

1.8.1 初始方案思路總結(jié)

在這個(gè)方案中,他確實(shí)可以使用對(duì)應(yīng)路徑的攔截,同時(shí)刷新登錄token令牌的存活時(shí)間,但是現(xiàn)在這個(gè)攔截器他只是攔截需要被攔截的路徑,假設(shè)當(dāng)前用戶(hù)訪(fǎng)問(wèn)了一些不需要攔截的路徑,那么這個(gè)攔截器就不會(huì)生效,所以此時(shí)令牌刷新的動(dòng)作實(shí)際上就不會(huì)執(zhí)行,所以這個(gè)方案他是存在問(wèn)題的。

仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略


1.8.2 優(yōu)化方案

既然之前的攔截器無(wú)法對(duì)不需要攔截的路徑生效,那么我們可以添加一個(gè)攔截器,在第一個(gè)攔截器中攔截所有的路徑,把第二個(gè)攔截器做的事情放入到第一個(gè)攔截器中,同時(shí)刷新令牌,因?yàn)榈谝粋€(gè)攔截器有了threadLocal的數(shù)據(jù),所以此時(shí)第二個(gè)攔截器只需要判斷攔截器中的user對(duì)象是否存在即可,完成整體刷新功能。

仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略


1.8.3 代碼實(shí)現(xiàn)

RefreshTokenInterceptor

public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.獲取請(qǐng)求頭中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            return true;
        }
        // 2.基于TOKEN獲取redis中的用戶(hù)
        String key  = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3.判斷用戶(hù)是否存在
        if (userMap.isEmpty()) {
            return true;
        }
        // 5.將查詢(xún)到的hash數(shù)據(jù)轉(zhuǎn)為UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.存在,保存用戶(hù)信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7.刷新token有效期
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用戶(hù)
        UserHolder.removeUser();
    }
}
	

LoginInterceptor

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.判斷是否需要攔截(ThreadLocal中是否有用戶(hù))
        if (UserHolder.getUser() == null) {
            // 沒(méi)有,需要攔截,設(shè)置狀態(tài)碼
            response.setStatus(401);
            // 攔截
            return false;
        }
        // 有用戶(hù),則放行
        return true;
    }
}

二、商戶(hù)查詢(xún)緩存

2.1 什么是緩存

緩存(Cache),就是數(shù)據(jù)交換的緩沖區(qū),俗稱(chēng)的緩存就是緩沖區(qū)內(nèi)的數(shù)據(jù),一般從數(shù)據(jù)庫(kù)中獲取,存儲(chǔ)于本地代碼。

2.1.1 為什么要使用緩存

一句話(huà):因?yàn)?strong>速度快,好用

緩存數(shù)據(jù)存儲(chǔ)于代碼中,而代碼運(yùn)行在內(nèi)存中,內(nèi)存的讀寫(xiě)性能遠(yuǎn)高于磁盤(pán),緩存可以大大降低 用戶(hù)訪(fǎng)問(wèn)并發(fā)量帶來(lái)的 服務(wù)器讀寫(xiě)壓力。

實(shí)際開(kāi)發(fā)過(guò)程中,企業(yè)的數(shù)據(jù)量,少則幾十萬(wàn),多則幾千萬(wàn),這么大數(shù)據(jù)量,如果沒(méi)有緩存來(lái)作為"避震器",系統(tǒng)是幾乎撐不住的,所以企業(yè)會(huì)大量運(yùn)用到緩存技術(shù);

但是緩存也會(huì)增加代碼復(fù)雜度和運(yùn)營(yíng)的成本:

仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略


2.1.2 如何使用緩存

實(shí)際開(kāi)發(fā)中,會(huì)構(gòu)筑多級(jí)緩存來(lái)使系統(tǒng)運(yùn)行速度進(jìn)一步提升,例如:本地緩存與redis中的緩存并發(fā)使用。

  • 瀏覽器緩存:主要是存在于瀏覽器端的緩存

  • 應(yīng)用層緩存: 可以分為tomcat本地緩存,比如之前提到的map,或者是使用redis作為緩存。

  • 數(shù)據(jù)庫(kù)緩存: 在數(shù)據(jù)庫(kù)中有一片空間是 buffer pool,增改查數(shù)據(jù)都會(huì)先加載到mysql的緩存中。

  • CPU緩存: 當(dāng)代計(jì)算機(jī)最大的問(wèn)題是 cpu性能提升了,但內(nèi)存讀寫(xiě)速度沒(méi)有跟上,所以為了適應(yīng)當(dāng)下的情況,增加了cpu的L1,L2,L3級(jí)的緩存。

仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略


2.2 添加客戶(hù)緩存

在我們查詢(xún)商戶(hù)信息時(shí),我們是直接操作從數(shù)據(jù)庫(kù)中去進(jìn)行查詢(xún)的,大致邏輯是這樣,直接查詢(xún)數(shù)據(jù)庫(kù)那肯定慢咯,所以我們需要增加緩存:

@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
    //這里是直接查詢(xún)數(shù)據(jù)庫(kù)
    return shopService.queryById(id);
}

2.2.1 緩存模型和思路

標(biāo)準(zhǔn)的操作方式就是查詢(xún)數(shù)據(jù)庫(kù)之前先查詢(xún)緩存,如果緩存數(shù)據(jù)存在,則直接從緩存中返回,如果緩存數(shù)據(jù)不存在,再查詢(xún)數(shù)據(jù)庫(kù),然后將數(shù)據(jù)存入redis。
仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略


2.2.2. 代碼實(shí)現(xiàn)

代碼思路:如果緩存有,則直接返回,如果緩存不存在,則查詢(xún)數(shù)據(jù)庫(kù),然后存入redis。

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryById(Long id) {
        String key = "cache:shop:" + id; //給每一個(gè)店鋪設(shè)置一個(gè)key,以便于區(qū)別
        //1.從redis查詢(xún)商品緩存
        String shopJson = stringRedisTemplate.opsForValue().get(key);

        //2.判斷是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3.存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }

        //4.不存在,根據(jù)id查詢(xún)數(shù)據(jù)庫(kù)
        Shop shop = getById(id);

        //5.不存在,返回錯(cuò)誤
        if (shop == null) {
            return Result.fail("店鋪不存在!");
        }

        //6.存在,寫(xiě)入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));

        //7.返回
        return Result.ok(shop);

    }

2.3 緩存更新策略

緩存更新是redis為了節(jié)約內(nèi)存而設(shè)計(jì)出來(lái)的一個(gè)東西,主要是因?yàn)閮?nèi)存數(shù)據(jù)寶貴,當(dāng)我們向redis插入太多數(shù)據(jù),此時(shí)就可能會(huì)導(dǎo)致緩存中的數(shù)據(jù)過(guò)多,所以redis會(huì)對(duì)部分?jǐn)?shù)據(jù)進(jìn)行更新,或者把他叫為淘汰更合適。

  • 內(nèi)存淘汰: redis自動(dòng)進(jìn)行,當(dāng)redis內(nèi)存達(dá)到咱們?cè)O(shè)定的max-memery的時(shí)候,會(huì)自動(dòng)觸發(fā)淘汰機(jī)制,淘汰掉一些不重要的數(shù)據(jù)(可以自己設(shè)置策略方式)。

  • 超時(shí)剔除: 當(dāng)我們給redis設(shè)置了過(guò)期時(shí)間ttl之后,redis會(huì)將超時(shí)的數(shù)據(jù)進(jìn)行刪除,方便咱們繼續(xù)使用緩存。

  • 主動(dòng)更新: 我們可以手動(dòng)調(diào)用方法把緩存刪掉,通常用于解決緩存和數(shù)據(jù)庫(kù)不一致問(wèn)題。

仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略


2.3.1 數(shù)據(jù)庫(kù)緩存不一致解決方案

由于我們的 緩存的數(shù)據(jù)源來(lái)自于數(shù)據(jù)庫(kù) ,而數(shù)據(jù)庫(kù)的 數(shù)據(jù)是會(huì)發(fā)生變化的 ,因此,如果當(dāng)數(shù)據(jù)庫(kù)中 數(shù)據(jù)發(fā)生變化,而緩存卻沒(méi)有同步 ,此時(shí)就會(huì)有 一致性問(wèn)題存在 ,其后果是:

用戶(hù)使用緩存中的過(guò)時(shí)數(shù)據(jù),就會(huì)產(chǎn)生類(lèi)似多線(xiàn)程數(shù)據(jù)安全問(wèn)題,從而影響業(yè)務(wù),產(chǎn)品口碑等;怎么解決呢?有如下幾種方案:

  • Cache Aside Pattern 人工編碼方式:緩存調(diào)用者在更新完數(shù)據(jù)庫(kù)后再去更新緩存,也稱(chēng)之為雙寫(xiě)方案。

  • Read/Write Through Pattern : 由系統(tǒng)本身完成,數(shù)據(jù)庫(kù)與緩存的問(wèn)題交由系統(tǒng)本身去處理。

  • Write Behind Caching Pattern:調(diào)用者只操作緩存,其他線(xiàn)程去異步處理數(shù)據(jù)庫(kù),實(shí)現(xiàn)最終一致。

仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略


2.3.2 數(shù)據(jù)庫(kù)與緩存不一致采用什么方案

綜合考慮使用方案一,但是方案一調(diào)用者如何處理呢?這里有幾個(gè)問(wèn)題

操作緩存和數(shù)據(jù)庫(kù)時(shí)有三個(gè)問(wèn)題需要考慮:

如果采用第一個(gè)方案,那么假設(shè)我們每次操作數(shù)據(jù)庫(kù)后,都操作緩存,但是中間如果沒(méi)有人查詢(xún),那么這個(gè)更新動(dòng)作實(shí)際上只有最后一次生效,中間的更新動(dòng)作意義并不大,我們可以把緩存刪除,等待再次查詢(xún)時(shí),將緩存中的數(shù)據(jù)加載出來(lái)

  • 刪除緩存還是更新緩存?

    • 更新緩存:每次更新數(shù)據(jù)庫(kù)都更新緩存,無(wú)效寫(xiě)操作較多
    • 刪除緩存:更新數(shù)據(jù)庫(kù)時(shí)讓緩存失效,查詢(xún)時(shí)再更新緩存
  • 如何保證緩存與數(shù)據(jù)庫(kù)的操作的同時(shí)成功或失敗?

    • 單體系統(tǒng),將緩存與數(shù)據(jù)庫(kù)操作放在一個(gè)事務(wù)
    • 分布式系統(tǒng),利用TCC等分布式事務(wù)方案

應(yīng)該具體操作緩存還是操作數(shù)據(jù)庫(kù),我們應(yīng)當(dāng)是先操作數(shù)據(jù)庫(kù),再刪除緩存,原因在于,如果你選擇第一種方案,在兩個(gè)線(xiàn)程并發(fā)來(lái)訪(fǎng)問(wèn)時(shí),假設(shè)線(xiàn)程1先來(lái),他先把緩存刪了,此時(shí)線(xiàn)程2過(guò)來(lái),他查詢(xún)緩存數(shù)據(jù)并不存在,此時(shí)他寫(xiě)入緩存,當(dāng)他寫(xiě)入緩存后,線(xiàn)程1再執(zhí)行更新動(dòng)作時(shí),實(shí)際上寫(xiě)入的就是舊的數(shù)據(jù),新的數(shù)據(jù)被舊數(shù)據(jù)覆蓋了。

  • 先操作緩存還是先操作數(shù)據(jù)庫(kù)?
    • 先刪除緩存,再操作數(shù)據(jù)庫(kù)
    • 先操作數(shù)據(jù)庫(kù),再刪除緩存

仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略


總結(jié):

緩存更新策略的最佳實(shí)踐方案:

  1. 低一致性需求:使用Redis自帶的內(nèi)存淘汰機(jī)制

  2. 高一致性需求:主動(dòng)更新,并以超時(shí)剔除作為兜底方案

    • 讀操作:

      • 緩存命中則直接返回
      • 緩存未命中則查詢(xún)數(shù)據(jù)庫(kù),并寫(xiě)入緩存,設(shè)定超時(shí)時(shí)間
    • 寫(xiě)操作:

      • 先寫(xiě)數(shù)據(jù)庫(kù),然后再刪除緩存
      • 要確保數(shù)據(jù)庫(kù)與緩存操作的原子性

2.4 實(shí)現(xiàn)店鋪的緩存與數(shù)據(jù)庫(kù)雙寫(xiě)一致

核心思路如下:

修改ShopController中的業(yè)務(wù)邏輯,滿(mǎn)足下面的需求:

  • 根據(jù)id查詢(xún)店鋪時(shí),如果緩存未命中,則查詢(xún)數(shù)據(jù)庫(kù),將數(shù)據(jù)庫(kù)結(jié)果寫(xiě)入緩存,并設(shè)置超時(shí)時(shí)間

  • 根據(jù)id修改店鋪時(shí),先修改數(shù)據(jù)庫(kù),再刪除緩存

修改重點(diǎn)代碼1:修改ShopServiceImpl的queryById方法

設(shè)置redis緩存時(shí)添加過(guò)期時(shí)間

   @Override
    public Result queryById(Long id) {
        String key = "cache:shop:" + id; //給每一個(gè)店鋪設(shè)置一個(gè)key,以便于區(qū)別
        //1.從redis查詢(xún)商品緩存
        String shopJson = stringRedisTemplate.opsForValue().get(key);

        //2.判斷是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3.存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }

        //4.不存在,根據(jù)id查詢(xún)數(shù)據(jù)庫(kù)
        Shop shop = getById(id);

        //5.不存在,返回錯(cuò)誤
        if (shop == null) {
            return Result.fail("店鋪不存在!");
        }

        //6.存在,寫(xiě)入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),30L, TimeUnit.MINUTES);

        //7.返回
        return Result.ok(shop);

    }

修改重點(diǎn)代碼2

代碼分析:通過(guò)之前的淘汰,我們確定了采用刪除策略,來(lái)解決雙寫(xiě)問(wèn)題,當(dāng)我們修改了數(shù)據(jù)之后,然后把緩存中的數(shù)據(jù)進(jìn)行刪除,查詢(xún)時(shí)發(fā)現(xiàn)緩存中沒(méi)有數(shù)據(jù),則會(huì)從mysql中加載最新的數(shù)據(jù),從而避免數(shù)據(jù)庫(kù)和緩存不一致的問(wèn)題。

    @Override
    @Transactional
    public Result update(Shop shop) {
        Long id = shop.getId();
        if (id == null) {
            return Result.fail("店鋪id不能為空");
        }
        //1.更新數(shù)據(jù)庫(kù)
        updateById(shop);

        //2.刪除緩存
        stringRedisTemplate.delete("cache:shop:" + id);
        return Result.ok();
    }

2.5 緩存穿透問(wèn)題的解決思路

緩存穿透:緩存穿透是指客戶(hù)端請(qǐng)求的數(shù)據(jù)在緩存和數(shù)據(jù)庫(kù)中都不存在,這樣緩存永遠(yuǎn)不會(huì)生效,這樣請(qǐng)求都會(huì)打到數(shù)據(jù)庫(kù)。

常見(jiàn)的解決方案有兩種:

  • 緩存空對(duì)象
    • 優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,維護(hù)方便
    • 缺點(diǎn):
      • 額外的內(nèi)存消耗
      • 可能造成短期的不一致
  • 布隆過(guò)濾
    • 優(yōu)點(diǎn):內(nèi)存占用較少,沒(méi)有多余key
    • 缺點(diǎn):
      • 實(shí)現(xiàn)復(fù)雜
      • 存在誤判可能

緩存空對(duì)象思路分析: 當(dāng)我們客戶(hù)端訪(fǎng)問(wèn)不存在的數(shù)據(jù)時(shí),先請(qǐng)求redis,但是此時(shí)redis中沒(méi)有數(shù)據(jù),此時(shí)會(huì)訪(fǎng)問(wèn)到數(shù)據(jù)庫(kù),但是數(shù)據(jù)庫(kù)中也沒(méi)有數(shù)據(jù),這個(gè)數(shù)據(jù)穿透了緩存,直擊數(shù)據(jù)庫(kù),我們都知道數(shù)據(jù)庫(kù)能夠承載的并發(fā)不如redis這么高,如果大量的請(qǐng)求同時(shí)過(guò)來(lái)訪(fǎng)問(wèn)這種不存在的數(shù)據(jù),這些請(qǐng)求就都會(huì)訪(fǎng)問(wèn)到數(shù)據(jù)庫(kù),簡(jiǎn)單的解決方案就是哪怕這個(gè)數(shù)據(jù)在數(shù)據(jù)庫(kù)中也不存在,我們也把這個(gè)數(shù)據(jù)存入到redis中去,這樣,下次用戶(hù)過(guò)來(lái)訪(fǎng)問(wèn)這個(gè)不存在的數(shù)據(jù),那么在redis中也能找到這個(gè)數(shù)據(jù)就不會(huì)進(jìn)入到數(shù)據(jù)庫(kù)了。

布隆過(guò)濾: 布隆過(guò)濾器其實(shí)采用的是哈希思想來(lái)解決這個(gè)問(wèn)題,通過(guò)一個(gè)龐大的二進(jìn)制數(shù)組,走哈希思想去判斷當(dāng)前這個(gè)要查詢(xún)的這個(gè)數(shù)據(jù)是否存在,如果布隆過(guò)濾器判斷存在,則放行,這個(gè)請(qǐng)求會(huì)去訪(fǎng)問(wèn)redis,哪怕此時(shí)redis中的數(shù)據(jù)過(guò)期了,但是數(shù)據(jù)庫(kù)中一定存在這個(gè)數(shù)據(jù),在數(shù)據(jù)庫(kù)中查詢(xún)出來(lái)這個(gè)數(shù)據(jù)后,再將其放入到redis中,假設(shè)布隆過(guò)濾器判斷這個(gè)數(shù)據(jù)不存在,則直接返回。這種方式優(yōu)點(diǎn)在于節(jié)約內(nèi)存空間,存在誤判,誤判原因在于:布隆過(guò)濾器走的是哈希思想,只要哈希思想,就可能存在哈希沖突。
仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略


2.6 編碼解決商品查詢(xún)的緩存穿透問(wèn)題

核心思路如下:

在原來(lái)的邏輯中,我們?nèi)绻l(fā)現(xiàn)這個(gè)數(shù)據(jù)在mysql中不存在,直接就返回404了,這樣是會(huì)存在緩存穿透問(wèn)題的。

現(xiàn)在的邏輯中:如果這個(gè)數(shù)據(jù)不存在,我們不會(huì)返回404 ,還是會(huì)把這個(gè)數(shù)據(jù)寫(xiě)入到Redis中,并且將value設(shè)置為空,當(dāng)再次發(fā)起查詢(xún)時(shí),我們?nèi)绻l(fā)現(xiàn)命中之后,判斷這個(gè)value是否是null,如果是null,則是之前寫(xiě)入的數(shù)據(jù),證明是緩存穿透數(shù)據(jù),如果不是,則直接返回?cái)?shù)據(jù)。

仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略


	@Override
    public Result queryById(Long id) {
        String key = "cache:shop:" + id; //給每一個(gè)店鋪設(shè)置一個(gè)key,以便于區(qū)別
        //1.從redis查詢(xún)商品緩存
        String shopJson = stringRedisTemplate.opsForValue().get(key);

        //2.判斷是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3.存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }

        //走到這一步,shopJson是null或者空值
        //判斷命中的是否是空值
        if (shopJson == "") {
            //返回一個(gè)錯(cuò)誤信息
            return Result.fail("店鋪信息不存在!");
        }

        //4.不存在,根據(jù)id查詢(xún)數(shù)據(jù)庫(kù)
        Shop shop = getById(id);

        //5.不存在,返回錯(cuò)誤
        if (shop == null) {
            //將空值寫(xiě)入redis
            stringRedisTemplate.opsForValue().set(key, "",2L, TimeUnit.MINUTES);
            return Result.fail("店鋪不存在!");
        }

        //6.存在,寫(xiě)入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),30L, TimeUnit.MINUTES);

        //7.返回
        return Result.ok(shop);

    }

小總結(jié):

緩存穿透產(chǎn)生的原因是什么?

  • 用戶(hù)請(qǐng)求的數(shù)據(jù)在緩存中和數(shù)據(jù)庫(kù)中都不存在,不斷發(fā)起這樣的請(qǐng)求,給數(shù)據(jù)庫(kù)帶來(lá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ù)的限流

2.7 緩存雪崩問(wèn)題及解決思路

緩存雪崩: 緩存雪崩是指在同一時(shí)段大量的緩存key同時(shí)生效或者Redis宕機(jī),導(dǎo)致大量請(qǐng)求到達(dá)數(shù)據(jù)庫(kù),帶來(lái)的巨大壓力。

解決方案:

  • 給不同的Key的TTL添加隨機(jī)值
  • 利用Redis集群提高服務(wù)的可用性
  • 給緩存業(yè)務(wù)添加降級(jí)限流策略
  • 給業(yè)務(wù)添加多級(jí)緩存

仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略


2.8 緩存擊穿問(wèn)題及解決思路

緩存擊穿: 緩存擊穿問(wèn)題也叫熱點(diǎn)Key問(wèn)題,就是一個(gè)被高并發(fā)訪(fǎng)問(wèn)并且緩存重建業(yè)務(wù)較復(fù)雜的key突然失效了,無(wú)數(shù)的請(qǐng)求訪(fǎng)問(wèn)會(huì)在瞬間給數(shù)據(jù)庫(kù)帶來(lái)巨大的沖擊。

常見(jiàn)的解決方案有兩種:

  • 互斥鎖
  • 邏輯過(guò)期

邏輯分析:假設(shè)線(xiàn)程1在查詢(xún)緩存之后,本來(lái)應(yīng)該去查詢(xún)數(shù)據(jù)庫(kù),然后把這個(gè)數(shù)據(jù)重新加載到緩存的,此時(shí)只要線(xiàn)程1走完這個(gè)邏輯,其他線(xiàn)程就都能從緩存中加載這些數(shù)據(jù)了,但是假設(shè)在線(xiàn)程1沒(méi)有走完的時(shí)候,后續(xù)的線(xiàn)程2,線(xiàn)程3,線(xiàn)程4同時(shí)過(guò)來(lái)訪(fǎng)問(wèn)當(dāng)前這個(gè)方法, 那么這些線(xiàn)程都不能從緩存中查詢(xún)到數(shù)據(jù),那么他們就會(huì)同一時(shí)刻來(lái)訪(fǎng)問(wèn)查詢(xún)緩存,都沒(méi)查到,接著同一時(shí)間去訪(fǎng)問(wèn)數(shù)據(jù)庫(kù),同時(shí)的去執(zhí)行數(shù)據(jù)庫(kù)代碼,對(duì)數(shù)據(jù)庫(kù)訪(fǎng)問(wèn)壓力過(guò)大。

仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略

解決方案一、使用鎖來(lái)解決:

因?yàn)殒i能實(shí)現(xiàn)互斥性。假設(shè)線(xiàn)程過(guò)來(lái),只能一個(gè)人一個(gè)人的來(lái)訪(fǎng)問(wèn)數(shù)據(jù)庫(kù),從而避免對(duì)于數(shù)據(jù)庫(kù)訪(fǎng)問(wèn)壓力過(guò)大,但這也會(huì)影響查詢(xún)的性能,因?yàn)榇藭r(shí)會(huì)讓查詢(xún)的性能從并行變成了串行,我們可以采用tryLock方法 + double check來(lái)解決這樣的問(wèn)題。

假設(shè)現(xiàn)在線(xiàn)程1過(guò)來(lái)訪(fǎng)問(wèn),他查詢(xún)緩存沒(méi)有命中,但是此時(shí)他獲得到了鎖的資源,那么線(xiàn)程1就會(huì)一個(gè)人去執(zhí)行邏輯,假設(shè)現(xiàn)在線(xiàn)程2過(guò)來(lái),線(xiàn)程2在執(zhí)行過(guò)程中,并沒(méi)有獲得到鎖,那么線(xiàn)程2就可以進(jìn)行到休眠,直到線(xiàn)程1把鎖釋放后,線(xiàn)程2獲得到鎖,然后再來(lái)執(zhí)行邏輯,此時(shí)就能夠從緩存中拿到數(shù)據(jù)了。
仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略

解決方案二、邏輯過(guò)期方案

方案分析:我們之所以會(huì)出現(xiàn)這個(gè)緩存擊穿問(wèn)題,主要原因是在于我們對(duì)key設(shè)置了過(guò)期時(shí)間,假設(shè)我們不設(shè)置過(guò)期時(shí)間,其實(shí)就不會(huì)有緩存擊穿的問(wèn)題,但是不設(shè)置過(guò)期時(shí)間,這樣數(shù)據(jù)不就一直占用我們內(nèi)存了嗎,我們可以采用邏輯過(guò)期方案。

我們把過(guò)期時(shí)間設(shè)置在 redis的value中,注意:這個(gè)過(guò)期時(shí)間并不會(huì)直接作用于redis,而是我們后續(xù)通過(guò)邏輯去處理。假設(shè)線(xiàn)程1去查詢(xún)緩存,然后從value中判斷出來(lái)當(dāng)前的數(shù)據(jù)已經(jīng)過(guò)期了,此時(shí)線(xiàn)程1去獲得互斥鎖,那么其他線(xiàn)程會(huì)進(jìn)行阻塞,獲得了鎖的線(xiàn)程他會(huì)開(kāi)啟一個(gè) 線(xiàn)程去進(jìn)行 以前的重構(gòu)數(shù)據(jù)的邏輯,直到新開(kāi)的線(xiàn)程完成這個(gè)邏輯后,才釋放鎖, 而線(xiàn)程1直接進(jìn)行返回,假設(shè)現(xiàn)在線(xiàn)程3過(guò)來(lái)訪(fǎng)問(wèn),由于線(xiàn)程線(xiàn)程2持有著鎖,所以線(xiàn)程3無(wú)法獲得鎖,線(xiàn)程3也直接返回?cái)?shù)據(jù),只有等到新開(kāi)的線(xiàn)程2把重建數(shù)據(jù)構(gòu)建完后,其他線(xiàn)程才能走返回正確的數(shù)據(jù)。

這種方案巧妙在于,異步的構(gòu)建緩存,缺點(diǎn)在于在構(gòu)建完緩存之前,返回的都是臟數(shù)據(jù)。
仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略

進(jìn)行對(duì)比

互斥鎖方案: 由于保證了互斥性,所以數(shù)據(jù)一致,且實(shí)現(xiàn)簡(jiǎn)單,因?yàn)閮H僅只需要加一把鎖而已,也沒(méi)其他的事情需要操心,所以沒(méi)有額外的內(nèi)存消耗,缺點(diǎn)在于有鎖就有死鎖問(wèn)題的發(fā)生,且只能串行執(zhí)行性能肯定受到影響。

邏輯過(guò)期方案: 線(xiàn)程讀取過(guò)程中不需要等待,性能好,有一個(gè)額外的線(xiàn)程持有鎖去進(jìn)行重構(gòu)數(shù)據(jù),但是在重構(gòu)數(shù)據(jù)完成前,其他的線(xiàn)程只能返回之前的數(shù)據(jù),且實(shí)現(xiàn)起來(lái)麻煩。

仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略


2.9 利用互斥鎖解決緩存擊穿問(wèn)題

核心思路:相較于原來(lái)從緩存中查詢(xún)不到數(shù)據(jù)后直接查詢(xún)數(shù)據(jù)庫(kù)而言,現(xiàn)在的方案是 進(jìn)行查詢(xún)之后,如果從緩存沒(méi)有查詢(xún)到數(shù)據(jù),則進(jìn)行互斥鎖的獲取,獲取互斥鎖后,判斷是否獲得到了鎖,如果沒(méi)有獲得到,則休眠,過(guò)一會(huì)再進(jìn)行嘗試,直到獲取到鎖為止,才能進(jìn)行查詢(xún)。

如果獲取到了鎖的線(xiàn)程,再去進(jìn)行查詢(xún),查詢(xún)后將數(shù)據(jù)寫(xiě)入redis,再釋放鎖,返回?cái)?shù)據(jù),利用互斥鎖就能保證只有一個(gè)線(xiàn)程去執(zhí)行操作數(shù)據(jù)庫(kù)的邏輯,防止緩存擊穿。
仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略

操作鎖的代碼:
核心思路就是利用redis的setnx方法來(lái)表示獲取鎖,該方法含義是redis中如果沒(méi)有這個(gè)key,則插入成功,返回1,在stringRedisTemplate中返回true, 如果有這個(gè)key則插入失敗,則返回0,在stringRedisTemplate返回false,我們可以通過(guò)true,或者是false,來(lái)表示是否有線(xiàn)程成功插入key,成功插入的key的線(xiàn)程我們認(rèn)為他就是獲得到鎖的線(xiàn)程。

//獲取互斥鎖
private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}

//釋放鎖
private void unlock(String key) {
    stringRedisTemplate.delete(key);
}

操作代碼:

//緩存擊穿
    public Shop queryWithMutex(Long id) {
        String key = "cache:shop:" + id; //給每一個(gè)店鋪設(shè)置一個(gè)key,以便于區(qū)別
        //1.從redis查詢(xún)商品緩存
        String shopJson = stringRedisTemplate.opsForValue().get(key);

        //2.判斷是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3.存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }

        //走到這一步,shopJson是null或者空值
        //判斷命中的是否是空值
        if (shopJson == "") {
            //返回一個(gè)錯(cuò)誤信息
            return null;
        }

        //4. 實(shí)現(xiàn)緩存重建
        //4.1 獲取互斥鎖
        String lockKey = "lock:shop:" + id;
        Shop shop = null;


        try {
            boolean isLock = tryLock(lockKey);
            //4.2 判斷是否獲取成功
            if (!isLock) {
                //4.3 失敗,則休眠重試
                Thread.sleep(50);
                return queryWithMutex(id);
            }

            //4.4 成功,根據(jù)id查詢(xún)數(shù)據(jù)庫(kù)
            shop = getById(id);

            //5.不存在,返回錯(cuò)誤
            if (shop == null) {
                //將空值寫(xiě)入redis
                stringRedisTemplate.opsForValue().set(key, "",2L, TimeUnit.MINUTES);
                return null;
            }

            //6.存在,寫(xiě)入redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),30L, TimeUnit.MINUTES);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //7.釋放互斥鎖
            unlock(lockKey);
        }
        return shop;

    }

2.10 利用邏輯過(guò)期解決緩存擊穿問(wèn)題

需求:修改根據(jù)id查詢(xún)商鋪的業(yè)務(wù),基于邏輯過(guò)期方式來(lái)解決緩存擊穿問(wèn)題

思路分析:當(dāng)用戶(hù)開(kāi)始查詢(xún)r(jià)edis時(shí),判斷是否命中,如果沒(méi)有命中則直接返回空數(shù)據(jù),不查詢(xún)數(shù)據(jù)庫(kù),而一旦命中后,將value取出,判斷value中的過(guò)期時(shí)間是否滿(mǎn)足,如果沒(méi)有過(guò)期,則直接返回redis中的數(shù)據(jù),如果過(guò)期,則在開(kāi)啟獨(dú)立線(xiàn)程后直接返回之前的數(shù)據(jù),獨(dú)立線(xiàn)程去重構(gòu)數(shù)據(jù),重構(gòu)完成后釋放互斥鎖。
仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】,仿大眾點(diǎn)評(píng),java,redis,互斥鎖,緩存更新策略

如果封裝數(shù)據(jù):因?yàn)楝F(xiàn)在redis中存儲(chǔ)的數(shù)據(jù)的value需要帶上過(guò)期時(shí)間,此時(shí)要么你去修改原來(lái)的實(shí)體類(lèi),要么你新建一個(gè)實(shí)體類(lèi),我們采用第二個(gè)方案,這個(gè)方案,對(duì)原來(lái)代碼沒(méi)有侵入性。

  • 新建實(shí)體類(lèi)
@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}
  • ShopServiceImpl 新增此方法,利用單元測(cè)試進(jìn)行緩存預(yù)熱
    public void saveShop2Redis(Long id, Long expireSeconds) {
        //1.查詢(xún)店鋪數(shù)據(jù)
        Shop shop = getById(id);

        //2.封裝邏輯過(guò)期時(shí)間
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));

        //3.寫(xiě)入redis
        stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(redisData));
    }
  • 在測(cè)試類(lèi)中
   
    @Test
    void testSaveShop() {
        shopService.saveShop2Redis(1L, 10L);
    }
  • ShopServiceImpl
 public Shop queryWithLogicalExpire(Long id) {
        String key = "cache:shop:" + id; //給每一個(gè)店鋪設(shè)置一個(gè)key,以便于區(qū)別
        //1.從redis查詢(xún)商品緩存
        String shopJson = stringRedisTemplate.opsForValue().get(key);

        //2.判斷是否存在
        if (StrUtil.isBlank(shopJson)) {
            //3.不存在,返回null
            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 shop;
        }

        //5.2已過(guò)期,需要緩存重建
        //6.緩存重建
        //6.1獲取互斥鎖
        String lockKey = "lock:shop:" + id;
        boolean isLock = tryLock(lockKey);

        //6.2判斷是否獲取鎖成功
        if (isLock) {
            // 6.3 成功,開(kāi)啟獨(dú)立線(xiàn)程,實(shí)現(xiàn)緩存重建
            CACHE_REBUILD_EXECUTOR.submit(() ->{
                try {
                    // 重建緩存
                    this.saveShop2Redis(id, 20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //釋放鎖
                    unlock(lockKey);
                }
            });
        }

        //6.4返回過(guò)期的商鋪信息
        return shop;
    }

2.11 封裝Redis工具類(lèi)

基于StringRedisTemplate封裝一個(gè)緩存工具類(lèi),滿(mǎn)足下列需求:

  • 方法1:將任意Java對(duì)象序列化為json并存儲(chǔ)在string類(lèi)型的key中,并且可以設(shè)置TTL過(guò)期時(shí)間

  • 方法2:將任意Java對(duì)象序列化為json并存儲(chǔ)在string類(lèi)型的key中,并且可以設(shè)置邏輯過(guò)期時(shí)間,用于處理緩存擊穿問(wèn)題

  • 方法3:根據(jù)指定的key查詢(xún)緩存,并反序列化為指定類(lèi)型,利用緩存空值的方式解決緩存穿透問(wèn)題

  • 方法4:根據(jù)指定的key查詢(xún)緩存,并反序列化為指定類(lèi)型,需要利用邏輯過(guò)期解決緩存擊穿問(wèn)題將邏輯進(jìn)行封裝

@Slf4j
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

    //創(chuàng)建一個(gè)線(xiàn)程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public void set(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
        // 設(shè)置邏輯過(guò)期
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        // 寫(xiě)入Redis
        stringRedisTemplate.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查詢(xún)商鋪緩存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判斷是否存在
        if (StrUtil.isNotBlank(json)) {
            // 3.存在,直接返回
            return JSONUtil.toBean(json, type);
        }
        // 判斷命中的是否是空值
        if (json != null) {
            // 返回一個(gè)錯(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, "", 30L, TimeUnit.MINUTES);
            // 返回錯(cuò)誤信息
            return null;
        }
        // 6.存在,寫(xiě)入redis
        this.set(key, r, time, unit);
        return r;
    }

    //邏輯過(guò)期解決緩存擊穿
    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查詢(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 r;
        }
        // 5.2.已過(guò)期,需要緩存重建
        // 6.緩存重建
        // 6.1.獲取互斥鎖
        String lockKey = "lock:shop:" + id;
        boolean isLock = tryLock(lockKey);
        // 6.2.判斷是否獲取鎖成功
        if (isLock){
            // 6.3.成功,開(kāi)啟獨(dú)立線(xiàn)程,實(shí)現(xiàn)緩存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 查詢(xún)數(shù)據(jù)庫(kù)
                    R newR = dbFallback.apply(id);
                    // 重建緩存
                    this.setWithLogicalExpire(key, newR, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    // 釋放鎖
                    unlock(lockKey);
                }
            });
        }
        // 6.4.返回過(guò)期的商鋪信息
        return r;
    }

    //互斥鎖解決緩存擊穿
    public <R, ID> R queryWithMutex(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        // 1.從redis查詢(xún)商鋪緩存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判斷是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 3.存在,直接返回
            return JSONUtil.toBean(shopJson, type);
        }
        // 判斷命中的是否是空值
        if (shopJson != null) {
            // 返回一個(gè)錯(cuò)誤信息
            return null;
        }

        // 4.實(shí)現(xiàn)緩存重建
        // 4.1.獲取互斥鎖
        String lockKey = "lock:shop:" + id;
        R r = null;
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2.判斷是否獲取成功
            if (!isLock) {
                // 4.3.獲取鎖失敗,休眠并重試
                Thread.sleep(50);
                return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
            }
            // 4.4.獲取鎖成功,根據(jù)id查詢(xún)數(shù)據(jù)庫(kù)
            r = dbFallback.apply(id);
            // 5.不存在,返回錯(cuò)誤
            if (r == null) {
                // 將空值寫(xiě)入redis
                stringRedisTemplate.opsForValue().set(key, "", 30L, TimeUnit.MINUTES);
                // 返回錯(cuò)誤信息
                return null;
            }
            // 6.存在,寫(xiě)入redis
            this.set(key, r, time, unit);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            // 7.釋放鎖
            unlock(lockKey);
        }
        // 8.返回
        return r;
    }

    //獲取互斥鎖
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    //釋放鎖
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }
}

ShopServiceImpl 中:

 @Resource
    private CacheClient cacheClient;

    @Override
    public Result queryById(Long id) {
        // 解決緩存穿透
        Shop shop = cacheClient
                .queryWithPassThrough("CACHE:SHOP:", id, Shop.class, this::getById, 30L, TimeUnit.MINUTES);

        // 互斥鎖解決緩存擊穿
        // Shop shop = cacheClient
        //         .queryWithMutex("CACHE:SHOP:", id, Shop.class, this::getById, 30L TimeUnit.MINUTES);

        // 邏輯過(guò)期解決緩存擊穿
        // Shop shop = cacheClient
        //         .queryWithLogicalExpire("CACHE:SHOP:", id, Shop.class, this::getById, 20L, TimeUnit.SECONDS);

        if (shop == null) {
            return Result.fail("店鋪不存在!");
        }
        // 7.返回
        return Result.ok(shop);
    }

?
非常感謝您閱讀到這里,如果這篇文章對(duì)您有幫助,希望能留下您的點(diǎn)贊?? 關(guān)注?? 分享?? 留言??thanks!??!文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-534985.html

到了這里,關(guān)于仿大眾點(diǎn)評(píng)項(xiàng)目 —— Day01【短信登錄、商戶(hù)查詢(xún)緩存】的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來(lái)自互聯(lián)網(wǎng)用戶(hù)投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • 《大眾點(diǎn)評(píng)爬蟲(chóng)程序?qū)崙?zhàn):爬取店鋪展示信息》

    《大眾點(diǎn)評(píng)爬蟲(chóng)程序?qū)崙?zhàn):爬取店鋪展示信息》

    ?在日常生活中,我們經(jīng)常會(huì)遇到一個(gè)問(wèn)題:不知道吃什么。尤其是在陌生的城市或附近的地方,面對(duì)眾多的餐館選擇,很難做出決策。隨著互聯(lián)網(wǎng)的發(fā)展,大眾點(diǎn)評(píng)等餐飲評(píng)價(jià)平臺(tái)應(yīng)運(yùn)而生,為我們提供了海量的餐館信息和用戶(hù)評(píng)價(jià)。然而,即使在這樣的平臺(tái)上,面對(duì)龐大

    2024年04月25日
    瀏覽(19)
  • reggie優(yōu)化01-緩存短信驗(yàn)證碼和菜品數(shù)據(jù)

    reggie優(yōu)化01-緩存短信驗(yàn)證碼和菜品數(shù)據(jù)

    在config包下,創(chuàng)建Redis配置類(lèi)RedisConfig: 納入Git管理: 之前的短信驗(yàn)證碼存放在session中,是存在一定的時(shí)間有效期,現(xiàn)在要將短信驗(yàn)證碼存放到Redis中。 1、注入RedisTemplate對(duì)象: 2、在sendMsg方法中,將生成的驗(yàn)證碼緩存到Redis中,并且設(shè)置有效期為5分鐘: 其中:redisTemplate.

    2024年02月16日
    瀏覽(23)
  • 使用影刀采集大眾點(diǎn)評(píng)數(shù)據(jù):打造自動(dòng)化數(shù)據(jù)采集工具

    在本教程中,我將向大家介紹如何使用影刀(YinDao)來(lái)采集大眾點(diǎn)評(píng)的數(shù)據(jù)。影刀是一款強(qiáng)大的自動(dòng)化流程處理工具,可以幫助我們自動(dòng)執(zhí)行網(wǎng)頁(yè)操作、數(shù)據(jù)提取等任務(wù),極大地提高了數(shù)據(jù)采集的效率和準(zhǔn)確性。通過(guò)本教程,你將學(xué)會(huì)如何使用影刀來(lái)構(gòu)建一個(gè)自動(dòng)化的數(shù)據(jù)采

    2024年04月17日
    瀏覽(25)
  • 01【MySQL架構(gòu)、查詢(xún)緩存】

    01【MySQL架構(gòu)、查詢(xún)緩存】

    1)下載安裝wget命令 2)在線(xiàn)下載mysql安裝包 3)解壓mysql安裝包: 安裝包介紹: 安裝包名稱(chēng) 簡(jiǎn)介 mysql-community-client MySQL客戶(hù)端應(yīng)用程序和工具 mysql-community-common 服務(wù)器和客戶(hù)端庫(kù)的通用文件 mysql-community-devel MySQL數(shù)據(jù)庫(kù)客戶(hù)端應(yīng)用程序的開(kāi)發(fā)頭文件和庫(kù) mysql-community-embedded-com

    2024年02月11日
    瀏覽(19)
  • JavaWeb_瑞吉外賣(mài)_業(yè)務(wù)開(kāi)發(fā)Day5-套餐管理, 短信發(fā)送, 手機(jī)驗(yàn)證碼登錄

    總結(jié) 接收List數(shù)據(jù)時(shí), 需要加上 @RequestParam 注解 操作2個(gè)及2個(gè)以上表, 加上 @Transactional 事務(wù)注解, 保持?jǐn)?shù)據(jù)的一致性. 發(fā)送短信 生成驗(yàn)證碼 黑馬程序員. 瑞吉外賣(mài)項(xiàng)目

    2024年02月12日
    瀏覽(29)
  • redis實(shí)戰(zhàn)-項(xiàng)目集成git及redis實(shí)現(xiàn)短信驗(yàn)證碼登錄

    redis實(shí)戰(zhàn)-項(xiàng)目集成git及redis實(shí)現(xiàn)短信驗(yàn)證碼登錄

    目錄 ? ? IDEA集成git 傳統(tǒng)session存在的問(wèn)題? redis方案 業(yè)務(wù)流程 選用的數(shù)據(jù)結(jié)構(gòu) 整體訪(fǎng)問(wèn)流程 發(fā)送短信驗(yàn)證碼 ?獲取校驗(yàn)驗(yàn)證碼 配置登錄攔截器 攔截器注冊(cè)配置類(lèi) 攔截器 用戶(hù)狀態(tài)刷新問(wèn)題 刷新問(wèn)題解決方案 ? 遠(yuǎn)程倉(cāng)庫(kù)采用碼云,創(chuàng)建好倉(cāng)庫(kù),復(fù)制倉(cāng)庫(kù)的url ? ?在idea中點(diǎn)

    2024年02月11日
    瀏覽(14)
  • 短信服務(wù)在項(xiàng)目中的配置及如何實(shí)現(xiàn)驗(yàn)證碼登錄

    短信服務(wù)在項(xiàng)目中的配置及如何實(shí)現(xiàn)驗(yàn)證碼登錄

    目前市面上有很多第三方提供的短信服務(wù),這些第三方短信服務(wù)會(huì)和各個(gè)運(yùn)營(yíng)商(移動(dòng)、聯(lián)通、電信)對(duì)接,我們只需要注冊(cè)成為會(huì)員并且按照提供的開(kāi)發(fā)文檔進(jìn)行調(diào)用就可以發(fā)送短信。需要說(shuō)明的是,這些短信服務(wù)一般都是收費(fèi)服務(wù)。 阿里云短信服務(wù)(Short Message Service)是

    2023年04月16日
    瀏覽(21)
  • 【SpringBoot項(xiàng)目】SpringBoot項(xiàng)目-瑞吉外賣(mài)【day01】

    【SpringBoot項(xiàng)目】SpringBoot項(xiàng)目-瑞吉外賣(mài)【day01】

    ??博客x主頁(yè):己不由心王道長(zhǎng)??! ??文章說(shuō)明:SpringBoot項(xiàng)目-瑞吉外賣(mài)【day01】?? ?系列專(zhuān)欄:SpringBoot項(xiàng)目 ??本篇內(nèi)容:對(duì)黑馬的瑞吉外賣(mài)項(xiàng)目的day01進(jìn)行筆記和項(xiàng)目實(shí)現(xiàn)?? ??每日一語(yǔ):人有退路,就有些許安全感。等到哪一天,你真沒(méi)了退路,你就發(fā)現(xiàn)眼前哪條路都

    2023年04月08日
    瀏覽(23)
  • day01-項(xiàng)目介紹與環(huán)境搭建

    day01-項(xiàng)目介紹與環(huán)境搭建

    Java基礎(chǔ)知識(shí) javaweb MySQL SpringBoot SSM(Spring,SpringMVC,MyBatis) Maven 了解企業(yè)項(xiàng)目開(kāi)發(fā)的完整流程,增長(zhǎng)開(kāi)發(fā)經(jīng)驗(yàn) 了解需求分析的過(guò)程,提高分析和設(shè)計(jì)能力 對(duì)所學(xué)的技術(shù)進(jìn)行靈活應(yīng)用,提高編碼能力 解決各種異常情況,提高代碼調(diào)試能力 開(kāi)發(fā)環(huán)境(development):開(kāi)發(fā)人員在

    2023年04月14日
    瀏覽(19)
  • Redis實(shí)戰(zhàn)——商戶(hù)查詢(xún)(一)

    Redis實(shí)戰(zhàn)——商戶(hù)查詢(xún)(一)

    緩存(Cache):就是數(shù)據(jù)交換的緩沖區(qū),俗稱(chēng)的緩存就是緩沖區(qū)內(nèi)的數(shù)據(jù),緩存數(shù)據(jù)在內(nèi)存中,內(nèi)存的讀寫(xiě)性能完全高于磁盤(pán),使用緩存可以大大降低用戶(hù)訪(fǎng)問(wèn)并發(fā)量帶來(lái)的服務(wù)器讀寫(xiě)壓力。當(dāng)數(shù)據(jù)量較大時(shí),如果沒(méi)有緩存來(lái)作為“避震器(防止過(guò)高的數(shù)據(jù)訪(fǎng)問(wèn)猛沖系統(tǒng),導(dǎo)致其

    2024年02月12日
    瀏覽(13)

覺(jué)得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包