面试经验整理

目录

·事务相关

 1、有一个类,a方法调用有事务的b,事务生效吗?

 ·数据库相关

        1、一张表里面有三个字段,abc是组合索引,select * from 表 where a=1 and b>1 and c=1;索引怎么走的,C用到索引没有


·事务相关

 1、有一个类,a方法调用有事务的b,事务生效吗?

        在Spring中,事务是基于代理实现的。如果a方法直接调用b方法,事务可能不会生效,因为事务代理无法拦截内部方法调用。

        如果a方法直接调用b方法,事务不会生效,因为b方法的事务注解不会被代理拦截。

@Service
public class MyService {

    @Transactional
    public void b() {
        // 事务操作
    }

    public void a() {
        b(); // 直接调用,事务不生效
    }
}

如果a方法通过代理调用b方法,事务会生效。可以通过以下方式实现:

  • b方法提取到另一个Service中。

  • 使用AopContext.currentProxy()获取当前代理对象。

  • //1、将b方法提取到另一个Service中
    @Service
    public class ServiceA {
    
        @Autowired
        private ServiceB serviceB;
    
        public void a() {
            serviceB.b(); // 通过代理调用,事务生效
        }
    }
    
    @Service
    public class ServiceB {
    
        @Transactional
        public void b() {
            // 事务操作
        }
    }
    
    //2、使用AopContext.currentProxy()
    @Service
    public class MyService {
    
        @Transactional
        public void b() {
            // 事务操作
        }
    
        public void a() {
            // 获取当前代理对象并调用b方法
            ((MyService) AopContext.currentProxy()).b(); // 事务生效
        }
    }

    事务生效的条件总结

  • 方法必须是public

    • Spring事务代理只能拦截public方法。

  • 方法必须通过代理调用

    • 直接调用内部方法时,事务不会生效。

  • 事务传播行为配置正确

    • 确保b方法的事务传播行为符合预期。

 ·数据库相关

   1、一张表里面有三个字段,abc是组合索引,select * from 表 where a=1 and b>1 and c=1;索引怎么走的,C用到索引没有

        根据最左前缀原则,MySQL会从左到右使用索引,直到遇到范围查询(><BETWEEN等)。

具体分析如下:

索引的使用步骤

  1. 使用a=1

    • 这是一个等值查询,索引可以精确匹配a=1的所有记录。

  2. 使用b>1

    • 这是一个范围查询,索引可以匹配b>1的记录,但范围查询之后的字段无法使用索引

  3. 使用c=1

    • 由于b>1是范围查询,c=1无法使用索引进行精确匹配,只能对a=1 AND b>1的结果进行过滤。

2、Redis遇到国缓存击穿和缓存雪崩的情况吗?怎么解决的? 

  • 缓存雪崩:当大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。

  • 缓存击穿:如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。

  • 缓存穿透:当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。

缓存雪崩解决方案:

  • 均匀设置过期时间:如果要给缓存数据设置过期时间,应该避免将大量的数据设置成同一个过期时间。我们可以在对缓存数据设置过期时间时,给这些数据的过期时间加上一个随机数,这样就保证数据不会在同一时间过期。

  • 互斥锁:当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据,再将数据更新到 Redis 里),当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象。

  • 后台更新缓存:业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新

缓存击穿解决方案:

  • 互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。

  • 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;

缓存穿透解决方案:

  • 非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。

  • 缓存空值或者默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。

  • 布隆过滤器:我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。

·数据结构 

1、zset什么场景用

        ZSET(有序集合,Sorted Set) 是Redis中的一种数据结构,它结合了 集合(Set)有序列表(Sorted List)的特性。ZSET中的每个元素都有一个分数(Score),Redis会根据分数对元素进行排序,并且保证元素的唯一性。

以下是一些典型的ZSET使用场景:

1. 排行榜

ZSET非常适合实现排行榜功能,例如:

  • 游戏中的玩家积分排行榜。

  • 社交平台中的用户活跃度排行榜。

  • 电商平台中的商品销量排行榜。

@Service
public class LeaderboardService {

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    //添加一个key "leaderboard",玩家,分数
    public void addPlayerScore(String player, double score) {
        ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
        zSetOps.add("leaderboard", player, score);
    }
    //获取排行前topN的玩家    
    //也可获取分数,方法zSetOps.reverseRangeWithScores("leaderboard", 0, topN - 1);
    public Set<String> getTopPlayers(int topN) {
        ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
        return zSetOps.reverseRange("leaderboard", 0, topN - 1);
    }
}

2. 优先级队列

