字节飞书一面,难度适中,必会!

面试问题

  1. redis的大key和热key问题。
  2. 为什么热k影响redis的背后底层原理(比如能抗住1000w qps的redis为什么会被100w 热k给打挂掉)
  3. 热点菜品问题处理方案。
  4. 限流应该怎么做?
  5. 服务降级应该有什么准则
  6. 服务注册和服务发现怎么做的?
  7. 服务注册和服务发现内部实现细节
  8. 权限校验机制怎么做的?
  9. 权限操作的数据安全和操作安全(给我讲讲原理,我的项目是否做了)
  10. 慢查询

算法题:

给定一个数n如23121;给定一组数字a如[2 4 9]求由a中元素组成的小于n的最大数

原问题链接:字节飞书一面_牛客网

面试参考回答

Redis的大key和热key问题

「面试官」:让我们从Redis开始。你能解释一下什么是大key和热key问题,以及它们可能带来的影响吗?

『求职者』:当然,我很乐意解释这两个概念。

大key问题

  • 定义:大key是指在Redis中存储了大量数据的键。例如,一个键对应的值可能是一个很大的字符串,或者一个包含大量元素的列表、集合或哈希表。
  • 影响
  1. 内存问题:大key会占用大量内存,可能导致内存不足。
  2. 性能问题:读取大key会消耗更多的网络带宽和处理时间。
  3. 阻塞操作:对大key进行删除或者执行一些操作可能会导致Redis阻塞。

热key问题

  • 定义:热key是指在短时间内被高频访问的键
  • 影响
  1. 性能瓶颈:大量请求集中在少数几个key上,可能导致Redis实例的CPU使用率飙升。
  2. 缓存击穿:如果热key突然失效,可能会导致大量请求直接打到后端数据库。
  3. 负载不均衡:在分布式系统中,热key可能导致某些Redis节点负载过高。

解决方案

  1. 对于大key:
  • 拆分大key,将数据分散到多个小的key中。
  • 使用哈希结构来替代大字符串或大列表。
  • 定期清理或设置过期时间。
  1. 对于热key:
  • 使用本地缓存来减轻Redis的压力。
  • 对热key进行分片,将访问负载分散到多个Redis实例。
  • 使用读写分离,增加从节点来分担读压力。

在实际应用中,我们通常会使用监控工具来识别大key和热key,然后根据具体情况采取相应的优化措施。

热key对Redis性能的影响

「面试官」:很好的解释。那么你能深入解释一下为什么热key会影响Redis的性能吗?比如说,一个能够处理1000万QPS的Redis实例,为什么可能会被100万的热key请求打垮?

『求职者』:这是一个非常好的问题,涉及到Redis的内部工作原理。让我从几个方面来解释为什么热key会对Redis性能造成如此大的影响:

  1. Redis的单线程模型
  • Redis主要使用单线程来处理客户端请求。虽然Redis 6.0引入了多线程I/O,但核心的命令处理仍然是单线程的。
  • 这意味着所有请求都需要排队等待处理,无法并行执行。
  1. CPU密集型操作
  • 热key导致大量相同的请求集中到单个键上,这可能会使Redis陷入CPU密集型操作。
  • 即使每个操作很快,但大量重复操作也会占用大量CPU时间。
  1. 内存访问模式
  • 频繁访问同一个key可能会导致缓存行颠簸(cache line thrashing)。
  • 多个CPU核心可能会争夺包含该key的缓存行,导致频繁的缓存同步操作。
  1. 网络带宽限制
  • 如果热key对应的是大数据量,频繁的访问可能会占用大量网络带宽
  • 这可能导致其他请求的响应变慢。
  1. 连接数限制
  • 大量并发请求可能会耗尽可用的连接数
  • 新的连接请求可能会被拒绝或等待,导致整体性能下降。
  1. 内存碎片
  • 频繁的写操作可能会导致内存碎片,影响内存使用效率。
  1. 持久化影响
  • 如果启用了AOF持久化,频繁的写操作会导致AOF文件快速增长。
  • 这可能会增加磁盘I/O压力,影响整体性能。
  1. 数据结构复杂度
  • 如果热key是一个复杂的数据结构(如大型哈希表或有序集合),每次操作的时间复杂度可能会更高。
  1. 负载不均衡
  • 在分布式Redis架构中,热key可能会导致某个分片节点负载过高,而其他节点相对空闲。

为什么100万QPS的热key请求可能会影响1000万QPS的Redis实例:

  • 虽然Redis可以处理1000万QPS,但这通常是在请求分散到不同key的理想情况下。
  • 当100万QPS集中在一个key上时,它会造成上述的多种问题,特别是在单线程模型下,这100万请求会串行处理,可能会阻塞其他请求。
  • 这种情况下,Redis的性能不再由总QPS决定,而是由单个key的处理速度决定。

