黑马点评Redis实战(短信登录;商户查询缓存)

news2025/3/11 6:26:34

黑马点评

通过一个类似于大众点评的项目了解学习redis在实战项目中的使用,下面是项目中会涉及到的模块:
在这里插入图片描述

一、导入黑马点评项目

导入springboot项目,导入sql脚本到数据库,开启nginx,更改项目配置文件中的redis和mysql的地址
没什么好写的,跟着视频做,nginx目录不要包含中文。

二、登录模块

1.基于session实现登录

下面是session实现登录的流程图
在这里插入图片描述
将实现逻辑写在UserServiceImpl.java中

1.1 发送短信验证码功能

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1.校验手机号
        if(RegexUtils.isPhoneInvalid(phone)){
            //2.如果手机号不合法则返回错误信息
            return Result.fail("手机号不合法");
        }
        //3.如果手机号合法,使用hutool工具生成验证码
        String code = RandomUtil.randomNumbers(6);
        //4.保存验证码到session中
        session.setAttribute("code",code);
        //5.发送验证码
        //模拟短信验证码发送,实际会调用阿里云等第三方服务
        log.debug("发送验证码成功,验证码:{}",code);
        return Result.ok();
    }
}

1.2 短信验证码登录注册功能

/**
     * 短信验证码登录注册
     * @param loginForm
     * @param session
     * @return
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.校验手机号
        String phone = loginForm.getPhone();
        if(RegexUtils.isPhoneInvalid(phone)){
            //3.校验失败,返回错误信息
            return Result.fail("手机号格式不正确");
        }
        //2.校验验证码
        String code = loginForm.getCode();
        if(RegexUtils.isCodeInvalid(code)){
            //3.格式校验失败,返回错误信息
            return Result.fail("验证码格式不正确");
        }
        String cacheCode = (String) session.getAttribute("code");
        if(!code.equals(cacheCode)){
            return Result.fail("验证码错误");
        }
        //4.根据手机号查询用户
        LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(User::getPhone,phone);
        User user = baseMapper.selectOne(lambdaQueryWrapper);
        //5.用户不存在,创建新用户存在数据库
        if(user == null){
            User newUser = new User();
            newUser.setPhone(phone);
            newUser.setNickName(SystemConstants.USER_NICK_NAME_PREFIX+ RandomUtil.randomString(10));
            baseMapper.insert(newUser);
            session.setAttribute("user",newUser);
        }
        //6.用户存在
        session.setAttribute("user",user);
        //7.返回登录信息
        return Result.ok();//不需要返回登录凭证,因为这里是基于session实现的,
        //浏览器发起请求会携带cookie中的sessionId,然后tomcat通过sessionId找到对应session
    }

1.3 登录校验功能

在每次请求之前都需要校验请求是否有用户登录,我们在拦截器中做这个功能,并且将后续需要的用户信息存在ThreadLocal中,那么后面在每个线程中就可以获取到这些信息
在这里插入图片描述

1.3.1 编写一个登录拦截器
package com.hmdp.utils;
import com.hmdp.entity.User;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
 * @author Watching
 * * @date 2023/4/2
 * * Describe:登录拦截器
 */
@Component
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中的用户
        User user = (User) session.getAttribute("user");
        //3.判断用户是否存在
        if(user == null){
            response.setStatus(401);//返回状态信息
            return false;//拦截,禁止通行
        }
        //5.存在,保存用户信息到ThreadLocal中,这个UserHolder是我们自己封装的一个类
        UserHolder.saveUser(user);//将用户信息保存到ThreadLocal
        //6.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //移除ThreadLocal中的用户信息,避免内存泄漏(可以去了解ThreadLocal的原理)
        UserHolder.removeUser();
    }
}
//UserHoder类
package com.hmdp.utils;
import com.hmdp.entity.User;
public class UserHolder {
    private static final ThreadLocal<User> tl = new ThreadLocal<>();
    public static void saveUser(User user){
        tl.set(user);
    }
    public static User getUser(){
        return tl.get();
    }
    public static void removeUser(){
        tl.remove();
    }
}
1.3.2 注册登录拦截器
package com.hmdp.config;
import com.hmdp.utils.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
 * @author Watching
 * * @date 2023/4/2
 * * Describe:注册拦截器
 */
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Autowired
    LoginInterceptor loginInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    	//注册登录拦截器,并列出拦截白名单
        registry.addInterceptor(loginInterceptor).excludePathPatterns(
                "/shop/**",
                "/voucher/**",
                "/shop-type/**",
                "/upload/**",
                "/blog/hot",
                "/user/code",
                "/user/login"
        );
    }
}
1.3.3完成用户模块/user/me接口的编写
    /**
     * 很多项目中需要在代码中使用当前登录用户的信息,但是又不方便把保存用户信息的session对象传来传去,
     * 这种情况下,就可以考虑使用 ThreadLocal
     * @return
     */
    @GetMapping("/me")
    public Result me(){
        // TODO 获取当前登录的用户并返回
        UserDTO user = UserHolder.getUser();
        return Result.ok(user);
    }

1.4 隐藏用户敏感信息

在上面的代码中,我们在登录接口中根据手机号将用户信息从DB中查出来并存在了session
在这里插入图片描述
然后在登录拦截器中又将session中的用户信息存在了ThreadLocal中
在这里插入图片描述
在/user/me接口将信息全部返回给了前端。
在这里插入图片描述

这样会造成一个问题,就是用户的所有信息都被返回给了前端,包括用户的密码等敏感信息,这样肯定是不行的,所以我们需要在登录时仅仅将非敏感信息存进session,并且返回。
我们可以创建UserDTO,用于其中的字段为前端所必须的用户信息,但不包括敏感信息,然后将用户信息封装进入UserDTO中后再返回。

