【Redis实战】击穿+雪崩+穿透

架构

image.png

短信登录

基于session实现登录

流程图

image.png

代码实现
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {/*** session用户key*/public static final String USER_CONSTANT = "user";@Overridepublic Result sendCode(String phone, HttpSession session) {//校验手机号码boolean phoneInvalid = RegexUtils.isPhoneInvalid(phone);if (phoneInvalid) {return Result.fail("手机号码格式错误!");}//生成6位数的验证码String code = RandomUtil.randomNumbers(6);session.setAttribute("code", code);//发送验证码log.info("send code success,code={}", code);return Result.ok();}@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {//校验手机号码if (Objects.isNull(loginForm)) {return Result.fail("参数为空!");}String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号码格式错误!");}//验证码校验String code = (String) session.getAttribute("code");if (StringUtils.isBlank(code) || !StringUtils.equals(code, loginForm.getCode())) {return Result.fail("验证码错误!");}LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();wrapper.eq(User::getPhone, phone);User user = getOne(wrapper);if (!Objects.nonNull(user)) {//注册新用户user = getNewUserByPhone(phone);save(user);}session.setAttribute(USER_CONSTANT, BeanUtil.copyProperties(user, UserDTO.class));return Result.ok();}/*** 根据手机号码创建新用户** @param phone 手机号码* @return*/private User getNewUserByPhone(String phone) {User user = new User();user.setCreateTime(LocalDateTime.now());user.setPhone(phone);user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));user.setUpdateTime(LocalDateTime.now());return user;}
}
集群session共享问题

image.png

session数据拷贝可以解决这个问题,但是多台tomcat之间存储相同的数据会浪费内存空间,拷贝会有数据延迟。
session每个浏览器有不同的code,tomcat里保存里很多code。

基于Redis实现session登录

验证码流程图

image.png

代码实现
    public Result sendCode(String phone, HttpSession session) {//校验手机号码boolean phoneInvalid = RegexUtils.isPhoneInvalid(phone);if (phoneInvalid) {return Result.fail("手机号码格式错误!");}//生成6位数的验证码String code = RandomUtil.randomNumbers(6);//保存验证码到redisstringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.SECONDS);//发送验证码log.info("send code success,code={}", code);return Result.ok();}
校验流程图

image.png

代码实现

登录

    public Result login(LoginFormDTO loginForm, HttpSession session) {//校验手机号码if (Objects.isNull(loginForm)) {return Result.fail("参数为空!");}String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号码格式错误!");}//验证码校验String code = (String) session.getAttribute("code");if (StringUtils.isBlank(code) || !StringUtils.equals(code, loginForm.getCode())) {return Result.fail("验证码错误!");}LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();wrapper.eq(User::getPhone, phone);User user = getOne(wrapper);if (!Objects.nonNull(user)) {//注册新用户user = getNewUserByPhone(phone);save(user);}session.setAttribute(USER_CONSTANT, BeanUtil.copyProperties(user, UserDTO.class));return Result.ok();
}

登录拦截器

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.http.HttpStatus;
import com.hmdp.dto.UserDTO;
import com.hmdp.utils.UserHolder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;import static com.hmdp.utils.RedisConstants.*;/*** 登录拦截器** @author zhangzengxiu* @date 2023/10/6*/
@Component
public class LoginInterceptor implements HandlerInterceptor {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//获取请求头的tokenString token = request.getHeader("authorization");if (StringUtils.isBlank(token)) {response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);return false;}//获取redis中的tokenString tokenKey = LOGIN_USER_KEY + token;Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(tokenKey);if (CollectionUtils.isEmpty(map)) {//未授权response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);return false;}UserDTO userDTO = BeanUtil.fillBeanWithMap(map, new UserDTO(), false);//用户信息保存到ThreadLocal中UserHolder.saveUser(userDTO);//刷新token有效期stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);return true;}
}
拦截器的操作方式
方式一

拦截器

public class LoginInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;/*** 这个LoginInterceptor是new出来的,所以不能使用Spring注入Bean*/public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}
}