解决方案:

  1. key拆分:将热key拆分成多个子key,分散访问压力。
  2. 本地缓存:在应用层使用本地缓存,减少对Redis的直接访问。
  3. 读写分离:对于读多写少的场景,可以使用Redis的主从复制,分散读压力。
  4. 使用Redis集群:将热key分散到不同的节点上。
  5. 异步处理:对于一些非实时的操作,可以使用消息队列异步处理,减轻Redis压力。

理解这些原理有助于我们在设计系统时更好地预防和处理热key问题。

热点菜品问题处理方案

「面试官」:非常好的解释。现在假设我们在开发一个外卖系统,遇到了热点菜品的问题,你会如何处理?

『求职者』:处理外卖系统中的热点菜品问题是一个典型的高并发场景。我们需要从多个层面来解决这个问题:

  1. 多级缓存策略
  • 应用层本地缓存:使用如Caffeine这样的本地缓存库,缓存热点菜品信息。
  • 分布式缓存:使用Redis缓存热点菜品数据。
  • CDN:对于菜品图片等静态资源,使用CDN加速。
  1. 数据分片
  • 将热点菜品信息分散到多个Redis实例上。
  • 使用一致性哈希算法来确保负载均衡。
  1. 读写分离
  • 对Redis实现主从复制,将读请求分发到多个从节点。
  • 使用Redis Cluster实现自动分片和高可用。
  1. 异步更新
  • 对于菜品评分、销量等非实时数据,使用消息队列异步更新。
  • 可以考虑使用Kafka等消息中间件。
  1. 预热缓存
  • 根据历史数据,预先将可能成为热点的菜品信息加载到缓存中。
  • 可以在系统负载较低时定期更新这些预热数据。
  1. 限流措施
  • 实现应用层限流,控制单个用户对特定菜品的访问频率。
  • 可以使用令牌桶或漏桶算法实现。
  1. 数据压缩
  • 对缓存中的数据进行压缩,减少网络传输量和内存占用。
  1. 智能降级
  • 当系统负载过高时,返回简化版的菜品信息。
  • 实现熔断机制,防止系统完全崩溃。
  1. 缓存更新策略
  • 采用缓存双写或者延迟双删策略,确保缓存与数据库的一致性。
  • 使用分布式锁来处理并发更新问题。
  1. 实时监控和动态调整
  • 实现实时监控系统,及时发现热点菜品。
  • 根据监控数据动态调整缓存策略和资源分配。
  1. 前端优化
  • 实现前端页面的懒加载和分页加载。
  • 使用客户端缓存策略,减少不必要的请求。
  1. 数据库优化
  • 优化数据库查询,为热点菜品相关的查询添加适当的索引。
  • 考虑使用读写分离或分库分表来提高数据库性能。

实现示例(Java代码片段):

@Service
public class HotDishService {

    @Autowired
    private RedisTemplate<String, Dish> redisTemplate;
    
    @Autowired
    private DishRepository dishRepository;
    
    @Cacheable(value = "dishes", key = "#dishId", unless = "#result == null")
    public Dish getDish(Long dishId) {
        // 先从Redis查询
        Dish dish = redisTemplate.opsForValue().get("dish:" + dishId);
        if (dish != null) {
            return dish;
        }
        
        // Redis中不存在,查询数据库
        dish = dishRepository.findById(dishId).orElse(null);
        if (dish != null) {
            // 将结果存入Redis,设置过期时间
            redisTemplate.opsForValue().set("dish:" + dishId, dish, 1, TimeUnit.HOURS);
        }
        return dish;
    }
    
    // 其他方法...
}

这个方案综合考虑了缓存、分布式系统、异步处理和监控等多个方面,能够有效地处理热点菜品问题,提高系统的整体性能和可用性。

限流实现方案

「面试官」:很好的解决方案。你提到了限流,能详细说说限流应该怎么做吗?

『求职者』:当然,限流是一种重要的系统保护机制,用于控制系统的访问速率,防止系统被过多的请求压垮。我们可以从多个层面和多种方法来实现限流:

  1. 常用限流算法:a. 令牌桶算法

b. 漏桶算法

c. 计数器算法

d. 滑动窗口算法

  • 以恒定速率向桶中添加令牌。
  • 每个请求需要获取一个令牌才能被处理。
  • 如果桶空了,请求会被拒绝或等待。
  • 适合处理突发流量。
  • 请求以任意速率流入桶中。
  • 请求以固定速率从桶中流出被处理。
  • 如果桶满了,新请求会被丢弃。
  • 适合限制平均流出速率。
  • 在固定时间窗口内计数请求数。
  • 如果超过阈值,拒绝新请求。
  • 简单但可能导致边界突发。
  • 计数器的改进版,使用滑动时间窗口。
  • 更平滑,避免了固定窗口的边界问题。
  1. 限流的实现层面:a. 应用层限流

b. 网关层限流

c. 中间件限流