2.集群的session共享问题

session共享问题:多台tomcat服务器之间并不共享session存储空间,当请求切换到不同的tomcat服务器时导致数据丢失的问题。
在这里插入图片描述
tomcat本身提供了一个session复制的方案,各个tomcat服务器之间会互相拷贝session。这样看似解决了session共享问题,但是又出现了几个新的问题
1.每个tomcat内存中都保存了同样的session,造成资源浪费。
2.tomcat彼此拷贝session的时候是存在延迟的,如果用户在延迟的这段时间内再次请求,还是会造成上面的情况。
所以我们需要一个替代方案,这个方案需要满足以下几个条件
1.数据共享
2.内存存储(速度快,安全
3.key-value结构
感觉答案已经呼之欲出了,这不就是redis的特点吗?只要将用户信息存在redis中,然后各个tomcat服务器去存取就可以了。
在这里插入图片描述

3.基于redis实现共享session登录

流程图
在这里插入图片描述

3.1 使用redis代替session
3.1.1 发送验证码功能,改写sendCode方法
    /**
     * 获取手机验证码功能
     *
     * @param phone
     * @param session
     * @return
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1.校验手机格式,符合/不符合
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式错误!");
        }
        String code = RandomUtil.randomNumbers(6);//生成验证码
        //2.保存验证码到redis,
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
        //3.发送验证码
        log.debug("发送短信验证码成功,验证码:{}", code);
        //4.返回值:OK
        return Result.ok();
    }
3.1.2 短信验证登录注册功能,改写login方法
/**
     * 短信验证码登录注册
     *
     * @param loginForm
     * @param session
     * @return
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.校验手机号和验证码
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) return Result.fail("手机号格式错误!");
        //1.2获取验证码并校验
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();
        if (cacheCode == null || !cacheCode.equals(code)) {//2.不一致报错
            return Result.fail("验证码错误");
        }
        //3.一致,查询用户是否已经注册,是/否 select * from tb_user where phone = ?
        User user = query().eq("phone", phone).one();
        //4否,创建用户并保存
        if (user == null) {
            user = createUserWithPhone(phone);
        }
        //5是,保持用户信息到redis
        //5.1随机生成32位数字字符token作为登录令牌
        String token = UUID.randomUUID().toString();
        //5.2将User对象转为Hash存储
        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()));
                        //使用CopyOptions参数避免Long转String异常
        //5.3往redis存储用户信息
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        //5.3.1设置token有效期
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.SECONDS);
        //6返回token
        return Result.ok(token);
    }
    
    private User createUserWithPhone(String phone) {
        //1.创建用户
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomNumbers(10));
        //2.保存用户
        save(user);
        return user;
    }
3.1.3 登录校验功能,改写登录拦截器
package com.hmdp.utils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author Watching
 * * @date 2023/4/2
 * * Describe:登录拦截器
 */
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.获取session,改为获取请求头中的token
//        HttpSession session = request.getSession();
        String token = request.getHeader("authorization");
        if(StrUtil.isBlank(token)){
        	response.setStatus(401);
            return false;
        }
        //2.获取session中的用户,根据token获取redis中的用户信息
//        Object user = session.getAttribute("user");
        Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
        //3.判断用户是否存在
        if(entries.isEmpty()){
           response.setStatus(401);
           return false;
        }
        //5.存在,保存用户信息到ThreadLocal中
//        UserHolder.saveUser((UserDTO)user);//将用户信息保存到ThreadLocal
        UserDTO userDTO = BeanUtil.fillBeanWithMap(entries, new UserDTO(), false);
        UserHolder.saveUser(userDTO);
        //TODO 放行之前要刷新token的有效期
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token,RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);
        //6.放行
        return true;
    }
        
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //移除ThreadLocal中的用户信息,避免内存泄漏(可以去了解ThreadLocal的原理)
        UserHolder.removeUser();
    }
}

3.2 解决登录状态刷新问题

我们上面对token的刷新操作存在一个问题
我们在拦截器LoginInteceptor中对token进行了一个刷新,但是这个拦截器是排除了很多路径的,所以当用户登录后,他访问被排除的这些路径请求,token是不会刷新的。
而我们的要求是,用户的每次请求都会刷新token
解决方法:在LoginInteceptor执行之前再添加一个拦截器,进行token刷新,这样的话每次请求都会对token进行刷新。
在这里插入图片描述

3.2.1 编写一个token刷新拦截器RefreshTokenInteceptor

RefreshTokenInteceptor不需要对请求进行拦截,只需要完成以下几个要求:
1.获取前端传来的token
2.根据token从redis中查询用户信息
3.如果从redis中查询出来的用户信息不为空,则存在ThreadLocal中
4.如果从redis中查询出来的用户信息不为空,则刷新redis中的token有效期
5.放行所有请求

/**
 * @author Watching
 * * @date 2023/4/2
 * * Describe:用于拦截所有请求,保证用户的请求都会刷新token有效期
 */
@Component
public class RefreshTokenInterceptor implements HandlerInterceptor {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.获取session,改为获取请求头中的token
//        HttpSession session = request.getSession();
        String token = request.getHeader("authorization");
        if(StrUtil.isBlank(token)){
            return true;
        }
        //2.获取session中的用户,根据token获取redis中的用户信息
//        Object user = session.getAttribute("user");
        Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
        //3.判断用户是否存在
        if(entries.isEmpty()){
           return true;
        }
        //5.存在,保存用户信息到ThreadLocal中
//        UserHolder.saveUser((UserDTO)user);//将用户信息保存到ThreadLocal
        UserDTO userDTO = BeanUtil.fillBeanWithMap(entries, new UserDTO(), false);
        UserHolder.saveUser(userDTO);
        //TODO 放行之前要刷新token的有效期
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token,RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);
        //6.放行
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //移除ThreadLocal中的用户信息,避免内存泄漏(可以去了解ThreadLocal的原理)
        UserHolder.removeUser();
    }
}
3.2.2 改写LoginInterceptor

