记录一下面试遇到的问题以及进行复盘回答和代码demo
  1. 数据库的索引你知道几种
  2. 在使用索引时有什么要注意的
  3. 有做过数据库调优方面的工作吗,具体是如何进行
  4. uuid和自增id的区别和优缺点
  5. 对sql进行explain时,每列的项有什么用,具体需要关注哪些数据
  6. 你们项目里使用到了redis,具体是用来做什么的呢
  7. 我现在需要做一个签到系统,请问要怎么设计
  8. 如果叫你设计一个短链服务,你会怎么做
  9. 我现在有一批量很大的数据需要去重,可以怎么做
  10. 你在你们项目里是怎么防止同一参数发起多次请求的呢

相关代码

redis消息队列以及签到系统demo
https://github.com/zyd100/redis-demo

1.数据库的索引你知道几种

看分类方式,功能上划分或者物理意义上的划分。

功能上主要可以分为 主键索引,唯一索引,普通索引,全文索引。

前面三个顾名思义就不展开说。全文索引是一种特殊索引主要用于对文本类型的高效关键词搜索,很适合模糊匹配和自然语言搜索

物理划分,启示区别方式为索引的存储方式。聚簇索引,非聚簇索引。两者都使用B+树实现,但是聚簇索引的叶子节点存放的是行记录数据,非聚簇索引的叶子节点存放的是数据行的地址。

聚簇索引一张表只能有一个(通常在InnoDB,如果定义了主键,会自动将其作为聚簇索引)。通常我们自己create index创建的都是非聚簇索引,查询到索引后,拿到主键再回表查询。

这时候其实可以结合项目业务进行回答,例如:在项目的xx业务中,我们原来会根据租户id和状态去查询用户的订单id,但是由于原来只给租户id和状态做了组合索引,导致需要回表查询订单id,我们后续的优化就是创建了他们3个的联合索引,这样查询索引本身就覆盖了所需数据无需回表。

还有一种分类就是单列索引和组合索引
单列就是只包含一个列的索引。组合索引由多个列组成,查询的时候遵守最左前缀原则。组合索引也是一种避免回表的优化使用,被称为索引覆盖。

参考阅读1
参考阅读2

2.在使用索引时有什么要注意的

首先就是不能说随便创建索引,索引本质是一种空间换时间的策略,索引过多会导致空间占用过多,以及新增数据时需要更多的时间去更新索引。

在创建索引的选择上,对于保存枚举值的字段,起创建的索引收益很低,更好的选择是给业务id,电话号,姓名等字段创建索引。

对于我们某些业务中,经常需要多个字段一起查询sql语句,可以考虑创建组合索引。对于索引覆盖,我们就需要对实际业务进行权衡,需要考虑到查询的字段是否较少,并且筛选条件比较固定,满足这些条件的话就可以创建覆盖查询字段的组合索引。实际业务查询分页场景下,可以给一些筛选条件+返回数据中字段长度可控的创建覆盖索引,然后大文本字段并不直接查询出来,等用户点击查看详情后,再利用主键进行查询。【通过分次查询来优化业务】

在写sql的时候要注意语句内是否存在让索引失效的场景,例如使用了函数或者计算,类型转换,以及模糊查询等这些都会让索引失效。

MySQL索引使用规则详解:从设计到优化的完整指南

3.有做过数据库调优方面的工作吗,具体是如何进行

这里其实是想问你在实际环境中,从发现问题,定位,再到解决问题的一整个过程。

我们的项目使用了skywalking作为链路追踪和性能监控,在管理页面上我们可以清晰地知道服务实例的运行情况和每个接口被调用的运行状态。在发现了接口出现时间过长的情况后,根据接口的接口名称,找到对应的代码位置,对具体sql使用explain进行sql分析,再根据实际情况如索引失效,或回表,未创建索引来优化。

索引失效 通常是使用了函数、发生了数据类型转换、or条件,不遵循最左匹配。还有一种问题就是跨表join时由于两表字符集规则不同导致索引失效