d. 基础设施层限流

  • 在代码中实现限流逻辑。
  • 可以更精细地控制具体的业务逻辑。
  • 在API网关(如Spring Cloud Gateway)中实现。
  • 可以集中管理多个服务的限流策略。
  • 使用如Redis、Sentinel等中间件来实现分布式限流。
  • 在负载均衡器或反向代理(如Nginx)层面实现。
  1. 限流的粒度
  • 接口级别:限制单个API的访问频率。
  • 用户级别:针对不同用户设置不同的限流规则。
  • 服务级别:限制整个服务的总体访问量。
  • IP级别:防止单一IP的大量请求。
  1. 实现示例:a. 使用Guava RateLimiter实现令牌桶算法:
import com.google.common.util.concurrent.RateLimiter;

@Service
public class OrderService {
    private final RateLimiter rateLimiter = RateLimiter.create(100.0); // 每秒100个请求

    public boolean createOrder(Order order) {
        if (rateLimiter.tryAcquire()) {
            // 处理订单
            return true;
        } else {
            // 限流,拒绝请求
            return false;
                       }
       }
   }

b. 使用Redis实现分布式限流:

@Service
public class RateLimiterService {
    @Autowired
    private StringRedisTemplate redisTemplate;

    public boolean isAllowed(String key, int limit, int period) {
        String script = "local cnt = redis.call('incr',KEYS[1]) " +
                        "if cnt == 1 then " +
                        "   redis.call('expire',KEYS[1],ARGV[1]) " +
                        "end " +
                        "if cnt > tonumber(ARGV[2]) then " +
                        "   return 0 " +
                        "end " +
                        "return 1";

        List<String> keys = Collections.singletonList(key);
        List<String> args = Arrays.asList(String.valueOf(period), String.valueOf(limit));

        Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), keys, args.toArray());
        return result != null && result == 1L;
    }
}

c. 使用Spring Cloud Gateway实现网关层限流:

spring:
  cloud:
    gateway:
      routes:
      - id: limit_route
        uri: http://example.org
        predicates:
        - Path=/search
        filters:
        - name: RequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 10
            redis-rate-limiter.burstCapacity: 20
  1. 限流的最佳实践
  • 动态配置:支持动态调整限流规则,适应不同场景。
  • 优雅降级:当触发限流时,返回友好的提示信息。
  • 监控和告警:实时监控限流情况,及时发现异常。
  • 分级限流:对不同重要程度的接口设置不同的限流策略。
  • 弹性限流:根据系统负载动态调整限流阈值。
  • 限流预热:系统刚启动时,逐渐增加限流阈值,而不是立即全量开放。
  1. 与其他技术的结合
  • 熔断:结合熔断机制,在下游服务不可用时快速失败。
  • 降级:当触发限流时,可以返回降级的服务响应。
  • 负载均衡:结合负载均衡,将请求分散到多个服务实例。

实现限流时,需要根据具体的业务场景和系统架构来选择合适的方案。通常,我们会在多个层面同时实施限流策略,以确保系统的稳定性和可用性。

服务降级准则

「面试官」:非常全面的回答。那么在实施限流的同时,我们可能还需要考虑服务降级。你认为服务降级应该遵循什么样的准则?

『求职者』:服务降级是一种在系统压力过大或部分服务不可用时,保证核心功能可用的策略。制定服务降级的准则时,我们需要考虑以下几个方面:

  1. 业务优先级
  • 明确区分核心业务和非核心业务。
  • 优先保证核心业务的可用性,非核心业务可以降级或暂时关闭。
  1. 用户体验
  • 降级后的服务应尽可能保持基本的用户体验。
  • 提供清晰的提示信息,告知用户当前状态和预期恢复时间。
  1. 性能影响
  • 降级方案应该能显著减轻系统负载。
  • 评估降级后对整体系统性能的影响。
  1. 可恢复性
  • 设计易于恢复的降级方案,确保系统能够快速恢复到正常状态。
  • 降级和恢复过程应该是平滑的,避免造成新的系统冲击。
  1. 监控和自动化
  • 实现自动化的降级和恢复机制。
  • 全面监控系统状态,及时发现需要降级的情况。
  1. 降级粒度
  • 支持细粒度的降级,可以针对特定用户、特定功能或特定时间段进行降级。
  1. 数据一致性
  • 确保降级不会导致数据不一致或丢失。
  • 对于重要的写操作,考虑使用消息队列等方式异步处理。
  1. 合规性和安全性
  • 确保降级方案符合法律法规和安全要求。
  • 敏感操作(如支付)的降级需要特别谨慎。
  1. 通知机制
  • 建立有效的通知机制,及时通知相关人员和用户。
  1. 降级的优雅性
  • 降级应该是渐进的,而不是突然的全面降级。
  • 考虑使用流量控制等方式,逐步引入降级。
  1. 测试和演练
  • 定期进行降级演练,确保降级机制有效。
  • 在非生产环境中充分测试降级方案。
  1. 文档和流程
  • 制定详细的降级文档和操作流程。
  • 确保相关人员了解降级策略和操作方法。

实现示例(Java代码片段):

public class OrderService {
    private boolean isDegrade = false;