使用拦截器

@Configuration
public class MvcConfig implements WebMvcConfigurer {@Autowiredprivate StringRedisTemplate stringRedisTemplate;/*** 添加拦截器** @param registry*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {InterceptorRegistration registration = registry.addInterceptor(new LoginInterceptor(stringRedisTemplate));registration.excludePathPatterns("/user/code");registration.excludePathPatterns("/user/login");registration.excludePathPatterns("/blog/hot");registration.excludePathPatterns("/shop/**");registration.excludePathPatterns("/shop-type/**");registration.excludePathPatterns("/voucher/**");}
}
方式二

拦截器:配置为Spring的组件

@Component
public class LoginInterceptor implements HandlerInterceptor {@Autowiredprivate StringRedisTemplate stringRedisTemplate;
}

注册拦截器:依赖注入使用即可

@Configuration
public class MvcConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;/*** 添加拦截器** @param registry*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {InterceptorRegistration registration = registry.addInterceptor(loginInterceptor);registration.excludePathPatterns("/user/code");registration.excludePathPatterns("/user/login");registration.excludePathPatterns("/blog/hot");registration.excludePathPatterns("/shop/**");registration.excludePathPatterns("/shop-type/**");registration.excludePathPatterns("/voucher/**");}
}
Redis实现session共享

image.png

拦截器优化

当前存在的问题:
如果用户访问不需要登录鉴权的接口,token就不会刷新,token可能会过期。
image.png
token刷新拦截器

import cn.hutool.core.bean.BeanUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.utils.UserHolder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;/*** @author zhangzengxiu* @date 2023/10/6*/
@Component
public class RefreshTokenInterceptor implements HandlerInterceptor {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//获取请求头的tokenString token = request.getHeader("authorization");if (StringUtils.isBlank(token)) {return true;}//获取redis中的tokenString tokenKey = LOGIN_USER_KEY + token;Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(tokenKey);if (CollectionUtils.isEmpty(map)) {//未授权return true;}UserDTO userDTO = BeanUtil.fillBeanWithMap(map, new UserDTO(), false);//用户信息保存到ThreadLocal中UserHolder.saveUser(userDTO);//刷新token有效期stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);return true;}
}

登录拦截器

import cn.hutool.http.HttpStatus;
import com.hmdp.dto.UserDTO;
import com.hmdp.utils.UserHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;/*** 登录拦截器** @author zhangzengxiu* @date 2023/10/6*/
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {UserDTO userDTO = UserHolder.getUser();if (Objects.isNull(userDTO)) {response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);return false;}return true;}/*** 后置拦截器* 销毁用户信息,防止内存泄露** @param request* @param response* @param handler* @param ex* @throws Exception*/@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
}

商户查询

缓存

缓存就是数据交换的缓冲区称作Cache,是存储数据的临时地方,一般读写性能比较高。

CPU缓存

计算机构造:CPU+内存+磁盘
CPU要做数据计算必须先从内存或者硬盘读取到数据,然后放到寄存器才可以运算。计算机性能受限
CPU会把经常需要读写的数据放到CPU缓存中,这样做高速运算的时候,就不需要每次从内存或者磁盘中进行数据读取,再进行运算,而是直接从缓存中获取数据进行运算。
这样可以充分释放CPU的运算能力。CPU缓存越大,可存储的数据越多,处理的性能越高。

web应用开发过程中的缓存

image.png

优缺点

image.png

缓存作用模型

image.png

优化商户缓存流程

image.png
代码实现

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryShopById(Long id) {if (Objects.isNull(id) || id < 0) {return Result.fail("非法商户!");}//从redis中查询缓存信息String shopCacheKey = CACHE_SHOP_KEY + id;String shopJson = stringRedisTemplate.opsForValue().get(shopCacheKey);if (StringUtils.isNotBlank(shopJson)) {Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}//未命中缓存查询数据库Shop shop = getById(id);if (Objects.isNull(shop)) {return Result.fail("商户不存在!");}//缓存商户信息stringRedisTemplate.opsForValue().set(shopCacheKey, JSONUtil.toJsonStr(shop));return Result.ok(shop);}
}