这时你可以提出一个具体生产发生过的问题。如之前发现过查询订单的接口很慢,explain发现了索引失效但是排查了一会也没发现到问题,最后才发现是在之前给表升级字符集的时候,漏了一张表,导致两表字符集规则不一致从而索引失效,最后统一了字符集规则才解决。

除了索引这种调优手段,其实还可以通过对复杂sql进行拆分,以及例如避免使用select *来进行一些优化。也能说一些热点查询且少更新的数据,引入redis来减少数据库压力,例如在我们项目里的发布单数据,通常在创建完成后并不会过多修改,但是在小程序内会被频繁查询,这里我们就引入了缓存来减轻数据库的压力。

相关文章

4.uuid和自增id的区别和优缺点

uuid是一种无序的随机id,他能够避免消息泄露以及全局唯一(其实存在很小很小的概率会冲突),但是同样会造成性能损耗,而且无序容易导致索引碎片化。
自增id则是有序自增,存储空间小,查询性能优秀,但是也容易被人通过自增id推断出具体业务量,以及数据合并时容易发生冲突。

在具体业务环境中,对于一些系统内部的信息表,配置表可以采用自增id的方式来保存,这样管理简单且性能较好。对于一些核心业务表,或要对外暴露的数据表,则可采用uuid。

这里如果有机会扩展的话可以说到雪花id相关,雪花id在做到全局唯一的同时也能在性能上比uuid好,做到相对有序。

5.对sql进行explain时,每列的项有什么用,具体需要关注哪些数据

主要关注type,key,rows,filtered,extra。
type能够知道查询中的访问方式,通常最好是range和range以上最好,如果是index,则会查询整个索引,all则是全表扫描

key是查询语句的实际使用索引,通过key可以知道查询语句是否有用到索引

rows则是查询语句需要读多少行才找到结果,这个越小越好

filtered则是经过索引后,返回的记录占扫描到的总数的百分比,越大越好

extra则是查询优化器的额外信息
如果发现using filtersort或者using temporary则要注意,查询语句需要额外的排序或者临时表,这可能会造成性能问题

相关阅读

6.你们项目里使用到了redis,具体是用来做什么的呢

这里结合实际场景多说一点,别就只是说拿来存数据那个干

在我们项目里对于一些数据查询频率高,更新不频繁的数据,如档案信息,服务项目,公司配置,对外订单等,我们会放到缓存里减轻数据库压力。通常会在查数据前先查缓存,未命中则查数据库返回并回填到缓存里,同时为了防止缓存雪崩,在固定的过期时间内增加一个随机的时间范围。

在营销模块方面,我们这边会有一个抢优惠券的营销设计,这里使用了redisson的分布式锁来防止超发优惠券(其实也可以用乐观锁来实现)。

在系统层面上,我们有通过redis对部分接口进行限流处理,防止恶意请求,以及在一些登录,一次性授权码,支付token等也会用到redis。

像一些业务完成后我们会给客户发生小程序模板消息,这里有用到redis来实现一个轻量化的消息队列。

7.我现在需要做一个签到系统,请问要怎么设计

如果直接使用数据库来存储用户的签到数据,会造成极大的内存占用,这个时候可以利用redis的位图来存储用户的签到数据,位图占用空间极小,即使用户数量庞大,一年下来的存储量占用也不会很高。