    @HystrixCommand(fallbackMethod = "createOrderFallback")
    public Order createOrder(OrderRequest request) {
        if (isDegrade) {
            return createOrderFallback(request);
        }
        // 正常创建订单的逻辑
        return new Order(/* ... */);
    }

    public Order createOrderFallback(OrderRequest request) {
        // 降级逻辑:只接受不需要库存检查的简单订单
        if (request.isSimpleOrder()) {
            return new Order(/* 简化的订单创建逻辑 */);
        } else {
            throw new ServiceDegradedException("服务已降级,暂不支持复杂订单");
        }
    }

    @Scheduled(fixedRate = 60000)
    public void checkSystemStatus() {
        // 检查系统负载、依赖服务状态等
        // 根据检查结果动态调整 isDegrade 标志
    }
}

在这个例子中:

  • 我们使用 Hystrix 来实现服务降级。
  • 通过 isDegrade 标志来控制是否启用降级。
  • 降级后,只处理简单订单,拒绝复杂订单。
  • 使用定时任务定期检查系统状态,动态决定是否需要降级。

遵循这些准则,我们可以实现一个既能保护系统,又能维持核心业务运行的服务降级策略。在实际应用中,具体的降级策略还需要根据业务特点和系统架构来细化和调整。

服务注册与发现机制

「面试官」:很好的解释。现在让我们转向微服务架构。你能详细讲解一下服务注册和服务发现是如何实现的吗?包括它们的内部实现细节。

『求职者』:当然,服务注册和服务发现是微服务架构中的核心组件,它们解决了在分布式系统中如何定位和访问服务的问题。让我详细解释它们的工作原理和实现细节:

  1. 服务注册

服务注册是指微服务实例在启动时,将自己的网络地址和其他元数据信息注册到一个中央目录(注册中心)的过程。

实现步骤: a. 服务启动时,向注册中心发送注册请求。 b. 注册中心接收请求,记录服务实例信息。 c. 服务定期发送心跳,维持注册状态。

  1. 服务发现

服务发现是指客户端通过注册中心获取可用服务实例信息的过程。

实现步骤: a. 客户端向注册中心查询服务。 b. 注册中心返回可用的服务实例列表。 c. 客户端使用负载均衡算法选择一个实例进行调用。

  1. 常见的注册中心实现
  • Eureka(Netflix)
  • Consul(HashiCorp)
  • Zookeeper(Apache)
  • Etcd(CoreOS)
  • Nacos(阿里巴巴)
  1. 内部实现细节

以 Eureka 为例,详细解释其内部实现:

a. 服务注册

  • 服务实例启动时,通过 Eureka Client 向 Eureka Server 发送 REST 请求。
  • 请求包含服务ID、IP地址、端口等信息。
  • Eureka Server 接收请求,将信息存储在内存中的注册表里。
  • 服务实例定期(默认30秒)发送心跳续约。

b. 服务发现

  • 客户端从 Eureka Server 获取注册表的完整信息。
  • 客户端会在本地缓存这些信息。
  • 客户端定期(默认30秒)从服务器获取增量更新。

c. 自我保护机制

  • 如果在15分钟内超过85%的实例没有正常心跳,Eureka会进入自我保护模式。
  • 在此模式下,Eureka会保留所有实例信息,不再删除注册信息。

d. 集群同步

  • Eureka servers 之间通过点对点通信来同步注册信息。

e. 负载均衡

  • 客户端侧负载均衡,通常使用 Ribbon 实现。
  1. 代码示例

服务注册(Spring Boot with Eureka):

@SpringBootApplication
@EnableEurekaClient
public class MyServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyServiceApplication.class, args);
    }
}

application.yml:

spring:
  application:
    name: my-service

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

服务发现:

@Configuration
public class RestTemplateConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

@Service
public class MyService {
    @Autowired
    private RestTemplate restTemplate;

    public String callService() {
        return restTemplate.getForObject("http://another-service/api/resource", String.class);
    }
}
  1. 注册中心的选择考虑因素
  • 一致性模型(CP vs AP)
  • 性能和可扩展性
  • 安全性(如加密、认证)
  • 可观察性(监控、日志)
  • 多数据中心支持
  • 社区活跃度和支持
  1. 最佳实践
  • 实现服务的健康检查机制
  • 合理设置超时和重试策略
  • 使用服务分组和版本管理
  • 实现优雅的服务上下线
  • 监控和警报机制
  • 考虑使用服务网格(如 Istio)进行更细粒度的控制

服务注册与发现机制是构建可靠、可扩展微服务架构的基础。理解其工作原理和实现细节对于设计和维护大规模分布式系统至关重要。

权限校验机制

「面试官」:非常详细的解答。现在让我们谈谈安全性。你能解释一下你们的系统是如何实现权限校验的吗?特别是在数据安全和操作安全方面。

