项目面试记录1

主要工作:
用户登录:
使用 Redis 实现分布式 Session,解决集群间登录态同步问题;

解答: session 共享   种 session 的时候注意范围,cookie.domain

比如两个域名:aaa.yupi.com,bbb.yupi.com
如果要共享 cookie,可以种一个更高层的公共域名,比如 yupi.com
当服务部署在不同的端口的时候 可以共享cookie 把cookie的作用域提高到了域名级别

比如下面:

#    session:
#      cookie:
#        domain: localhost

解决方案:共享存储 ,而不是把数据放到单台服务器的内存中**

修改 spring-session 存储配置 spring.session.store-type 默认是 none,表示存储在单台服务器  store-type: redis,表示从 redis 读写 session

  # session 失效时间(分钟)
  session:
    timeout: 86400
#session存储在redis上面  共享session
    store-type: redis

对于项目中复杂的集合处理(比如为队伍列表关联已加入队伍的用户),使用 Java 8 Stream API 和 Lambda 表达式来简化编码。

平时多练习使用lamda optional类  stream

参考这篇文章

**使用 Easy Excel 读取收集来的基础用户信息,并通过自定义线程池 + CompletableFuture 并发编程提高批量导入数据库的性能。
实测导入 100 万行的时间从 160秒缩短至100 秒。

解答: 首先采用一条条地插入数据   这时候时间都花费在建立数据库链接上  然后改用将100万条数据分成每一万条数据做一次批量插入  最后自定义线程池 开启多线程批量插入

在这里插入图片描述

还需要解决的问题:mybatis-plus使用 具体批量查询这块  使用不清楚 以及原理

使用 Redis 缓存首页高频访问的用户信息列表,将接口响应时长从25s秒缩短至250毫秒且通过自定义Redis 序列化器来解决数据乱码、空间浪费的问题。

@Configuration
public class RedisTemplateConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
//        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
//        redisTemplate.setConnectionFactory(connectionFactory);
//        redisTemplate.setKeySerializer(RedisSerializer.string());
//        return redisTemplate;
        // 创建 RedisTemplate 对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();

        // 设置 RedisConnection 工厂。 它就是实现多种 Java Redis 客户端接入的秘密工厂
        template.setConnectionFactory(connectionFactory);

        // 使用 String 序列化方式,序列化 KEY 。
        template.setKeySerializer(RedisSerializer.string());

        // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
        template.setValueSerializer(RedisSerializer.json());

        return template;
    }
}

redis缓存一定要设置过期时间 否则会造成内存溢出 只进不出 同时当内存满的时候redis会自动触发删除机制 如果存有用户的数据不小心删除重要数据 就要毕业啦

进一步改进:同时如何使第一个用户进来的时候查询的速度也变快?

为解决首次访问系统的用户主页加载过慢的问题,使用 Spring Scheduler 定时任务来实现缓存预热,并通过分布式锁保证多机部署时定时任务不会重复执行。

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

首先 数据库内容多 页面加载缓慢 怎么办 第一步 想到分页

第二步 可以做缓存 把要展示的内容提前查出来 用的时候直接取出 走完数据库查询后 就保留在缓存中 (缓存不一致问题怎么解决
突发奇想) 第三步 第一个用户来的时候还是照样很慢如何解决 进行 预缓存 对于重点用户的数据 我们对他推荐用户信息也进行缓存

第四步 如何实现 第三步 用定时任务

第五步 定时任务如何开启比较合理 避免所有的服务器同时打鸣 使用分布式锁 抢到锁的服务器才执行 定时任务

缓存预热
缓存预热就是系统上线后,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
缓存预热解决方案:
(1)直接写个缓存刷新页面,上线时手工操作下;
(2)数据量不大,可以在项目启动的时候自动进行加载;
(3)定时刷新缓存;

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

为解决同一用户重复加入队伍、入队人数超限的问题,使用 Redisson 分布式锁来实现操作互斥,保证了接口幂等性。