ZSET可以用作优先级队列,任务根据优先级(分数)排序,优先级高的任务先被处理。

@Service
public class PriorityQueueService {

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    public void addTask(String task, double priority) {
        ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
        zSetOps.add("tasks", task, priority);
    }
    //实现一个任务优先级队列。
    public String getHighestPriorityTask() {
        ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
        Set<String> tasks = zSetOps.range("tasks", 0, 0);
        return tasks.isEmpty() ? null : tasks.iterator().next();
    }
}

3. 时间轴

ZSET可以用于存储按时间排序的事件,例如:

  • 社交平台中的用户动态时间轴。

public void addPost(String post, long timestamp) {
        ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
        zSetOps.add("timeline", post, timestamp);
    }
    //实现一个用户动态时间轴。
    public Set<String> getLatestPosts(int count) {
        ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
        return zSetOps.reverseRange("timeline", 0, count - 1);
    }

4. 范围查询

ZSET支持根据分数范围查询元素,适合以下场景:

  • 查找某个分数区间内的用户。

    public Set<String> getPlayersInRange(double minScore, double maxScore) {
        ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
        return zSetOps.rangeByScore("leaderboard", minScore, maxScore);
    }

5. 去重并排序

ZSET可以用于对数据进行去重并排序,例如:

  • 统计用户的唯一访问记录并按时间排序。

  • 对数据进行去重并按照某个指标排序。

    public void addVisit(String user, long timestamp) {
        ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
        zSetOps.add("visits", user, timestamp);
    }

    public Set<String> getUniqueVisits() {
        ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
        return zSetOps.range("visits", 0, -1);
    }

6. 延迟任务

ZSET可以用于实现延迟任务队列,任务根据执行时间(分数)排序,定时任务系统定期检查并执行到期的任务。

7. 实时统计

ZSET可以用于实时统计和排序,例如:

  • 统计用户的实时在线时长并排序。

  • 统计商品的实时点击量并排序。

8. 地理空间索引

虽然Redis提供了专门的GEO数据类型,但ZSET也可以用于实现简单的地理空间索引,例如:

  • 查找某个范围内的地点。

  • 按距离排序的地点列表。

9. 数据分片

ZSET可以用于对数据进行分片存储,例如:

  • 将用户数据按分数分片存储到多个ZSET中。

  • 将任务按优先级分片存储到多个ZSET中。

10. 实时推荐系统

ZSET可以用于实时推荐系统,例如:

  • 根据用户的兴趣分数推荐内容。

  • 根据商品的评分推荐商品。

2、hash什么场景用

 Hash 是 Redis 中的一种数据结构,用于存储字段(Field)和值(Value)的映射关系。Hash 适合存储对象或结构化数据,每个 Hash 可以存储多个字段和值。

1. 存储对象

Hash 适合存储对象的属性和值,例如用户信息、商品信息等。

场景

存储用户信息,如用户ID、姓名、年龄、邮箱等。

数据库查询缓存。

2. 计数器

Hash 可以用于存储多个计数器,例如用户的点赞数、评论数、收藏数等。

场景

统计用户的点赞数、评论数、收藏数。

3. 购物车

Hash 适合存储购物车信息,每个用户的购物车是一个 Hash,字段是商品ID,值是商品数量。

场景

存储用户的购物车信息。

4. 配置管理

Hash 可以用于存储系统的配置信息,例如应用的参数设置。

场景

存储应用的配置参数。

 数据一致性

1、如何保证MySQL和redis数据一致性

1、最简单的方式(旁路缓存模式)

  • 读操作

    1. 先读缓存,如果缓存命中,则直接返回数据。

    2. 如果缓存未命中,则从数据库读取数据,并写入缓存。

  • 写操作

    1. 先更新数据库。

    2. 再删除缓存。

优点

  • 实现简单,适合大多数场景。

  • 缓存未命中时,可以自动从数据库加载数据。

缺点

  • 写操作时,如果删除缓存失败,会导致数据不一致。

  • 高并发场景下,可能出现缓存与数据库不一致的情况。

 不做示例,但记住代码中对写操作添加事务,保证数据库和缓存同时成功

2、写穿透模式:

  • 写操作

    1. 先更新缓存。

    2. 再更新数据库。

  • 读操作

    1. 直接读缓存。

优点

  • 写操作时,缓存和数据库同时更新,保证数据一致性。

缺点

  • 实现复杂,需要保证缓存和数据库的更新是原子的。

  • 如果缓存更新失败,会导致数据不一致。

3、写回模式

  • 写操作

    1. 先更新缓存。

    2. 异步更新数据库。

  • 读操作

    1. 直接读缓存。