『求职者』:当然,权限校验是系统安全的核心组成部分。在我们的系统中,我们采用了多层次的权限校验机制,以确保数据安全和操作安全。让我详细解释一下我们的实现:

  1. 认证(Authentication)
  • 使用 JWT(JSON Web Token) 进行用户认证。
  • 实现 OAuth2.0 协议,支持多种认证方式(密码、手机验证码、第三方登录等)。
  • 使用 Spring Security 框架来管理认证流程。
  1. 授权(Authorization)
  • 基于 RBAC(基于角色的访问控制) 模型实现细粒度的权限控制。
  • 使用 Spring Security 的 @PreAuthorize 注解进行方法级别的权限控制。
  • 实现动态权限管理,支持运行时更新权限配置。
  1. 数据安全
  • 实现行级数据权限控制,确保用户只能访问其有权限的数据。
  • 使用 AES 加密算法对敏感数据进行加密存储。
  • 实现数据脱敏,在返回敏感信息时自动脱敏。
  1. 操作安全
  • 实现操作日志记录,记录所有关键操作。
  • 使用 Spring AOP 实现操作审计。
  • 实现敏感操作的多因素认证。
  1. API 安全
  • 使用 HTTPS 加密所有 API 通信。
  • 实现 API 访问频率限制,防止 DDoS 攻击。
  • 使用 API 签名机制,确保请求的完整性和来源可信。
  1. 代码实现示例:a. JWT认证实现:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/auth/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .apply(new JwtConfigurer(jwtTokenProvider));
    }
}

b. 方法级权限控制:

@Service
public class UserService {
    @PreAuthorize("hasRole('ADMIN')")
    public List<User> getAllUsers() {
        // 只有ADMIN角色可以访问
    }

    @PreAuthorize("hasPermission(#userId, 'User', 'READ')")
    public User getUser(Long userId) {
        // 检查用户是否有权限读取特定用户信息
    }
}

c. 数据级权限控制:

@Service
public class DataService {
    @Autowired
    private SecurityUtils securityUtils;

    public List<Data> getData() {
        User currentUser = securityUtils.getCurrentUser();
        return dataRepository.findByUserIdOrPublicData(currentUser.getId(), true);
    }
}

d. 操作审计:

@Aspect
@Component
public class AuditAspect {
    @Autowired
    private AuditLogService auditLogService;

    @AfterReturning("@annotation(Audited)")
    public void auditMethod(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String methodName = signature.getName();
        Object[] args = joinPoint.getArgs();
        
        auditLogService.log(methodName, args);
    }
}
  1. 数据安全实现:a. 数据加密:
@Component
public class EncryptionUtil {
    @Value("${encryption.key}")
    private String encryptionKey;

    public String encrypt(String data) {
        // 使用AES加密
    }

    public String decrypt(String encryptedData) {
        // 使用AES解密
    }
}

b. 数据脱敏:

@JsonComponent
public class SensitiveInfoSerializer extends JsonSerializer<String> {
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (value == null) {
            gen.writeNull();
        } else {
            gen.writeString(maskSensitiveInfo(value));
        }
    }

    private String maskSensitiveInfo(String info) {
        // 实现脱敏逻辑
    }
}
  1. 最佳实践:a. 最小权限原则:只赋予用户完成其工作所需的最小权限集。b. 职责分离:确保没有单个角色可以完成所有敏感操作。c. 定期审核:定期审查用户权限,移除不再需要的权限。d. 密码策略:实施强密码策略,定期要求用户更改密码。e. 多因素认证:对敏感操作实施多因素认证。f. 实时监控:实现实时的安全事件监控和告警机制。
  2. 安全性测试:a. 定期进行渗透测试。b. 使用自动化工具进行安全扫描。c. 进行代码安全审计。d. 模拟各种攻击场景,如SQL注入、XSS等。
  3. 持续改进:a. 跟踪最新的安全威胁和最佳实践。b. 定期更新和升级安全组件。c. 培训开发团队,提高安全意识。d. 建立安全事件响应机制。

在我们的项目中,我们特别注重以下几点:

  1. 我们实现了基于JWT的无状态认证,提高了系统的可扩展性。
  2. 使用RBAC模型,我们能够灵活地管理复杂的权限结构。
  3. 通过实现细粒度的数据权限控制,我们确保了即使在同一角色内,用户也只能访问其有权限的数据。
  4. 我们的审计日志系统不仅记录了谁在什么时候做了什么操作,还记录了操作的上下文信息,便于后续的安全分析。
  5. 对于敏感数据,我们不仅在数据库层面进行了加密,在传输过程中也使用了HTTPS加密,确保了端到端的数据安全。

这种多层次、全方位的安全策略帮助我们建立了一个强大的安全防线,有效地保护了系统和用户数据的安全。

「面试官」:非常全面的回答。你提到了使用JWT进行认证,能详细说说JWT的工作原理,以及它相比于传统session认证的优缺点吗?

『求职者』:当然,我很乐意详细解释JWT的工作原理及其与传统session认证的比较。