因为很多操作都在RefreshTokenInterceptor中执行了,所以登录拦截器中只需要从localthread中取数据,并判断是否为空就行。
如果没有用户,说明当前没有用户登录,所以直接拦截请求返回401
如果存在用户,说明当前有用户登录,放行

/**
 * @author Watching
 * * @date 2023/4/2
 * * Describe:登录拦截器
 */
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
       //1.判断是否需要拦截(ThreadLocal中是否有用户)
        if(UserHolder.getUser() == null){
            //没有用户,则说明未登录,拦截
            response.setStatus(401);
            return false;
        }
        //不为空,有用户,则放行
        return true;
    }
}

三、商户查询缓存

1.什么是缓存?

缓存就是数据交换的缓冲区( 称作Cache [kaef),是存贮数据的临时地方,一般读写性能较高。
缓存的作用:

  • 提高读写效率,降低响应时间
  • 降低后端负载
    缓存的成本:
  • 数据一致性成本,要保证数据库中的数据和缓存中的数据保持一致
  • 代码维护成本
  • 运维成本,比如集群搭建

2.商户信息添加缓存

为查询商户信息添加缓存
在这里插入图片描述

    /**
     * 根据商户id查询商户信息,并缓存在redis中
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        //1.从redis查询商铺信息
        Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(RedisConstants.CACHE_SHOP_KEY + id);
        Shop shop = BeanUtil.fillBeanWithMap(entries, new Shop(), false);
        //2.判断是否存在
        if (!entries.isEmpty()) {
            //3.存在直接返回
            return Result.ok(shop);
        }
        //4.不存在,根据id查询DB
        Shop dbShop = baseMapper.selectById(id);
        //5.DB中也不存在,返回错误
        if (dbShop == null) {
            return Result.fail("商户不存在");
        }
        //6.将查询到的数据存在redis缓存中
        Map<String, Object> map = BeanUtil.beanToMap(dbShop, new HashMap<>(), CopyOptions.create()
                .setIgnoreNullValue(true)
                .setFieldValueEditor((fieldName, fieldValue) -> {
                            if (fieldValue != null) {//需要判空,否则空指针
                                fieldValue += "";
                            }
                            return fieldValue;
                        }
                )
        );
       stringRedisTemplate.opsForHash().putAll(RedisConstants.CACHE_SHOP_KEY + id, map);
        //7.返回数据
        return Result.ok(dbShop);
    }

重点就是使用hash存储时,需要将bean转为hash,转的时候需要使用CopyOptions将shop的属性转为String类型,因为我们使用的是StringRedisTemplate。使用CopyOptions的时候还需要注意空值的问题。
注意
建议是大部分情况下使用 String 存储就好,毕竟在存储具有多层嵌套的对象时方便很多,占用的空间也比 Hash 小。当我们需要存储一个特别大的对象时,而且在大多数情况中只需要访问该对象少量的字段时,可以考虑使用 Hash。

3.为商铺分类列表添加缓存

首页的商户分类列表也是需要从DB查询的,而且每人每次访问首页都会访问DB,这样对DB的压力是很大的,所以也需要将他们放入缓存中
在这里插入图片描述

    /**
     * 查询商户类型列表并存储在redis中
     * @return
     */
    @Override
    public Result getTypeList() {
        //1.查询redis中是否有数据
        String shopTypeStr = stringRedisTemplate.opsForValue().get(RedisConstants.SHOP_TYPE_KEY);
        //2.如果有,直接返回
        if (!StrUtil.isBlank(shopTypeStr)) {
           List<ShopType> shopTypes = JSONUtil.toList(shopTypeStr, ShopType.class);//还要将json字符串转换为对象再传入ok()函数,否则前端无法解析字符串会报错
            return Result.ok(shopTypes);
        }
        //3.如果没有,则查询DB
        List<ShopType> shopTypes = baseMapper.selectList(null);
        //4.如果DB中没有,返回错误信息
        if(shopTypes.isEmpty()){
            return Result.fail("商户类型数据为空");
        }
        //5.DB中有,则放入Redis缓存
        String parse = String.valueOf(JSONUtil.parse(shopTypes));
        stringRedisTemplate.opsForValue().set(RedisConstants.SHOP_TYPE_KEY,parse);
        //6.返回数据
        return Result.ok(shopTypes);
    }

这里没有什么需要特别注意的地方,但是redis存储类型我们可以斟酌一下,因为商户类型是一个list,我们可以使用String,List等数据类型。

============2023/4/3更新bug

在存放Result.ok(Object)数据时,我之前存放的是json字符串,而mvc会自动将对象转成字符串传给前端,导致字符串再被转了一边json,所以前端解析不了,显示undefined,导致商户分类无法显示图片。只需要将json字符串转为对象,再放到Result.ok()函数中就正确了。

四、缓存更新策略

1.缓存更新策略;内存淘汰;超时剔除;主动更新;

为了保证缓存与数据库的一致性,我们需要使用使用一些缓存更新策略,有以下三种:
在这里插入图片描述

  • 内存淘汰是指redis在检测到内存已经满了之后会自动删除一些数据,来腾出空间,但是这样很难保证数据一致性
  • 超时剔除是指我们在存数据的时候为数据设置过期时间,到期自动删除,这样和内存淘汰策略一样存在一个问题,就是在数据库数据发生改变后,redis中的数据并没有到过期时间,在过期之前,为前端返回的都是过期数据。
  • 主动更新是指我们在修改数据库的同时,主动修改缓存数据,这样就可以保证缓存数据和数据库数据的一致性

2.主动更新

主动更新是需要我们每次修改数据库的时候手动对象缓存进行操作的,常见的三种主动更新策略
在这里插入图片描述
综合考虑我们会选择第一种。主动更新策略

操作数据库和缓存时,我们需要考虑三个问题:
1.删除缓存还是更新缓存?
①如果更新缓存,那么每次更新数据库都更新缓存,如果我们多次更新数据库,就需要同步更新多次缓存,但是实际只有最后一次更新缓存操作有效。
②如果删除缓存,那么下次查询会直接访问数据库,然后更新缓存,无效操作较少。
2.如何保证缓存操作和数据库操作同时成功或同时失败?
①如果是单体应用,我们可以使用事务,将缓存操作和数据库操作放在同一个事务中
②如果是分布式应用我们就需要使用TCC等分布式事务方案
3.先删除缓存还是先删除数据库?
两种删除方案都可以,但是建议使用先删除数据库
们来模拟一下先删缓存,在线程1删除缓存且还未更新数据库的时候,线程2进来查询缓存,未命中,直接就去查询数据库,并且将数据库中的数据存在缓存中。但是!此时数据库的数据还没更新,导致缓存中的数据是错误的。
在这里插入图片描述
们再来模拟先删数据库,线程1在查询缓存时,缓存恰好失效,那么线程1就去查询数据库,然后写入缓存。线程2更新数据库并删除缓存,但是由于线程1写入缓存是在线程2结束之后,所以缓存中也存放了过期的数据
在这里插入图片描述
先删除数据库数据还是先删除缓存数据

3.给查询商铺的缓存添加超时剔除和主动更新策略

根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间。这里只需要加一行代码就行
在这里插入图片描述

根据id修改店铺时,先修改数据库,再删除缓存
注意点:我们存商户信息时是使用的hash数据结构,删除hash类型数据时,如果要删除的是该key下的所有数据,应该直接使用stringRedisTemplate的delete,而不是opsForxxx下的delete

    /**
     * 更新商铺信息
     * 更新数据库的同时还要修改缓存数据
     * @param shop
     * @return
     */
    @Override
    @Transactional
    public Result update(Shop shop) {
        Long id = shop.getId();
        if(id==null){
            return Result.fail("商户id为空");
        }
        //1.更新数据库
        baseMapper.updateById(shop);
        //2.删除缓存
        String key = RedisConstants.CACHE_SHOP_KEY + shop.getId();
        stringRedisTemplate.delete(key);//删除hash类型数据时,如果要删除的是该key下的所有数据,应该直接使用stringRedisTemplate的delete,而不是opsForxxx下的delete
        return Result.ok();
    }
}