优点

  • 写操作性能高,适合写多读少的场景。

缺点

  • 数据一致性较弱,可能会出现缓存与数据库不一致的情况。

4、缓存失效策略

通过合理的缓存失效策略,保证缓存与数据库数据的一致性。

方案

  1. 设置缓存过期时间

    • 为缓存设置合理的过期时间,确保缓存数据不会长期与数据库不一致。

  2. 主动失效缓存

    • 在数据更新时,主动删除或更新缓存。

5、使用消息队列

通过消息队列实现 MySQL 和 Redis 的异步数据同步,保证最终一致性

方案

  1. 写操作

    • 先写 MySQL,然后将更新操作发送到消息队列。

  2. 消费消息

    • 消费消息队列中的更新操作,更新 Redis。

 6、延迟双删

  • 先删除缓存:在更新数据库之前,先删除 Redis 中的缓存数据。这样可以确保在数据库更新过程中,其他请求不会从缓存中获取到旧数据。

  • 更新数据库:执行数据库的更新操作,将数据更新为最新值。

  • 延迟删除缓存:在更新数据库之后,延迟一段时间再次删除缓存。这个延迟时间需要根据具体业务场景和数据库操作的耗时来确定,一般来说要确保在这段时间内,可能存在的并发读请求已经完成从数据库加载数据并更新到缓存的操作。通过再次删除缓存,可以保证即使有后续的请求在数据库更新后、缓存更新前访问了缓存,也能在延迟时间过后删除旧的缓存数据,避免数据不一致的问题持续存在。

总结:在实际应用中,根据业务需求选择合适的方案(个人认为多种方案可结合起来使用),并结合重试机制、监控和告警系统,进一步提高数据一致性的可靠性。

高并发 

1、服务如何抗的这么高的QPS,说下架构设计和抗qps,保持性能的手段

2、kafka消息积压的时候怎么处理 

1. 监控与定位问题

在解决消息积压问题之前,首先需要通过监控工具定位问题的根源。

1.1 监控指标
  • 生产者指标

    • 生产者的发送速率(Producer Throughput)。

    • 生产者的请求延迟(Producer Request Latency)。

  • 消费者指标

    • 消费者的拉取速率(Consumer Fetch Rate)。

    • 消费者的处理延迟(Consumer Process Latency)。

    • 消费者的 Lag(消息积压量)。

  • Broker 指标

    • Broker 的磁盘 I/O、CPU、内存使用率。

    • Topic 的分区数和副本状态。

1.2 监控工具
  • Kafka 自带工具

    • kafka-consumer-groups.sh:查看消费者组的 Lag。

    • kafka-topics.sh:查看 Topic 的分区数和消息量。

  • 第三方工具

    • Prometheus + Grafana:实时监控 Kafka 集群的性能指标。

    • Kafka Manager:可视化管理和监控 Kafka 集群。


2. 消费者优化

消费者是消息积压问题的核心,优化消费者的处理能力是解决积压问题的关键。

2.1 提高消费者并发度
  • 增加消费者实例

    • 增加消费者组的消费者数量,确保消费者数量与 Topic 的分区数匹配。

  • 多线程消费

    • 在消费者内部使用多线程处理消息,提高消费速度。

2.2 批量消费
  • 批量拉取消息

    • 配置消费者的 max.poll.records 参数,增加每次拉取的消息数量。

  • 批量处理消息

    • 将多条消息合并处理,减少处理次数。

2.3 异步处理
  • 异步提交 Offset

    • 使用异步提交 Offset(commitAsync),减少提交 Offset 的延迟。

  • 异步处理消息

    • 将消息处理逻辑异步化,避免阻塞消费者线程。

2.4 优化处理逻辑
  • 减少处理时间

    • 优化消费者的处理逻辑,减少单条消息的处理时间。

  • 避免阻塞操作

    • 避免在消费者中执行阻塞操作(如同步调用外部服务)。


3. 生产者优化

如果生产者发送消息的速度过快,也可能导致消息积压。

3.1 限流
  • 控制发送速率

    • 在生产端限制发送速率,避免消息积压。

3.2 批量发送
  • 批量发送消息

    • 配置生产者的 linger.ms 和 batch.size 参数,实现批量发送,减少网络开销。

3.3 压缩消息
  • 启用消息压缩

    • 配置生产者的 compression.type 参数(如 snappy 或 gzip),减少消息传输的大小。


4. Kafka 集群优化

Kafka 集群的性能也会影响消息积压问题。

4.1 增加分区数
  • 增加 Topic 的分区数

    • 分区数越多,消费者的并发度越高,可以提升消费速度。