JWT(JSON Web Token)工作原理

  1. 结构: JWT由三部分组成,用点(.)分隔:

格式:xxxxx.yyyyy.zzzzz

  • Header(头部)
  • Payload(负载)
  • Signature(签名)
  1. 认证流程: a. 用户登录,服务器验证凭证。 b. 服务器创建JWT,包含用户信息和过期时间。 c. 服务器将JWT返回给客户端。 d. 客户端存储JWT(通常在localStorage或cookie中)。 e. 后续请求中,客户端在Authorization header中携带JWT。 f. 服务器验证JWT的签名和有效期。 g. 如果有效,允许访问受保护的资源。
  2. JWT生成过程
base64UrlEncode(header) + "." +
base64UrlEncode(payload) + "." +
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
  1. 安全性
  • 使用密钥签名,防止篡改。
  • 可以包含过期时间,限制token的有效期。

JWT vs 传统Session认证

优点:

  1. 无状态:服务器不需要存储session信息,更容易扩展。
  2. 跨域友好:适用于分布式系统和微服务架构。
  3. 移动端适用:不依赖于cookie,适合移动应用。
  4. 性能:减少了数据库查询,提高了性能。
  5. 解耦:认证信息存储在客户端,服务端无需保存状态。

缺点:

  1. 安全性考虑
  • JWT一旦签发,在过期之前都是有效的,无法主动撤销。
  • 需要谨慎处理敏感信息,避免将其放在JWT中。
  1. 大小:JWT可能比简单的session id更大,增加了网络负载。
  2. 灵活性较低:难以实现类似"强制登出"的功能。
  3. 密钥管理:需要安全地管理签名密钥。

实现示例

  1. JWT生成(使用jjwt库):
public String createToken(String username) {
    Date now = new Date();
    Date validity = new Date(now.getTime() + 3600000); // 1 hour

    return Jwts.builder()
        .setSubject(username)
        .setIssuedAt(now)
        .setExpiration(validity)
        .signWith(SignatureAlgorithm.HS256, secretKey)
        .compact();
}
  1. JWT验证:
public boolean validateToken(String token) {
    try {
        Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
        return !claims.getBody().getExpiration().before(new Date());
    } catch (JwtException | IllegalArgumentException e) {
        return false;
    }
}
  1. 在Spring Security中使用JWT:
@Component
public class JwtTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String token = resolveToken(request);
        if (token != null && jwtTokenProvider.validateToken(token)) {
            Authentication auth = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        filterChain.doFilter(request, response);
    }

    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

在我们的项目中,我们选择JWT主要是因为它适合我们的微服务架构,并且能够很好地支持我们的移动应用。我们通过合理设置过期时间、实现刷新token机制,以及在必要时实现token黑名单来弥补JWT的一些局限性。

总的来说,JWT为我们提供了一种灵活、安全且高效的认证方式,很好地满足了我们系统的需求。

「面试官」:非常好的解释。现在让我们来看一道算法题。给定一个数n如23121,给定一组数字a如[2, 4, 9],求由a中元素组成的小于n的最大数。你能给出解决方案吗?

『求职者』:这是一个很有趣的问题。我们可以使用贪心算法来解决这个问题。基本思路是从高位到低位,尽可能选择不超过对应位的最大数字。让我用Java来实现这个算法:

import java.util.*;

public class Solution {
    public int findMaxNumber(int n, int[] a) {
        // 将n转换为字符数组
        char[] nChars = String.valueOf(n).toCharArray();
        int len = nChars.length;
        
        // 对a进行排序
        Arrays.sort(a);
        
        // 结果数组
        char[] result = new char[len];
        
        // 标记是否已经选择了小于n对应位的数
        boolean smaller = false;
        
        for (int i = 0; i < len; i++) {
            if (smaller) {
                // 如果已经选择了小于n对应位的数,后面都选最大的
                result[i] = (char) (a[a.length - 1] + '0');
            } else {
                // 找到不大于当前位的最大数
                int j = a.length - 1;
                while (j >= 0 && a[j] > (nChars[i] - '0')) {
                    j--;
                }
                
                if (j >= 0) {
                    result[i] = (char) (a[j] + '0');
                    if (a[j] < (nChars[i] - '0')) {
                        smaller = true;
                    }
                } else {
                    // 如果所有数都大于当前位,选择最小的数,并标记为smaller
                    result[i] = (char) (a[0] + '0');
                    smaller = true;
                }
            }
        }
        
        // 将结果转换为整数
        return Integer.parseInt(new String(result));
    }

    public static void main(String[] args) {
        Solution solution = new Solution();
        int n = 23121;
        int[] a = {2, 4, 9};
        System.out.println(solution.findMaxNumber(n, a));  // 输出:22999
    }
}

这个算法的工作原理如下:

  1. 将给定的数n转换为字符数组,便于逐位比较。
  2. 对数组a进行排序,以便我们可以方便地找到不大于某个数的最大值。
  3. 从高位到低位遍历n的每一位:
  • 如果我们已经选择了一个小于n对应位的数,那么后面的位都选择a中的最大数。
  • 否则,我们找到a中不大于当前位的最大数。
  • 如果找不到这样的数,我们选择a中的最小数,并标记后续位都可以选最大的数。
  1. 最后,将结果转换回整数。