缓存更新策略

缓存一致性问题
image.png
image.png

业务场景

  • 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
  • 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存

主动更新策略

image.png
image.png

  • 01
    • 维护成本高,需要手动编写代码实现
  • 02
    • 可能没现成的,需要单独维护
  • 03
    • 一致性差:还没异步去更新DB,其他线程去查询了数据库
    • 可靠性差:还没将数据更新到DB,Redis服务挂了,数据丢失了
手动维护

image.png

先删除缓存再操作DB
正常情况

image.png

异常情况

数据不一致情况
image.png

先操作DB再删除缓存(使用)
正常情况

image.png

异常情况

出现的可能性相对较低,加超时时间作为兜底!!!
出现的条件:

  • 两条线程并行执行
  • 线程1执行时,缓存刚好失效
  • 查询数据库后写缓存是微秒级别的
    • 这时刚好另一条线程来进更新了数据库并且删除了缓存,可能性很低

image.png

总结

image.png
image.png

最佳实践方案

image.png
业务代码实现
image.png

设置超时时间

image.png
image.png

缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求会全部打到数据库中。

解决方案

image.png

缓存空对象

image.png

image.png

布隆过滤器

并不是100%准确,有风险
image.png
image.png

业务代码

image.png
解决方案:
image.png

总结

image.png

缓存雪崩

缓存雪崩是指同意时段大量的缓存key同时失效或者redis宕机,导致大量请求到达数数据库,带来巨大压力。
image.png

未命中

image.png

服务宕机

image.png

解决方案

  • 给不同的key的TTL添加随机值。(缓存预热过期都时间一样)
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加服务降级、限流(如:快速失败,拒绝服务等)
  • 给业务添加多级缓存

缓存击穿(热点key)

缓存击穿问题也叫热点key问题,就是一个被高并发访问并且缓存重建业务复杂的key突然失效,无数的请求在瞬间给业务数据库带来巨大的冲击。
image.png
image.png

image.png

解决方案

互斥锁

image.png
性能差,阻塞

逻辑过期

不设置过期时间,永不过期,做活动的时候才会去添加
image.png

VS

image.png

互斥锁牺牲了可用性,保证了一致性:CP
逻辑过期牺牲了一致性,保证了可用性:AP

互斥锁解决缓存击穿问题

image.png

setnx

setnx只有第一个可以操作成功,其他的都会失败。
可以设置有效期作为兜底
有效期设置为业务执行时间的10-20倍
image.png