4.2 增加 Broker
  • 扩展 Kafka 集群

    • 增加 Broker 节点,提升集群的处理能力。

4.3 优化磁盘和网络
  • 使用高性能磁盘

    • 使用 SSD 磁盘,提升 Broker 的 I/O 性能。

  • 优化网络配置

    • 确保 Broker 和消费者之间的网络带宽充足。


5. 应急处理

如果消息积压已经发生,可以采取以下应急措施:

5.1 临时增加消费者
  • 动态扩容

    • 临时增加消费者实例,快速消费积压的消息。

5.2 跳过积压消息
  • 重置 Offset

    • 如果积压的消息可以丢弃,可以使用 kafka-consumer-groups.sh 工具重置消费者的 Offset,跳过积压的消息。

    kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group my-group --reset-offsets --to-latest --execute --topic my-topic
5.3 数据重放
  • 重新消费消息

    • 如果积压的消息需要重新处理,可以将消费者组的 Offset 重置到较早的位置,重新消费消息。


6. 预防措施

为了避免消息积压问题,可以采取以下预防措施:

6.1 容量规划
  • 预估流量

    • 根据业务需求,合理预估 Kafka 集群的容量(如分区数、Broker 数量)。

  • 监控预警

    • 设置 Kafka 集群的监控预警,及时发现潜在问题。

6.2 消费者容错
  • 重试机制

    • 在消费者中实现重试机制,避免因单条消息处理失败导致消费阻塞。

  • 死信队列

    • 将处理失败的消息发送到死信队列,避免阻塞正常消息的处理。

6.3 定期维护
  • 清理过期数据

    • 定期清理 Kafka 中的过期数据,避免磁盘空间不足。

  • 优化配置

    • 根据业务需求,定期优化 Kafka 的配置参数(如分区数、副本因子)。


7. 总结

处理 Kafka 消息积压问题需要从以下几个方面入手:

  1. 监控与定位问题

    • 使用监控工具定位问题的根源。

  2. 消费者优化

    • 提高消费者并发度、批量消费、异步处理、优化处理逻辑。

  3. 生产者优化

    • 限流、批量发送、压缩消息。

  4. Kafka 集群优化

    • 增加分区数、扩展 Broker、优化磁盘和网络。

  5. 应急处理

    • 临时增加消费者、跳过积压消息、数据重放。

  6. 预防措施

    • 容量规划、消费者容错、定期维护。

通过合理的优化和预防措施,可以有效解决 Kafka 消息积压问题,确保系统的高效运行。

算法 

 1、10瓶药中有1瓶变质,每颗正常药1g,变质药每颗1.1g,如何用天平秤一次找到变质的那一瓶药

  • 从第1瓶取1颗,第2瓶取2颗,……,第10瓶取10颗。

  • 把所有取出的药片一起称重。

  • 如果没有变质药,总重量应该是 1 + 2 + 3 + ... + 10 = 55g

  • 由于变质药每颗多0.1g,如果第n瓶变质,总重量会比55g多0.1 × n克。

  • 计算 (实际重量 - 55) / 0.1,结果就是变质的瓶号。

public class PoisonedPillFinder {

    /**
     * 模拟称重过程,返回变质的瓶号
     * @param poisonedBottleNumber 变质的瓶号(1~10)
     * @return 计算出的变质瓶号(应与输入一致)
     */
    public static int findPoisonedBottle(int poisonedBottleNumber) {
        // 1. 模拟从每瓶取药:第n瓶取n颗
        int totalPills = 0;
        double totalWeight = 0.0;

        for (int bottle = 1; bottle <= 10; bottle++) {
            int pillsTaken = bottle; // 第n瓶取n颗
            totalPills += pillsTaken;

            // 如果是变质瓶,每颗药多0.1g
            if (bottle == poisonedBottleNumber) {
                totalWeight += pillsTaken * 1.1;
            } else {
                totalWeight += pillsTaken * 1.0;
            }
        }

        // 2. 计算理论正常重量(1+2+...+10 = 55g)
        double expectedWeight = totalPills * 1.0;

        // 3. 通过重量差反推变质瓶号
        double weightDifference = totalWeight - expectedWeight;
        int calculatedPoisonedBottle = (int) (weightDifference / 0.1);

        return calculatedPoisonedBottle;
    }

    public static void main(String[] args) {
        // 测试:假设第4瓶变质
        int poisonedBottle = 4;
        int foundBottle = findPoisonedBottle(poisonedBottle);

        System.out.println("变质的药瓶是第 " + foundBottle + " 瓶");
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值