接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条...这就没有保证接口的幂等性。

  @Override
    public boolean joinTeam(TeamJoinRequest teamJoinRequest, User loginUser) {
        if (teamJoinRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        Long teamId = teamJoinRequest.getTeamId();
        Team team = getTeamById(teamId);
        Date expireTime = team.getExpireTime();
        if (expireTime != null && expireTime.before(new Date())) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍已过期");
        }
        Integer status = team.getStatus();
        TeamStatusEnum teamStatusEnum = TeamStatusEnum.getEnumByValue(status);
        if (TeamStatusEnum.PRIVATE.equals(teamStatusEnum)) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "禁止加入私有队伍");
        }
        String password = teamJoinRequest.getPassword();
        if (TeamStatusEnum.SECRET.equals(teamStatusEnum)) {
            if (StringUtils.isBlank(password) || !password.equals(team.getPassword())) {
                throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误");
            }
        }
        // 该用户已加入的队伍数量

        long userId = loginUser.getId();
        // 只有一个线程能获取到锁
        RLock lock = redissonClient.getLock("yupao:join_team");
        try {
            // 抢到锁并执行
            while (true) {
                if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
                    System.out.println("getLock: " + Thread.currentThread().getId());
                    QueryWrapper<UserTeam> userTeamQueryWrapper = new QueryWrapper<>();
                    userTeamQueryWrapper.eq("userId", userId);
                    long hasJoinNum = userTeamService.count(userTeamQueryWrapper);
                    if (hasJoinNum > 5) {
                        throw new BusinessException(ErrorCode.PARAMS_ERROR, "最多创建和加入 5 个队伍");
                    }
                    // 不能重复加入已加入的队伍
                    userTeamQueryWrapper = new QueryWrapper<>();
                    userTeamQueryWrapper.eq("userId", userId);
                    userTeamQueryWrapper.eq("teamId", teamId);
                    long hasUserJoinTeam = userTeamService.count(userTeamQueryWrapper);

                    if (hasUserJoinTeam > 0) {
                        throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户已加入该队伍");
                    }
                    // 已加入队伍的人数
                    long teamHasJoinNum = this.countTeamUserByTeamId(teamId);
                    if (teamHasJoinNum >= team.getMaxNum()) {
                        throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍已满");
                    }
                    // 修改队伍信息
                    UserTeam userTeam = new UserTeam();
                    userTeam.setUserId(userId);
                    userTeam.setTeamId(teamId);
                    userTeam.setJoinTime(new Date());
                    return userTeamService.save(userTeam);
                }
            }
        } catch (InterruptedException e) {
            log.error("doCacheRecommendUser error", e);
            return false;
        } finally {
            // 只能释放自己的锁
            if (lock.isHeldByCurrentThread()) {
                System.out.println("unLock: " + Thread.currentThread().getId());
                lock.unlock();
            }
        }
    }

使用编辑距离算法实现了根据标签匹配最相似用户的功能,并通过优先队列 (各种优化方法)来减少 TOP N 运算过程中的内存占用。

    public List<User> matchUsers(long num, User loginUser) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.select("id", "tags");
        queryWrapper.isNotNull("tags");
        List<User> userList = this.list(queryWrapper);
        String tags = loginUser.getTags();
        Gson gson = new Gson();
        List<String> tagList = gson.fromJson(tags, new TypeToken<List<String>>() {
        }.getType());
        // 用户列表的下标 => 相似度
        List<Pair<User, Long>> list = new ArrayList<>();
        // 依次计算所有用户和当前用户的相似度
        for (int i = 0; i < userList.size(); i++) {
            User user = userList.get(i);
            String userTags = user.getTags();
            // 无标签或者为当前用户自己
            if (StringUtils.isBlank(userTags) || user.getId() == loginUser.getId()) {
                continue;
            }
            List<String> userTagList = gson.fromJson(userTags, new TypeToken<List<String>>() {
            }.getType());
            // 计算分数
            long distance = AlgorithmUtils.minDistance(tagList, userTagList);
            list.add(new Pair<>(user, distance));
        }
        // 按编辑距离由小到大排序
        List<Pair<User, Long>> topUserPairList = list.stream()
                .sorted((a, b) -> (int) (a.getValue() - b.getValue()))
                .limit(num)
                .collect(Collectors.toList());
        // 原本顺序的 userId 列表
        List<Long> userIdList = topUserPairList.stream().map(pair -> pair.getKey().getId()).collect(Collectors.toList());
        QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.in("id", userIdList);
        // 1, 3, 2
        // User1、User2、User3
        // 1 => User1, 2 => User2, 3 => User3
        Map<Long, List<User>> userIdUserListMap = this.list(userQueryWrapper)
                .stream()
                .map(user -> getSafetyUser(user))
                .collect(Collectors.groupingBy(User::getId));

        List<User> finalUserList = new ArrayList<>();

        for (Long userId : userIdList) {
            finalUserList.add(userIdUserListMap.get(userId).get(0));
        }
        return finalUserList;
    }

在这里插入图片描述

/**
 * 全局异常处理器
 *
 * @author yupi
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public BaseResponse businessExceptionHandler(BusinessException e) {
        log.error("businessException: " + e.getMessage(), e);
        return ResultUtils.error(e.getCode(), e.getMessage(), e.getDescription());
    }

    @ExceptionHandler(RuntimeException.class)
    public BaseResponse runtimeExceptionHandler(RuntimeException e) {
        log.error("runtimeException", e);
        return ResultUtils.error(ErrorCode.SYSTEM_ERROR, e.getMessage(), "");
    }
}

在这里插入图片描述

在这里插入图片描述

自主编写 Dockerfile,通过容器部署,提高部署上线效率。

使用 Knife4j + Swagger 自动生成后端接口文档,并通过编写 ApiOperation 等注解补充接口注释,避免了人工编写维护文档的麻烦。

前端使用 Vant UI 组件库,并封装了全局通用的 Layout 组件,使主页、搜索页、组队页布局一致、并减少重复代码。

基于 Vue Router 全局路由守卫实现了根据不同页面来动态切换导航栏标题, 并通过在全局路由配置文件扩展 title 字段来
减少无意义的 if else 代码。

经验:
1.方法可能被复用的时候需要把方法写在 service层中 不写在controller层中

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值