五、缓存穿透、缓存击穿、缓存雪崩

1.缓存穿透

缓存穿透是指客户端请求的数据在数据库和缓存中都不存在,这样的话缓存永远不会生效,这些请求都会打的数据库。
两种解决方案:

  • 缓存空对象
    优点:实现简单,维护方便
    缺点
    造成额外的内存消耗,因为会缓存很多空值(可以通过对空值设置较短的过期时间解决)
    可能会造成短期不一致,比如当我们缓存空值的时候,数据库真的插入了一条不为空的数据,但是此时我们在缓存中缓存的却是空值,只有当空值过期被删除后才能缓存真正的值,所以造成了短暂不一致。
    在这里插入图片描述
  • 布隆过滤
    优点:内存占用较少,没有多余的key
    缺点:①实现复杂②存在误判可能
    在这里插入图片描述

1.1 使用缓存空值解决缓存商户信息时的缓存击穿问题

在这里插入图片描述
使用缓存空值解决缓存商户信息缓存击穿问题,需要在原有的缓存操作上添加两个操作:
①查询数据库发现无数据后,将空值缓存进redis中 (5.1步)
②查询缓存命中后,判断是否是空值,如果是空值则直接返回空,如果不是空值则返回商户信息。(2.1步)
因为我们使用的是hash结构存储商户信息,所以在做缓存空值时无法像String结构那样直接缓存null,使用String结构可以看看视频
预防缓存击穿(String结构)

    /**
     * 根据商户id查询商户信息,并缓存在redis中
     *
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        HashOperations<String, Object, Object> ops = stringRedisTemplate.opsForHash();
        //1.从redis查询商铺信息
        Map<Object, Object> entries = ops.entries(RedisConstants.CACHE_SHOP_KEY + id);
        Shop shop = BeanUtil.fillBeanWithMap(entries, new Shop(), false);
        //2.判断是否存在
        if (!entries.isEmpty()) {
            //2.1判断是否是我们为了防止缓存穿透放的一个空键值对
            if(entries.size() == 1){
                return Result.fail("商户不存在(id无法匹配)");
            }
            //3.存在直接返回
            return Result.ok(shop);
        }
        entries.size();
        //4.不存在,根据id查询DB
        Shop dbShop = baseMapper.selectById(id);
        //5.DB中也不存在,返回错误
        if (dbShop == null) {
            //5.1如果不存在,则缓存一个空值,并设置一个较短的有效期,避免缓存击穿
            ops.put(RedisConstants.CACHE_SHOP_KEY+id,"","");
            stringRedisTemplate.expire(RedisConstants.CACHE_SHOP_KEY+id,RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
            return Result.fail("商户不存在(id无法匹配)");
        }
        //6.将查询到的数据存在redis缓存中
        Map<String, Object> map = BeanUtil.beanToMap(dbShop, new HashMap<>(), CopyOptions.create()
                .setIgnoreNullValue(true)
                .setFieldValueEditor((fieldName, fieldValue) -> {
                            if (fieldValue != null) {//需要判空,否则空指针
                                fieldValue += "";
                            }
                            return fieldValue;
                        }
                )
        );
        ops.putAll(RedisConstants.CACHE_SHOP_KEY + id, map);
        stringRedisTemplate.expire(RedisConstants.CACHE_SHOP_KEY + id, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //7.返回数据
        return Result.ok(dbShop);
    }

上面的缓存空值、布隆过滤器都是被动预防换尺寸穿透,下面还有一些主动预防缓存穿透的方法:

  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

2.缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,给DB带来巨大压力。
在这里插入图片描述
解决方案:

  • 给不同的key的TTL添加随机值(解决key失效
  • 利用redis集群提高服务可用性(解决redis宕机
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

3.缓存击穿

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

发生条件

  • 高并发
  • 缓存重建耗时较长
    比如一个热点key失效之后,大量线程进入,访问缓存后都会未命中,都会直接取查询数据库,导致DB压力过大。
    在这里插入图片描述
    常见的两种解决方案:
  • 互斥锁
  • 逻辑过期

3.1 互斥锁

线程1在查询缓存未命中之后会获取互斥锁,然后进行DB查询重建缓存。如果此时有线程2进入,线程2会去查缓存,如果未命中也会尝试去获取锁,具体流程看下图:
在这里插入图片描述
但是这样使用互斥锁会有一个问题,就是大量的线程都会因为获取不到互斥锁而等待,直到获取到锁的线程完成缓存重建,这样的效率是比较低的。

3.1.1 基于互斥锁解决缓存击穿问题

在这里插入图片描述
在解决缓存穿透的代码的基础上,添加互斥锁解决缓存击穿问题

    /**
     * 根据商户id查询商户信息,并缓存在redis中
     * 预防缓存穿透和缓存击穿
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        //获取商铺信息,并且在queryWithPassThrough方法中预防了缓存穿透
//        Shop shop = queryWithPassThrough(id);
        //预防了缓存穿透并使用互斥锁解决缓存击穿
        Shop shop = queryWithMutex(id);
        if (shop != null) {
            return Result.ok(shop);
        }
        return Result.fail("商家不存在");
    }

编写queryWithMutex()方法解决。

/**
     * 使用互斥锁解决缓存击穿
     *
     * @param id
     * @return
     */
    public Shop queryWithMutex(Long id) {
        HashOperations<String, Object, Object> ops = stringRedisTemplate.opsForHash();
        //1.从redis查询商铺信息
        Map<Object, Object> entries = ops.entries(RedisConstants.CACHE_SHOP_KEY + id);
        Shop shop = BeanUtil.fillBeanWithMap(entries, new Shop(), false);
        //2.判断是否存在
        if (!entries.isEmpty()) {
            //2.1 判断是否存在我们为了防止缓存穿透放的一个空键值对
            if (entries.size() == 1) {
                return null;
            }
            //3.存在直接返回
            return shop;
        }
        //4.不存在 实现缓存重建
        //4.1 获取互斥锁
        Shop dbShop = null;
        try {
            boolean isLock = tryLock(RedisConstants.LOCK_SHOP_KEY);
            //4.2 判断是否获取成功
            if (!isLock) {
                //4.3 失败,则休眠并重试
                Thread.sleep(50);
                return queryWithMutex(id);
            }
            //4.4 成功 则根据id从数据库中查询
            //获取锁成功,再次检测redis缓存是否存在 做一个DoubleCheck,
//            Map<Object, Object> entries1 = ops.entries(RedisConstants.CACHE_SHOP_KEY + id);
//            Shop shop1 = BeanUtil.fillBeanWithMap(entries, new Shop(), false);
//            //2.判断是否存在
//            if (!entries1.isEmpty()) {
//                //2.1 判断是否存在我们为了防止缓存穿透放的一个空键值对
//                if (entries1.size() == 1) {
//                    return null;
//                }
//                //3.存在直接返回
//                return shop1;
//            }
            dbShop = baseMapper.selectById(id);

            //模拟重建的耗时
            Thread.sleep(200);

            //5.DB中也不存在,返回错误
            if (dbShop == null) {
                //5.1如果不存在,则缓存一个空值,并设置一个较短的有效期,避免缓存击穿
                ops.put(RedisConstants.CACHE_SHOP_KEY + id, "", "");
                stringRedisTemplate.expire(RedisConstants.CACHE_SHOP_KEY + id, RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            //6.将查询到的数据存在redis缓存中
            Map<String, Object> map = BeanUtil.beanToMap(dbShop, new HashMap<>(), CopyOptions.create()
                    .setIgnoreNullValue(true)
                    .setFieldValueEditor((fieldName, fieldValue) -> {
                                if (fieldValue != null) {//需要判空,否则空指针
                                    fieldValue += "";
                                }
                                return fieldValue;
                            }
                    )
            );
            ops.putAll(RedisConstants.CACHE_SHOP_KEY + id, map);
            stringRedisTemplate.expire(RedisConstants.CACHE_SHOP_KEY + id, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //7.释放互斥锁
            stringRedisTemplate.delete(RedisConstants.LOCK_SHOP_KEY);
        }
        //8.返回数据
        return dbShop;
    }

3.2 逻辑过期

线程1查询缓存未命中后,它也会获取一个互斥锁,并开启新线程2去做缓存重建工作,然后线程1直接返回过期数据。线程3在查询缓存未命中后会尝试获取互斥锁重建缓存,但是获取失败后就会直接返回过期数据,不会循环获取锁,就不会阻塞。
在这里插入图片描述

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

在这里插入图片描述
以下是代码示范:

    /**
     * 使用逻辑过期解决缓存击穿问题
     *
     * @param id
     * @return
     */
    //创建一个线程池用于重建缓存
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public Shop queryWithLogicExpire(Long id) {
        HashOperations<String, Object, Object> ops = stringRedisTemplate.opsForHash();
        //1.从redis查询商铺信息
        Map<Object, Object> entries = ops.entries(RedisConstants.CACHE_SHOP_KEY + id);
        Shop shop = BeanUtil.fillBeanWithMap(entries, new Shop(), false);
        //2.判断是否命中
        if (entries.isEmpty()) {
            //3.未命中,返回空
            return null;
        }
        //4.命中,获取逻辑过期字段
        //获取店铺数据
        String str = (String) stringRedisTemplate.opsForHash().get(RedisConstants.CACHE_SHOP_KEY + id, "data");
        Shop data = JSONUtil.toBean(str, Shop.class);
        //获取逻辑过期时间
        Object o =  stringRedisTemplate.opsForHash().get(RedisConstants.CACHE_SHOP_KEY + id, "expireTime");
        String s = o.toString();
        LocalDateTime expireTime = LocalDateTime.parse(s);
        //5.根据逻辑过期字段判断数据是否过期
        //5.1未过期,直接返回数据
        if (expireTime.isAfter(LocalDateTime.now())) {
            return data;
        }
        //5.2过期,需要缓存重建
        //6.缓存重建
        //6.1获取互斥锁
        boolean b = tryLock(RedisConstants.LOCK_SHOP_KEY + id);
        //6.2判断是否取锁成功
        //6.3成功,开启单独线程进行缓存重建
        if (b) {
             //TODO 获取锁成功之后应该再次检查redis缓存是否过期,做DOUBLE_CHECK,如果缓存未过期就不需要重建了,因为获取到的锁可能是其它线程重建线程完成之后刚释放的锁,而当前线程还不知道缓存已经被重建了
            //开启线程进行缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    this.saveShop2Redis(id, 20L);
                } catch (Exception e) {
                    throw new RuntimeException();
                } finally {//解锁要在finally中,保证肯定会被解锁
                    unLock(RedisConstants.LOCK_SHOP_KEY + id);
                }
            });
        }
        //6.4失败,直接返回过期数据
        return data;
    }

    /**
     * 数据预热
     *
     * @param id
     * @param expireTime
     */
    public void saveShop2Redis(Long id, Long expireTime) {
        //模拟缓存重建耗时
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //1.从DB查询店铺数据
        Shop shop = baseMapper.selectById(id);
        //2.封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        //获取当前时间并添加一段时间,单位为second
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));
        //3.写入redis
        Map<String, Object> map = new HashMap<>();
        BeanUtil.beanToMap(redisData, map, CopyOptions.create()
                .setIgnoreNullValue(true)
                .setFieldValueEditor((fieldName, fieldValue) -> {
                    String s = "";
                    //判断属性的类型,如果不是LocalDateTime,则说明是引用数据类型(Object),则转为json字符串
                    //如果是LocalDateTime类型,则保持原样
                    if (!(fieldValue instanceof LocalDateTime)) {
                        s = JSONUtil.toJsonStr(fieldValue);
                    }else {
                        s = fieldValue.toString();
                    }
                    return s;
                })
        );
        stringRedisTemplate.opsForHash().putAll(RedisConstants.CACHE_SHOP_KEY + id, map);
    }