代码实现
获取锁
    /*** 尝试获取锁** @param key* @return*/private boolean tryLock(String key) {//setnxBoolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);//自动拆箱 防止NPEreturn BooleanUtil.isTrue(flag);}
释放锁
    /*** 释放锁** @param key*/private void unLock(String key) {stringRedisTemplate.delete(key);}
业务代码

image.png

    /*** 互斥锁解决缓存缓存击穿问题** @param id* @return*/private Shop queryShopByMutex(Long id) {//从redis中查询缓存信息String shopCacheKey = CACHE_SHOP_KEY + id;String shopJson = stringRedisTemplate.opsForValue().get(shopCacheKey);if (StringUtils.isNotBlank(shopJson)) {return getShopFromCache(shopJson);}Shop shop = null;String lockKey = "lock:shop:" + id;try {//获取互斥锁boolean isLock = tryLock(lockKey);if (!isLock) {//获取锁失败,休眠 重试TimeUnit.MILLISECONDS.sleep(50);//一直重试 会有性能问题return queryShopByMutex(id);}//获取锁成功,再次查询缓存是否存在,Double CheckshopJson = stringRedisTemplate.opsForValue().get(shopCacheKey);if (StringUtils.isNotBlank(shopJson)) {return getShopFromCache(shopJson);}//未命中缓存查询数据库shop = getById(id);//模拟重建延时200msTimeUnit.MILLISECONDS.sleep(200);if (Objects.isNull(shop)) {//缓存空值 缓存2minstringRedisTemplate.opsForValue().set(shopCacheKey, NULL_VAL, CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}//缓存商户信息,添加过期时间 30分钟stringRedisTemplate.opsForValue().set(shopCacheKey, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (InterruptedException e) {throw new RuntimeException();} finally {//释放互斥锁unLock(lockKey);}return shop;}/*** 从缓存中获取shop信息** @param shopJson* @return*/private Shop getShopFromCache(String shopJson) {if (StringUtils.equals(NULL_VAL, shopJson)) {//空值return null;}return JSONUtil.toBean(shopJson, Shop.class);}
模拟并发请求

image.png
线程组 QPS=200
image.png

逻辑过期解决缓存击穿问题

业务流程图

image.png

缓存预热
    /*** 模拟缓存预热** @param id* @param expireSeconds 过期时间*/public void saveShopToRedis(Long id, long expireSeconds) {if (Objects.isNull(id)) {return;}Shop shop = getById(id);RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));//未设置过期时间stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));}
    @Autowiredprivate ShopServiceImpl shopService;/*** 单测:缓存预热*/@Testpublic void saveShopToRedis() {shopService.saveShopToRedis(1L, 10L);}

逻辑过期时间
image.png
业务代码实现

    /*** 缓存重建线程池*/private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);/*** 查询商户信息* 逻辑过期解决缓存击穿问题** @param id* @return*/public Shop queryShopByLogicExpire(Long id) {if (Objects.isNull(id) || id < 0) {return null;}RedisData redisData = getRedisDataFromCache(id);JSONObject data = (JSONObject) redisData.getData();Shop shop = JSONUtil.toBean(data, Shop.class);//是否过期if (!cacheIsExpire(redisData)) {//未过期return shop;}//过期String lockKey = LOCK_SHOP_KEY + id;//获取互斥锁if (!tryLock(lockKey)) {//获取互斥锁失败,返回已经过期的商户信息return shop;}//获取锁成功redisData = getRedisDataFromCache(id);//Double Check 再次查看缓存是否过期if (!cacheIsExpire(redisData)) {//没过期,无需重建缓存return JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);}//开启独立线程进行缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {this.saveShopToRedis(id, 20L);} catch (Exception e) {throw new RuntimeException(e);} finally {//释放锁unLock(lockKey);}});return shop;}/*** 缓存是否过期** @param redisData* @return*/private boolean cacheIsExpire(RedisData redisData) {//是否过期 Double checkif (redisData.getExpireTime().isAfter(LocalDateTime.now())) {//未过期return false;}return true;}private RedisData getRedisDataFromCache(Long id) {String shopCacheKey = CACHE_SHOP_KEY + id;String shopJson = stringRedisTemplate.opsForValue().get(shopCacheKey);if (StringUtils.isBlank(shopJson)) {//不存在 直接返回return null;}return JSONUtil.toBean(shopJson, RedisData.class);}
压测

jmeter压测100QPS
查看运行结果
前面会返回旧数据
image.png
后面会返回新数据
image.png
数据会有短暂不一致的问题,但是保证了可用性。
image.png

封装缓存工具

image.png
代码

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;/*** @author zhangzengxiu* @date 2023/10/7*/
@Slf4j
@Component
public class CacheClient {@Autowiredprivate StringRedisTemplate stringRedisTemplate;/*** 缓存不存在的数据*/public static final String NULL_VAL = "-1";/*** 锁key前缀*/private static final String LOCK_KEY = "lock:";/*** 缓存重建线程池*/private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);/*** 设置缓存** @param key* @param value* @param expireTime* @param timeUnit*/public void set(String key, Object value, long expireTime, TimeUnit timeUnit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), expireTime, timeUnit);}/*** 逻辑过期时间** @param key* @param value* @param expireTime* @param timeUnit*/public void setLogicExpire(String key, Object value, long expireTime, TimeUnit timeUnit) {RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(expireTime)));stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}/*** 解决缓存穿透问题** @param id* @param keyPrefix* @param type* @param function* @param expireTime* @param timeUnit* @param <R>* @param <ID>* @return*/public <R, ID> R queryWithPassThrough(ID id, String keyPrefix, Class<R> type, Function<ID, R> function, long expireTime, TimeUnit timeUnit) {if (Objects.isNull(id)) {return null;}//从redis中查询缓存信息String cacheKey = keyPrefix + id;String json = stringRedisTemplate.opsForValue().get(cacheKey);if (StringUtils.isNotBlank(json)) {if (StringUtils.equals(NULL_VAL, json)) {//空值return null;}return JSONUtil.toBean(json, type);}//未命中缓存查询数据库R res = function.apply(id);if (Objects.isNull(res)) {//缓存空值 缓存2minthis.set(cacheKey, NULL_VAL, 2L, TimeUnit.MINUTES);return null;}//缓存添加过期时间this.set(cacheKey, JSONUtil.toJsonStr(res), expireTime, timeUnit);return res;}/*** 逻辑过期解决缓存击穿问题** @param id* @return*/public <R, ID> R queryByLogicExpire(String keyPrefix, ID id, Class<R> type, long expireTime, TimeUnit unit, Function<ID, R> function) {if (Objects.isNull(id)) {return null;}String cacheKey = keyPrefix + id;RedisData redisData = getRedisDataFromCache(cacheKey);JSONObject data = (JSONObject) redisData.getData();R res = JSONUtil.toBean(data, type);//是否过期if (!cacheIsExpire(redisData)) {//未过期return res;}//过期String lockKey = LOCK_KEY + id;//获取互斥锁if (!tryLock(lockKey)) {//获取互斥锁失败,返回已经过期的信息return res;}//获取锁成功redisData = getRedisDataFromCache(cacheKey);//Double Check 再次查看缓存是否过期if (!cacheIsExpire(redisData)) {//没过期,无需重建缓存return JSONUtil.toBean((JSONObject) redisData.getData(), type);}//开启独立线程进行缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {//查询DBR r = function.apply(id);//写入redisthis.setLogicExpire(lockKey, r, expireTime, unit);} catch (Exception e) {throw new RuntimeException(e);} finally {//释放锁unLock(lockKey);}});return res;}/*** 缓存是否过期** @param redisData* @return*/private boolean cacheIsExpire(RedisData redisData) {//是否过期 Double checkif (redisData.getExpireTime().isAfter(LocalDateTime.now())) {//未过期return false;}return true;}/*** 从缓存中获取RedisData** @param cacheKey* @return*/private RedisData getRedisDataFromCache(String cacheKey) {String shopJson = stringRedisTemplate.opsForValue().get(cacheKey);if (StringUtils.isBlank(shopJson)) {//不存在 直接返回return null;}return JSONUtil.toBean(shopJson, RedisData.class);}/*** 尝试获取锁** @param key* @return*/private boolean tryLock(String key) {//setnxBoolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);//自动拆箱 防止NPEreturn BooleanUtil.isTrue(flag);}/*** 释放锁** @param key*/private void unLock(String key) {stringRedisTemplate.delete(key);}}

使用方式