这个算法的时间复杂度是O(d + m log m),其中d是n的位数,m是数组a的长度。空间复杂度是O(d)。

这个解决方案利用了贪心的思想,保证了每一位都是在满足条件的情况下的最大可能值,从而得到了小于n的最大数。

「面试官」:很好的解答。你能解释一下为什么这个贪心算法是正确的吗?有没有可能存在一种情况,选择一个较小的数字反而能得到更大的结果?

『求职者』:这是一个很好的问题,它触及了贪心算法正确性的核心。让我详细解释为什么这个贪心算法是正确的:

  1. 最高位的重要性: 在一个数字系统中,高位的数字总是比低位的任何数字更有影响力。例如,在十进制系统中,一个数的千位比它的个位、十位和百位加起来还要大。
  2. 逐位最优选择: 我们的算法从最高位开始,每一位都选择不超过n对应位的最大可能数字。这保证了结果数在每一位上都尽可能大,同时不会超过n。
  3. 不存在更优解: 假设存在一种情况,选择一个较小的数字能得到更大的结果。那么这意味着: a. 在某一位上,我们选择了一个小于当前可选最大数字的数。 b. 在后续的某些位上,我们选择了更大的数字,使得整体结果更大。但是,这种情况是不可能的,因为:
  • 如果我们在某一位选择了小于可选最大数字的数,那么这一位及之前的所有位组成的数字就已经小于n的对应部分了。
  • 在这种情况下,我们的算法会在后续所有位上选择a中的最大数字。
  • 没有任何其他选择能产生比这更大的结果。
  1. 转折点的重要性: 当我们选择了一个小于n对应位的数字时,这个位置成为了一个转折点。在这之后,我们可以安全地选择所有可用的最大数字,而不用担心超过n。
  2. 举例说明: 考虑n = 23121,a = [2, 4, 9]

结果是22999,这确实是小于23121的最大数。

  • 第一位:选2(最大的不超过2的数)
  • 第二位:选2(最大的不超过3的数)
  • 第三位:选9(已经小于n了,所以选最大的)
  • 第四位和第五位:继续选9
  1. 反证法: 假设存在一个更好的解决方案。这意味着在某个位置,它选择了一个比我们算法选择的数字小的数。但是,如果在那个位置选择一个更大的数字(就像我们的算法做的那样)仍然不会超过n,那么选择更小的数字显然不会得到更大的结果。
  2. 算法的单调性: 我们的算法保证了结果是单调的。一旦我们选择了一个小于n对应位的数字,后面的选择就变得简单了 —— 总是选最大的。这种单调性保证了我们不会错过更优的解。

总结来说,这个贪心算法之所以正确,是因为它利用了数字系统的基本性质(高位权重更大),并在每一步都做出了当前情况下的最优选择。由于高位的选择决定性更强,一旦我们在某位上选择了小于n对应位的数字,后面的选择就变得简单明了。这种策略保证了我们能找到小于n的最大数,不存在通过选择较小数字而得到更大结果的可能性。

「面试官」:非常好的解释。你对算法的理解很深入。最后一个问题,你能谈谈你是如何处理数据库中的慢查询问题的吗?

『求职者』:当然,处理数据库慢查询是优化数据库性能的重要部分。我们通常采用以下步骤来解决慢查询问题:

  1. 识别慢查询
  • 开启MySQL的慢查询日志,设置合适的 long_query_time
  • 使用工具如 pt-query-digest 分析慢查询日志。
  • 在应用层面使用APM工具监控数据库操作的耗时。
  1. 分析查询计划
  • 使用 EXPLAIN 命令分析查询的执行计划。
  • 关注 typekeyrows 等字段,判断索引使用情况和扫描的行数。
  1. 优化索引
  • 为 WHERE 子句、JOIN 条件、ORDER BY 和 GROUP BY 中的列添加适当的索引。
  • 使用复合索引优化多列查询。
  • 注意索引的选择性,避免过多的索引。
  1. 优化查询语句
  • 避免使用 SELECT *,只选择需要的列。
  • 优化 JOIN 操作,确保 ON 子句中的列有索引。
  • 使用 LIMIT 限制结果集大小。
  • 适当使用子查询、临时表等优化复杂查询。
  1. 分区表
  • 对于大表,考虑使用分区表来提高查询效率。
  1. 数据库设计优化
  • 适当的表结构设计,如避免过多的列。
  • 合理使用范式化和反范式化。
  1. 配置优化
  • 调整 MySQL 的配置参数,如 innodb_buffer_pool_sizequery_cache_size 等。
  1. 硬件升级
  • 如果软件优化无法满足需求,考虑升级硬件,如使用SSD、增加内存等。
  1. 读写分离
  • 对于读多写少的场景,实现主从复制,将读操作分流到从库。
  1. 分库分表
  • 对于超大规模的数据,考虑水平或垂直拆分。
  1. 使用缓存
  • 对于频繁访问的数据,使用 Redis 等缓存系统。