上面这两段代码主要需要注意的地方有以下几点:

  • 因为redis使用的存储结构是hash,所以在往里存数据和取数据的时候要注意数据类型之间的转换,比如在saveShop2Redis() 方法中,要根据数据类型进行判断,来决定存入redis的数据类型(data用Json格式,LocaldateTime用字符串形式)。然后往外取的时候方便使用JSONUtil工具直接将json格式的data数据转换为目标对象,将字符串格式的LocaldateTime通过 LocaldateTime.parse(String s) 方法转换为LocaldateTime类型的对象。
  • 一些热点数据是需要预热的,所以在未命中缓存的时候直接返回null就行了,不需要去查询DB来构建缓存,所以也就不存在缓存穿透问题了。
  • 在另外的线程中获取互斥锁重建缓存后,需要在finally中解锁。
  • //TODO 获取锁成功之后应该再次检查redis缓存是否过期,做DOUBLE_CHECK,如果缓存未过期就不需要重建了,因为获取到的锁可能是其它线程重建线程完成之后刚释放的锁,而当前线程还不知道缓存已经被重建了

3.3 互斥锁和逻辑过期的对比

在这里插入图片描述
互斥锁死锁是指当一个业务需要获取多个缓存锁,但是锁却在另外一个业务里,彼此都需要被对方持有的锁,这样就会死锁。