    @Overridepublic Result queryShopById(Long id) {//获取店铺信息 缓存穿透//Shop shop = queryShopByPassThrough(id);//使用工具类实现Shop shop = cacheClient.queryWithPassThrough(id, CACHE_SHOP_KEY, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);//互斥锁 缓存击穿//Shop shop = queryShopByMutex(id);//逻辑过期时间 解决缓存击穿问题//Shop shop = queryShopByLogicExpire(id);Shop shop = cacheClient.queryByLogicExpire(CACHE_SHOP_KEY, id, Shop.class, CACHE_SHOP_TTL, TimeUnit.MINUTES, this::getById);if (Objects.isNull(shop)) {return Result.fail("商户不存在!");}return Result.ok(shop);}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/151343.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【ElasticSearch】基于 Java 客户端 RestClient 实现对 ElasticSearch 索引库、文档的增删改查操作,以及文档的批量导入

文章目录 前言一、对 Java RestClient 的认识1.1 什么是 RestClient1.2 RestClient 核心类&#xff1a;RestHighLevelClient 二、使用 Java RestClient 操作索引库2.1 根据数据库表编写创建 ES 索引的 DSL 语句2.2 初始化 Java RestClient2.2.1 在 Spring Boot 项目中引入 Rest…

Ubuntu 20.04使用源码安装nginx 1.14.0

nginx安装及使用&#xff08;详细版&#xff09;是一篇参考博文。 http://nginx.org/download/可以选择下载源码的版本。 sudo wget http://nginx.org/download/nginx-1.14.0.tar.gz下载源代码。 sudo tar xzf nginx-1.14.0.tar.gz进行解压。 cd nginx-1.14.0进入到源代码…

Scala第十九章节

Scala第十九章节 scala总目录 文档资料下载 章节目标 了解Actor的相关概述掌握Actor发送和接收消息掌握WordCount案例 1. Actor介绍 Scala中的Actor并发编程模型可以用来开发比Java线程效率更高的并发程序。我们学习Scala Actor的目的主要是为后续学习Akka做准备。 1.1 Ja…

LabVIEW开发教学实验室自动化INL和DNL测试系统

LabVIEW开发教学实验室自动化INL和DNL测试系统 如今&#xff0c;几乎所有的测量仪器都是基于微处理器的设备。模拟输入量在进行数字处理之前被转换为数字量。对于参加电气和电子测量课程的学生来说&#xff0c;了解ADC以及如何欣赏其性能至关重要。ADC的不确定性可以根据其传输…

Unity Golang教程-Shader编写一个流动的云效果

创建目录 一个友好的项目&#xff0c;项目目录结构是很重要的。我们先导入一个登录界面模型资源。 我们先创建Art表示是美术类的资源&#xff0c;资源是模型创建Model文件夹&#xff0c;由于是在登录界面所以创建Login文件夹&#xff0c;下面依次是模型对应的资源&#xff0c…

世界前沿技术发展报告2023《世界信息技术发展报告》(六)网络与通信技术

&#xff08;六&#xff09;网络与通信技术 1. 概述2. 5G与光通讯2.1 美国研究人员利用电磁拓扑绝缘体使5G频谱带宽翻倍2.2 日本东京工业大学推出可接入5G网络的高频收发器2.3 美国得克萨斯农工大学通过波束管理改进5G毫米波通信2.4 联发科完成全球首次5G NTN卫星手机连线测试2…

自动定时删除磁盘文件的脚本(从文件日期最早的开始删)

#!/bin/bash# 指定的挂载点 MOUNTPOINT"/media/vm/MyDisk512GB"# 设置磁盘大小的限制 (例如&#xff1a;800G) LIMIT$((800 * 1024 * 1024)) # 单位是KB# 获取挂载点的已使用空间 USED_SPACE$(df -kP "$MOUNTPOINT" | tail -1 | awk {print $3})echo &quo…

【Oracle】Oracle系列十九--Oracle的体系结构

文章目录 往期回顾前言1. 物理结构2. 内存结构2.1 SGA2.2 后台进程 3. 逻辑结构 往期回顾 【Oracle】Oracle系列之一–Oracle数据类型 【Oracle】Oracle系列之二–Oracle数据字典 【Oracle】Oracle系列之三–Oracle字符集 【Oracle】Oracle系列之四–用户管理 【Oracle】Or…

应用案例 | dataFEED OPC Suite为化工行业中的质量控制和成本节约提供数据集成方案

一 背景 在当今化工行业中&#xff0c;质量控制对于特种塑料供应商至关重要。一家国际性的特种塑料供应商在全球拥有五个生产基地&#xff0c;每个基地都运行着2-6台塑料挤出机。为了确保塑料质量&#xff0c;他们需要每两小时分析一次挤出样品——导致这项工作占用了较大的生…

Bigemap是如何在生态林业科技行业去应用的

选择Bigemap的原因&#xff1a; ①之前一直是使用的谷歌地球&#xff0c;现在谷歌不能使用了就在网上搜索找一款可以替代的软件&#xff0c;工作使用需求还是挺大的&#xff0c;谷歌不能用对工作进展也非常影响&#xff0c;在网上搜索到软件大部分功能都可以满足需求 ②软件卫…

Tauri | 新版2.0路线图:更强大的插件以及支持 iOS、Android 应用构建

Tauri官方在9月7号发布了新版2.0的路线图&#xff0c;该版本主要是对移动端进行升级&#xff0c;主要特性如下&#xff1a; 强大的插件系统&#xff0c;官方把常用的功能进行了插件化&#xff08;见下图&#xff09;支持使用 Swift、Kotlin 编程语言开发插件&#xff0c;对 iO…

Nginx配置文件的通用语法介绍

要是参考《Ubuntu 20.04使用源码安装nginx 1.14.0》安装nginx的话&#xff0c;nginx配置文件在/nginx/conf目录里边&#xff0c;/nginx/conf里边的配置文件结构如下图所示&#xff1a; nginx.conf是主配置文件&#xff0c;它是一个ascii文本文件。配置文件由指令&#xff08;…

【Vue面试题九】、Vue中给对象添加新属性界面不刷新?

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 面试官&#xff1a;动态给vue的data添加一个…

C# OpenCvSharp Yolov8 Pose 姿态识别

效果 项目 代码 using OpenCvSharp; using OpenCvSharp.Dnn; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms;namespace OpenC…

【数据结构--八大排序】之希尔排序

&#x1f490; &#x1f338; &#x1f337; &#x1f340; &#x1f339; &#x1f33b; &#x1f33a; &#x1f341; &#x1f343; &#x1f342; &#x1f33f; &#x1f344;&#x1f35d; &#x1f35b; &#x1f364; &#x1f4c3;个人主页 &#xff1a;阿然成长日记 …

C++简单上手helloworld 以及 vscode找不到文件的可能性原因

helloworld #include <iostream>int main() {std::cout << "hello world!" << std::endl;return 0; }输入输出小功能 #include <iostream> using namespace std; /* *主函数 *输出一条语句 */int main() {// 输出一条语句cout << &q…

javaWeb宠物领养系统

一、引言 1.1 系统背景 计算机网络的发展&#xff0c;促进了社会各行业的进步&#xff0c;带来了经济快速增长。管理员通过流浪宠物的信息&#xff0c;在平台上和领养人进行实时的交流&#xff0c;达成领养协议。用户登录后&#xff0c;把想要领养的宠物向本平台发起申请&…

Python3操作Redis最新版|CRUD基本操作(保姆级)

Python3中类的高级语法及实战 Python3(基础|高级)语法实战(|多线程|多进程|线程池|进程池技术)|多线程安全问题解决方案 Python3数据科学包系列(一):数据分析实战 Python3数据科学包系列(二):数据分析实战 Python3数据科学包系列(三):数据分析实战 Win11查看安装的Python路…

【SpringBoot】| Thymeleaf 模板引擎

目录 Thymeleaf 模板引擎 1. 第一个例子 2. 表达式 ①标准变量表达式 ②选择变量表达式&#xff08;星号变量表达式&#xff09; ③链接表达式&#xff08;URL表达式&#xff09; 3. Thymeleaf的属性 ①th:action ②th:method ③th:href ④th:src ⑤th:text ⑥th:…

Swift SwiftUI CoreData 过滤数据 2

预览 Code import SwiftUI import CoreDatastruct HomeSearchView: View {Environment(\.dismiss) var dismissState private var search_value ""FetchRequest(entity: Bill.entity(),sortDescriptors: [NSSortDescriptor(keyPath: \Bill.c_at, ascending: false)…