@Service
public class SignService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * redis key格式
     * sign:userid:{userId}:{yyyyMM}
     */
    private static final String REDIS_KEY_SIGN_FORMAT = "sign:userid:%s:%s";

    /**
     * 根据用户id,对应年月,以及签到日进行签到
     * @param userId
     * @param yyyyMM 202602
     * @param day 从0开始 每个月的第一天从0开始计算
     */
    public void sign(String userId,String yyyyMM,int day){

        redisTemplate.opsForValue()
                .setBit(String.format(REDIS_KEY_SIGN_FORMAT,userId,yyyyMM),day,true);
    }


	/**
	* 根据用户id和年月获取对应月份的总体签到情况
	*
	*/
    public List<Integer> getSignDays(String userId, String yyyyMM) {
        // 1. 获取该月总天数
        int year = Integer.parseInt(yyyyMM.substring(0, 4));
        int month = Integer.parseInt(yyyyMM.substring(4));

        LocalDate localDate = java.time.LocalDate.of(year, month, 1);
        int daysInMonth = localDate.lengthOfMonth();

        // 2. 获取该月所有签到记录
        String key = String.format(REDIS_KEY_SIGN_FORMAT, userId, yyyyMM);
        List<Long> result = redisTemplate.opsForValue().bitField(key,
                BitFieldSubCommands.create().get(
                        BitFieldSubCommands.BitFieldType.unsigned(daysInMonth)
                ).valueAt(0));

        // 3. 处理结果
        if (result == null || result.isEmpty()) {
            // 返回全为0的列表
            return Collections.nCopies(daysInMonth, 0);
        }

        long bitMap = result.get(0);
        List<Integer> signStatusList =new ArrayList<>();

        // 4. 逐位解析,构建签到状态列表
        for (int i = 0; i < daysInMonth; i++) {
            if ((bitMap & (1L << i)) != 0) {
                signStatusList.add(1); // 已签到
            } else {
                signStatusList.add(0); // 未签到
            }
        }
        Collections.reverse(signStatusList);
        return signStatusList;
    }


}

8.如果叫你设计一个短链服务,你会怎么做

  在回答这种要你设计某个系统/服务的时候,首先你需要明确核心功能是什么,以及这个系统/服务的性能要求需要到什么地步。

  对于短链服务,其最主要的功能便是短链生成以及短链重定向长链。短链生成这部分我们可以简单归结两部分:短链生成策略以及短链的保存问题。
  短链生成除了需要保证短链足够简短,还需要保证短链的唯一性,这一部分我们可以使用自增id,uuid,雪花或者自定义转码(网上有,通常是转为62进制从而简短长度)来完成。这里可以设计一个基于redis/mysql的发号器,通过发号器生成全局唯一id,然后将唯一id转化为62进制字符串短码。
  在短链保存这个问题上,基于上面生成的字符串短码,和长链进行关联保存。当外部使用短链进行访问时,首先会去redis里查询,没有则查数据库并回填到redis。然后就是根据查到的长链进行重定向。
  考虑到会出现缓存穿透的情况,在redis查询短链前,可以增加使用redis布隆过滤器进行判断,防止被恶意请求击穿。
  短链重定向长链这部分,除了说常规的通过后端程序查询到长链然后重定向外,可以考虑在nginx处增加对缓存的查询,如果nignx层查到后变直接进行重定向,可以减小一层跳转的延迟和后端服务器的资源消耗。

9.我现在有一批量很大的数据需要去重,可以怎么做

  数据量小的情况下可以直接考虑使用java的set或mysql的唯一索引进行去除,数据量开始增大后就得考虑使用redis来辅助完成。如果对误判率有严格要求的话,就使用redis的set来完成,能容忍误判率则可以考虑耗时更低内存占用更小的布隆过滤器。

10.你在你们项目里是怎么防止同一参数发起多次请求的呢

  首先我们会在前端做处理,在用户点击按钮后,按钮状态会变更为loading状态,防止用户进行二次点击。
  在后端程序上,对于需要保证幂等性的接口,我们会让接口调用前,额外调用一个生成较短过期时间的随机id的接口,让接口携带这个id进行访问。接收到请求后如果查到存在此id,则表明是重复请求。当然也可以自定义一个环绕(spring aop),将方法+参数存入缓存并设置一个较短的过期时间,重复请求发起后查询缓存是否存在相同方法参数的调用记录,有则表明是重复请求。
  还有就是对一些如更新支付订单状态的服务,我们会在数据库表增加乐观锁字段,防止同一时间的多次请求导致内容覆盖。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