具体实践:

  1. 索引优化示例
-- 假设有一个经常用于查询的字段 user_id
ALTER TABLE orders ADD INDEX idx_user_id (user_id);
  1. 查询优化示例
-- 优化前
SELECT * FROM orders WHERE user_id = 1;

-- 优化后
SELECT id, order_date, total_amount 
FROM orders 
WHERE user_id = 1 
LIMIT 100;
  1. 使用 EXPLAIN 分析
EXPLAIN SELECT * FROM orders WHERE user_id = 1;
  1. 配置优化示例(my.cnf):
innodb_buffer_pool_size = 4G
innodb_log_file_size = 256M
innodb_flush_log_at_trx_commit = 2
  1. 使用存储过程优化复杂查询
DELIMITER //
CREATE PROCEDURE GetUserOrders(IN userId INT)
BEGIN
  SELECT id, order_date, total_amount 
  FROM orders 
  WHERE user_id = userId 
  ORDER BY order_date DESC 
  LIMIT 10;
END //
DELIMITER ;
  1. 分区表示例
ALTER TABLE orders 
PARTITION BY RANGE (YEAR(order_date)) (
  PARTITION p0 VALUES LESS THAN (2020),
  PARTITION p1 VALUES LESS THAN (2021),
  PARTITION p2 VALUES LESS THAN (2022),
  PARTITION p3 VALUES LESS THAN MAXVALUE
);

在实际项目中,我们通常会结合使用这些方法。例如,在一个电商系统中,我们遇到了订单查询慢的问题:

  1. 首先,我们通过慢查询日志识别了问题查询。
  2. 使用 EXPLAIN 分析后发现,order_date 和 user_id 上缺少索引。
  3. 添加了复合索引后,查询速度提升了,但仍不够理想。
  4. 进一步分析发现,订单表数据量已经达到了上亿级别。
  5. 我们决定按年份进行分区,并实现了读写分离。
  6. 对于热点数据(如最近3个月的订单),我们使用 Redis 进行缓存。
  7. 最后,我们还优化了应用层的查询逻辑,实现了分页加载。

这些措施综合起来,将原本需要几秒的查询优化到了毫秒级别。

处理慢查询是一个持续的过程,需要定期监控和优化。通过这些方法,我们不仅解决了当前的性能问题,还为未来的系统扩展打下了基础。

「面试官」:非常全面和深入的回答。看来你在数据库优化方面有丰富的经验。今天的面试到此结束,你的表现很出色。还有什么问题想问我的吗?

『求职者』:谢谢您的肯定。我确实有几个问题想请教:

  1. 贵公司在技术栈选择上有什么特别的考虑吗?特别是在处理大规模数据和高并发方面。
  2. 对于新入职的员工,公司有什么培训或成长计划吗?
  3. 在当前的团队中,您认为最大的技术挑战是什么?
  4. 公司如何看待工作与生活的平衡?有远程工作的政策吗?
  5. 在接下来的6-12个月里,团队或公司有什么重要的项目或技术方向吗?

这些问题可以帮助我更好地了解公司的技术方向、文化和发展前景。非常感谢您的时间和宝贵的反馈。

「面试官」:这些都是很好的问题。[面试官会根据公司实际情况回答这些问题]。感谢你今天的表现,我们会尽快给你反馈。祝你好运!

『求职者』:非常感谢您的回答和这次宝贵的面试机会。我对贵公司的技术实力和文化氛围留下了深刻的印象。我会耐心等待您的反馈,也希望有机会能加入您的团队,为公司的发展贡献自己的力量。再次感谢您的时间,祝您工作顺利!

建议直接收藏专栏,每日更新一次面经!不少于100篇!专栏地址👉:https://www.nowcoder.com/creation/manager/columnDetail/MKaNda ​ 辛苦大家点赞👍收藏📚评论💬!白露拜谢!

#面试##24届软开秋招面试经验大赏#
2024计算机面经详解 文章被收录于专栏

收录网络上的各个公司,包括但不限于阿里、字节、腾讯、小红书等公司的面经,并给出参考答案。 什么?!你不会面试?🤨 看这个就对了!😄 OfferNow!

全部评论
写得好好,但是感觉这些太难了对我来说
1 回复 分享
发布于 07-22 22:59 芬兰
点赞 回复 分享
发布于 07-21 14:37 广东
算法题写的有问题。 n = 21121, 第二位选变成2, 然后后面都是999, 22999,答案错误
点赞 回复 分享
发布于 08-31 19:43 浙江

相关推荐

不愿透露姓名的神秘牛友
11-22 11:13
美团 前端工程师 27k 本科985
点赞 评论 收藏
分享
22 160 评论
分享
牛客网
牛客企业服务