4.封装一个redis操作类

这个类中对查询预防缓存穿透和逻辑过期预防缓存击穿做了封装,并且这两个方法使用了泛型,支持缓存任意类型的数据

package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import com.hmdp.service.impl.ShopServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
/**
 * @author Watching
 * * @date 2023/4/7
 * * Describe:封装redis工具类
 * 默认redis存储结构为String
 */
@Component
@Slf4j
public class CacheClient {
    @Autowired
    private ShopServiceImpl shopService;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    public CacheClient() {
    }
    /**
     * 写入redis
     *
     * @param key
     * @param value
     * @param time
     * @param unit
     */
    public void set(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    /**
     * 向redis中添加数据,并添加逻辑过期字段
     *
     * @param key
     * @param value
     * @param time
     * @param unit
     */
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
        RedisData redisData = new RedisData();
        redisData.setData(value);
        //使用unit.toSeconds(time)将传来的单位换算成秒
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        //写入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }
    /**
     * 预防缓存穿透
     *
     * @param id
     * @return
     */
    public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> doFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        //1.从redis查询商铺信息
        String json = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if (StrUtil.isNotBlank(json)) {
            //3.存在直接返回
            return JSONUtil.toBean(json, type);
        }
        //判断返回的是否是一个空值,如果是则返回错误信息
        if (json != null) {//不是null,说明是个空字符串""
            return null;
        }

        //4.不存在,根据id查询DB, 函数式编程,数据库数据由调用者主动提供
        R apply = doFallback.apply(id);
        //5.DB中也不存在,返回错误
        if (apply == null) {
            //5.1如果不存在,则缓存一个空值,并设置一个较短的有效期,避免缓存击穿
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
            //返回错误信息
            return null;
        }
        //6.将查询到的数据存在redis缓存中
        this.set(key, apply, time, unit);
        //7.返回数据
        return apply;
    }
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    /**
     * 逻辑过期预防缓存击穿
     */
    public <R, ID> R queryWithLogicExpire(String prefix, ID id, Class<R> type, Function<ID, R> doFallback, Long time, TimeUnit unit) {
        String key = prefix + id;
        //1.从redis查询商铺信息
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否命中
        if (StrUtil.isBlank(shopJson)) {
            //3.未命中,返回空
            return null;
        }
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        //获取RedisData中保存的数据
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        //4.命中,获取逻辑过期字段
        LocalDateTime expireTime = redisData.getExpireTime();
        //5.根据逻辑过期字段判断数据是否过期
        //5.1未过期,直接返回当前数据
        if (expireTime.isAfter(LocalDateTime.now())) {
            return r;
        }
        //5.2过期,需要缓存重建
        //6.缓存重建
        //6.1获取互斥锁
        boolean b = tryLock(RedisConstants.LOCK_SHOP_KEY);
        //6.2判断是否取锁成功
        //6.3成功,开启单独线程进行缓存重建
        if (b) {
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    Thread.sleep(500);
                    //查询数据库
                    R apply = doFallback.apply(id);
                    //写入redis
                    setWithLogicalExpire(key, apply, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException();
                } finally {
                    unLock(RedisConstants.LOCK_SHOP_KEY);
                }
            });
        }
        //6.4失败,直接返回过期数据
        return r;
    }

    /**
     * 尝试获取互斥锁
     *
     * @return
     */
    private boolean tryLock(String key) {
        //使用setIfAbsent()来执行创建缓存操作,setIfAbsent是redis命令setNX的java函数,只有当缓存中没有该key存在时才会插入成功
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        //使用hutool的工具来判断包装类,预防空指针异常
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 解锁(删除缓存中的锁
     */
    private void unLock(String key) {
        //删除缓存中的锁
        stringRedisTemplate.delete(key);
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

头部证券公司安全体系搭建实战讲解—开源网安S-SDLC平台助力金融科技安全发展

数字化时代背景下&#xff0c;新兴技术广泛应用导致软件安全隐患不断扩大。而金融行业由于项目周期长、业务规模大、应用数量多、合规监管严、内外合作多等特性&#xff0c;进一步加重了安全风险。 与此同时&#xff0c;《等保2.0》、《网络安全法》等国家政策的发布&#xff0…

【排序】直接插入排序与希尔排序(图示详解哦)

全文目录 引言直接插入排序思路实现 希尔排序思路实现 总结 引言 在上一篇文章中&#xff0c;我们实现了选择排序与堆排序&#xff0c;在本篇文章中将继续介绍直接插入排序与希尔排序&#xff1a; 直接插入排序与希尔排序都属于插入排序的一种&#xff1a; 这两种排序的思想都…

Ae:摄像机设置

Ae菜单&#xff1a;图层/摄像机设置 Camera Settings 快捷键&#xff1a;Ctrl Shift Y 新建摄像机图层时&#xff0c;首先会弹出摄像机设置 Camera Settings对话框。 经典 3D 渲染器时的摄像机设置 Cinema 4D 渲染器时的摄像机设置 类型 Type 有两种类型的摄像机供选择。 提…

空间矢量数据保存为GeoJSON、PDF等文件格式

专注系列化、高质量的R语言教程 推文索引 | 联系小编 | 付费合集 我们使用的空间矢量数据一般是Shapefile格式的&#xff0c;它在ArcGIS、R语言中都能加载&#xff0c;但是这种数据格式在使用时也有不便&#xff1a;它是由多个文件构成的&#xff0c;一般有.shp、.shx、.dbf、.…

Session和Cookie区别介绍+面试题

Session 会话&#xff1a; 对应的英文单词&#xff1a;session用户打开浏览器&#xff0c;进行一系列操作&#xff0c;然后关闭浏览器。整个过程叫做一次会话一个会话包含多次请求 session机制属于B/S结构的一部分&#xff0c;主要的作用就是为了保存会话状态。(用户登录成功后…

Spring使用注解存储和读取对象

文章目录 一、存储Bean对象配置扫描添加注解存储Bean对象注解使用范围Bean的命名五大类注解的关系为什么需要五大类注解? 二、方法注解BeanBean重命名 三、对象注入属性注入Setter注入构造方法注入Autowired 和 Resource 的区别 一、存储Bean对象 之前我们存储Bean时&#xff…

【MATLAB基础绘图第2棒】绘制柱状/饼图填充图

MATLAB绘制柱状填充图 方法1&#xff1a;hatchfill2工具1.1 案例1&#xff1a;柱状图填充1.2 案例2&#xff1a;饼图填充 方法2&#xff1a;applyhatch函数2.1 案例1&#xff1a;柱状图填充2.2 案例2&#xff1a;饼图填充 方法3&#xff1a; applyhatch_plusC函数3.1 案例1&…

分析软件及其隐藏后门实验笔记

软件后门和软件加壳是什么 软件后门可以理解为在软件中植入病毒等具有后门功能的代码&#xff0c;通过运行软件来对用户的系统造成破坏、窃取机密等。 软件加壳一种常用的方式是在二进制的程序中植入一段代码&#xff0c;在运行的时候优先取得程序的控制权&#xff0c;之后再把…

Cisco SD-WAN (Viptela) version 20.11.1 ED - 软件定义广域网

请访问原文链接&#xff1a;https://sysin.org/blog/cisco-sd-wan-20/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;sysin.org 支持 SASE 的架构&#xff0c;其集成了面向多云、安全、统一通信和应用优化的各种功能&#xff0c;可用于轻…

ChatGPT干掉程序员?想多了...

GPT-4才诞生没几天&#xff0c;感觉朋友圈已经被这个人工智能刷屏了&#xff0c;大家一边在感叹人工智能行业蓬勃发展的同时&#xff0c;一边又有不少人患上了AI焦虑症。 这其中&#xff0c;以程序员首当其冲。原因无他&#xff0c;只因为GPT-4的惊人的能力和不少大佬的发言。 …

4.17~4.18学习总结

网络编程 概述 1.什么是网络编程 在网络通信协议下&#xff0c;不同计算机上运行的程序&#xff0c;进行的数据传输&#xff0c;计算机跟计算机之间可以通过网络进行数据传输。 2.常见的软件架构&#xff1a; B/S&#xff0c;C/S 3.通信的软件架构CS BS各有什么区别和优点…

阿里云免费使用stable diffusion三个月【ai生成图片】详细教程【保姆级】

起因 这两天关注了ai生成图片&#xff0c;尝试了mijiourney服务【比较贵没入手】&#xff0c;结果免费的没有了&#xff0c;没用上&#xff0c;换了国内的一些小程序体验了下 综合体验式是太慢了&#xff0c;而他们是基于国外开源的stable diffiusion模型开发的【可以比肩mij…

【FAQ】关于华为推送服务因营销消息频次管控导致服务通讯类消息下发失败的解决方案

一&#xff0e; 问题描述 使用华为推送服务下发IM消息时&#xff0c;下发消息请求成功且code码为80000000&#xff0c;但是手机总是收不到消息&#xff1b; 在华为推送自助分析&#xff08;Beta&#xff09;平台查看发现&#xff0c;消息发送触发了频控。 二&#xff0e; 问题…

java 快排算法详解,java 快排代码

快排是一种高效的数据结构&#xff0c;它使用一个关键字&#xff08;Key&#xff09;来表示数据元素的一个集合。也就是说&#xff0c;快排是一个有序数组&#xff0c;而这个有序数组由两个元素组成。 快排的基本思想是&#xff1a;如果数组元素的值比它前面的两个元素都大&…

记录一 :对象锁和类锁

目录 简介 通过8个案例来解释说明 案例及总结 简介 阿里规约【强制】高并发时&#xff0c;同步调用应该去考量锁的性能损耗。能用无锁数据结构&#xff0c;就不要用锁&#xff1b;能 锁区块&#xff0c;就不要锁整个方法体&#xff1b;能用对象锁&#xff0c;就不要用类锁。…

提高工作效率的宝藏网站和宝藏工具

一、好用的网站 面包多 面包多 创作者在面包多&#xff0c;通过出售课程&#xff0c;文章&#xff0c;绘画&#xff0c;创意作品&#xff0c;软件&#xff0c;电子书&#xff0c;音乐&#xff0c; 游戏&#xff0c;咨询服务&#xff0c;每月获得 数百万元 收入。 写作素材模板…

二阶段算法:R-CNN类网络

博主简介 博主是一名大二学生&#xff0c;主攻人工智能研究。感谢让我们在CSDN相遇&#xff0c;博主致力于在这里分享关于人工智能&#xff0c;c&#xff0c;Python&#xff0c;爬虫等方面知识的分享。 如果有需要的小伙伴可以关注博主&#xff0c;博主会继续更新的&#xff0c…

【Java 数据结构】ArrayList的实现和底层源码讲解

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了 博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点!人生格言&#xff1a;当你的才华撑不起你的野心的时候,你就应该静下心来学习! 欢迎志同道合的朋友一起加油喔&#x1f9be;&am…

java day9

第九章 使用swing 9.1 创建应用程序9.1.1 创建页面9.1.2 开发框架9.1.3 创建组件&& 9.1.4 将组件加入到容器中 9.2 使用组件9.2.1 图标9.2.2 标签9.2.3 文本框9.2.4 文本区域9.2.5 可滚动窗格9.2.6 复选框和单选按钮9.2.7 组合框9.2.8 列表 9.1 创建应用程序 import j…

FPGA基于SFP光口实现10G万兆网UDP通信 10G Ethernet Subsystem替代网络PHY芯片 提供工程源码和技术支持

目录 1、前言2、我这里已有的UDP方案3、详细设计方案4、vivado工程详解5、上板调试验证并演示6、福利&#xff1a;工程代码的获取 1、前言 目前网上的fpga实现udp基本生态如下&#xff1a; 1&#xff1a;verilog编写的udp收发器&#xff0c;但不带ping功能&#xff0c;这样的代…