JAVA面试题经典十问,面试必问,附带详细答案解析

在面试中,常常遇到一些经典且易出错的问题,尤其是高并发、缓存、框架应用等模块。今天,我总结了十个必问的JAVA面试题,覆盖高并发超买超卖、索引、SpringBoot接口注解、Mybatis二级缓存、Redis和RabbitMQ应用场景、本地缓存框架Caffeine等核心内容。每个问题都附带详细解析、案例代码和应用场景,协助大家在面试中游刃有余,提升实战能力。文章内容充实,条理清晰,提议收藏反复阅读!


问题一:高并发场景下,如何防止商品超买和超卖?

问题描述:在电商系统中,多个用户同时抢购商品时,可能导致库存扣减错误,出现超买(库存扣成负数)或超卖(实际库存不足但订单生成)。请解释如何解决这一问题。

为什么易出错:许多开发者只思考单机锁(如synchronized),忽略分布式环境下的并发问题,或者对乐观锁、悲观锁的理解不够深入,导致在实际高并发场景中性能低下或数据不一致。

详细答案解析
超买和超卖的本质是并发条件下对共享资源(库存)的竞争。解决方案包括:

  • 悲观锁:假设并发冲突会发生,提前加锁,如数据库行锁(SELECT … FOR UPDATE)或Java中的synchronized、ReentrantLock。适用于写操作多的场景,但可能引发性能瓶颈。
  • 乐观锁:假设冲突较少,通过版本号或时间戳实现,如数据库的CAS(Compare And Swap)操作。适用于读多写少的场景,性能更高。
  • 分布式锁:在分布式系统中,使用Redis或ZooKeeper实现锁,确保跨服务节点的互斥访问。

案例代码
以下是一个使用数据库乐观锁的Java示例(基于SpringBoot和MyBatis)。假设商品表有id、stock和version字段。

// 商品实体类
public class Product {
    private Long id;
    private Integer stock;
    private Integer version;
    // getter和setter
}

// Service层方法:扣减库存
@Service
public class ProductService {
    @Autowired
    private ProductMapper productMapper;

    public boolean deductStock(Long productId) {
        // 循环重试,模拟乐观锁机制
        for (int i = 0; i < 3; i++) { // 最多重试3次
            Product product = productMapper.selectById(productId);
            if (product.getStock() <= 0) {
                return false; // 库存不足
            }
            int updated = productMapper.updateStockWithVersion(productId, product.getVersion());
            if (updated > 0) {
                return true; // 更新成功
            }
            // 版本冲突,重试
        }
        return false; // 更新失败
    }
}

// MyBatis Mapper接口
@Mapper
public interface ProductMapper {
    Product selectById(Long id);
    // 乐观锁更新:版本号匹配时才更新
    int updateStockWithVersion(@Param("id") Long id, @Param("version") Integer version);
}

对应的MyBatis XML映射文件:

<update id="updateStockWithVersion">
    UPDATE product 
    SET stock = stock - 1, version = version + 1 
    WHERE id = #{id} AND version = #{version}
</update>

应用场景描述
在电商秒杀活动中,例如“双11”抢购,每秒可能有数万请求。使用乐观锁可以避免数据库死锁,提升并发性能。如果冲突频繁,可以结合Redis分布式锁(如Redisson)或消息队列(如RabbitMQ)进行流量削峰,确保库存准确性。


问题二:数据库索引的原理和优化策略是什么?

问题描述:索引是数据库性能优化的关键,请解释索引的底层原理(如B+树),并说明如何设计高效索引以避免全表扫描。

为什么易出错:开发者常滥用索引,导致写性能下降,或忽略复合索引的最左前缀原则,造成索引失效。

详细答案解析

  • 索引原理:大多数数据库(如MySQL)使用B+树索引,其特点包括有序存储、叶子节点链表连接(利于范围查询)、非叶子节点只存键值(减少磁盘I/O)。B+树相比B树,更适合磁盘存储,由于所有数据都在叶子节点,查询稳定。
  • 优化策略
    • 选择合适的列:为高频查询条件(WHERE、JOIN、ORDER BY)创建索引。
    • 避免索引失效:注意最左前缀原则(复合索引中,必须从最左列开始使用),避免在索引列上使用函数或类型转换。
    • 索引类型:主键索引、唯一索引、普通索引。覆盖索引(索引包含所有查询字段)可以减少回表操作。
    • 监控与调整:使用EXPLAIN分析查询计划,避免Using filesort或Using temporary。

案例代码
假设有一个用户表user,字段包括id(主键)、name、age、city。
创建复合索引优化查询:

-- 创建复合索引(name, age)
CREATE INDEX idx_name_age ON user(name, age);

-- 有效查询:使用索引
EXPLAIN SELECT * FROM user WHERE name = '张三' AND age = 25;
-- 可能失效的查询:未使用最左前缀
EXPLAIN SELECT * FROM user WHERE age = 25; -- 索引失效,全表扫描

应用场景描述
在社交App中,用户常常根据姓名和年龄搜索好友。复合索引(name, age)可以加速这类查询。如果查询条件只包含age,索引失效,需思考单独为age建索引或调整查询顺序。


问题三:SpringBoot中常用接口注解有哪些?它们的作用和区别是什么?

问题描述:SpringBoot简化了Web开发,请列举常用接口注解(如@RestController、@RequestMapping),并解释它们的应用场景。

为什么易出错:初学者容易混淆@Controller和@RestController,或错误使用@RequestParam和@PathVariable,导致接口调用失败。

详细答案解析

  • @Controller vs @RestController
    • @Controller:用于MVC模式,返回视图名称,需配合@ResponseBody返回JSON。
    • @RestController:组合了@Controller和@ResponseBody,直接返回JSON数据,适用于RESTful API。
  • @RequestMapping:定义请求映射,可指定HTTP方法(GET、POST等)、路径、参数等。
  • @GetMapping/@PostMapping:简化版@RequestMapping,专用于特定HTTP方法。
  • @RequestParam vs @PathVariable
    • @RequestParam:用于查询参数,如/user?id=1。
    • @PathVariable:用于路径变量,如/user/1。
  • @RequestBody:接收JSON请求体,自动反序列化为对象。

案例代码
一个简单的用户管理API示例:

@RestController
@RequestMapping("/api/user")
public class UserController {
    
    // GET请求:路径变量
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
    
    // POST请求:请求体参数
    @PostMapping("/create")
    public String createUser(@RequestBody User user) {
        userService.save(user);
        return "用户创建成功";
    }
    
    // GET请求:查询参数
    @GetMapping("/list")
    public List<User> listUsers(@RequestParam String city) {
        return userService.findByCity(city);
    }
}

应用场景描述
在微服务架构中,前端通过RESTful API与后端交互。例如,用户管理模块使用@RestController提供JSON接口,@PathVariable用于资源标识(如用户ID),@RequestParam用于过滤条件(如城市查询)。正确使用注解能提高代码可读性和维护性。


问题四:Mybatis二级缓存是如何工作的?使用时有哪些注意事项?

问题描述:Mybatis提供一级缓存(SqlSession级别)和二级缓存(Mapper级别),请解释二级缓存的机制,并说明如何配置和避免脏读。

为什么易出错:二级缓存默认关闭,配置不当可能导致数据不一致(如多表关联更新时缓存未清除),或引发序列化问题。

详细答案解析

  • 工作原理:二级缓存是Mapper级别的缓存,多个SqlSession共享。当查询执行时,结果会被缓存;更新操作(INSERT、UPDATE、DELETE)会清除缓存,确保数据一致性。
  • 配置步骤
  • 在Mybatis配置文件中开启二级缓存:<setting value=”true”/>。
  • 在Mapper XML中添加<cache/>标签,可自定义缓存策略(如LRU)。
  • 注意事项
    • 实体类必须实现Serializable接口,由于缓存数据可能序列化到磁盘。
    • 避免在多表关联场景使用,由于更新关联表时,缓存可能不会自动清除。
    • 可以通过<cache-ref/>共享缓存,但需谨慎使用。

案例代码
配置Mybatis二级缓存的示例:
在mybatis-config.xml中:

<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

在Mapper XML文件(如UserMapper.xml)中:

<!-- 开启二级缓存,使用LRU策略 -->
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>

<select id="findById" resultType="User" useCache="true">
    SELECT * FROM user WHERE id = #{id}
</select>

<update id="updateUser" parameterType="User">
    UPDATE user SET name = #{name} WHERE id = #{id}
</update>

应用场景描述
在查询频繁、数据变化少的系统中,如新闻App的文章详情页,二级缓存可以显著减少数据库压力。但如果有频繁更新(如用户评论),需谨慎使用,或通过手动清除缓存(flushCache=”true”)避免脏读。


问题五:Redis在项目中的常见应用场景有哪些?

问题描述:Redis作为内存数据库,高性能且支持多种数据结构,请列举其典型应用场景,并说明优势。

为什么易出错:开发者可能过度依赖Redis,忽略数据持久化问题,或错误选择数据结构(如用String存储列表),导致性能不佳。

详细答案解析
Redis支持String、List、Set、Hash、ZSet等数据结构,常见应用场景包括:

  • 缓存:缓存热点数据(如用户信息、商品详情),减少数据库访问,提升响应速度。
  • 会话存储(Session Storage):分布式系统中存储用户会话,实现多服务器共享。
  • 分布式锁:通过SETNX命令实现互斥访问,解决高并发问题。
  • 消息队列:使用List或Pub/Sub实现异步任务处理。
  • 计数器/排行榜:利用ZSet实现实时排名,如电商销量榜。

案例代码
使用RedisTemplate在SpringBoot中实现缓存和分布式锁:

@Service
public class RedisService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 缓存用户信息
    public User getUserById(Long id) {
        String key = "user:" + id;
        User user = (User) redisTemplate.opsForValue().get(key);
        if (user == null) {
            user = userService.findById(id); // 从数据库查询
            redisTemplate.opsForValue().set(key, user, Duration.ofMinutes(30)); // 缓存30分钟
        }
        return user;
    }

    // 分布式锁示例
    public boolean tryLock(String key, String value, long expireTime) {
        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofSeconds(expireTime));
        return Boolean.TRUE.equals(result);
    }

    public void unlock(String key) {
        redisTemplate.delete(key);
    }
}

应用场景描述
在在线教育平台中,Redis可用于缓存课程信息(减少数据库压力)、存储用户学习进度(Session存储)、实现秒杀系统的分布式锁(防止超买)。例如,使用ZSet维护课程热度排行榜,实时更新访问量。


问题六:RabbitMQ如何保证消息的可靠传输?应用在哪些业务场景?

问题描述:消息队列是解耦系统的利器,请解释RabbitMQ如何防止消息丢失,并举例说明其应用场景。

为什么易出错:开发者可能忽略消息确认机制(ACK)或持久化设置,导致消息在传输过程中丢失。

详细答案解析
RabbitMQ通过以下机制保证可靠性:

  • 生产者确认(Publisher Confirm):生产者发送消息后,等待Broker确认,确保消息到达交换机。
  • 消息持久化:将消息和队列设置为持久化(durable=true),防止服务器重启后消息丢失。
  • 消费者确认(Consumer ACK):消费者处理消息后手动发送ACK,Broker才删除消息;如果处理失败,可重试或进入死信队列。
  • 事务机制:但性能较差,一般推荐使用Confirm模式。

应用场景

  • 异步处理:如用户注册后发送邮件,避免阻塞主流程。
  • 流量削峰:在高并发场景(如秒杀)中,将请求缓冲到队列,逐步处理。
  • 系统解耦:微服务间通过消息通信,降低依赖。

案例代码
SpringBoot整合RabbitMQ,实现可靠消息发送和消费:

// 配置类,定义队列和交换机
@Configuration
public class RabbitConfig {
    @Bean
    public Queue orderQueue() {
        return new Queue("order.queue", true); // 持久化队列
    }
}

// 生产者
@Service
public class OrderService {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void createOrder(Order order) {
        // 发送消息,设置Confirm回调
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if (!ack) {
                // 消息发送失败,记录日志或重试
                System.out.println("消息发送失败: " + cause);
            }
        });
        rabbitTemplate.convertAndSend("order.queue", order);
    }
}

// 消费者
@Component
public class OrderConsumer {
    @RabbitListener(queues = "order.queue")
    public void processOrder(Order order, Channel channel, Message message) throws IOException {
        try {
            // 处理订单逻辑
            orderService.process(order);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); // 手动ACK
        } catch (Exception e) {
            // 处理失败,重试或进入死信队列
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
        }
    }
}

应用场景描述
在电商订单系统中,用户下单后,通过RabbitMQ异步通知库存服务和物流服务。即使某个服务暂时不可用,消息也不会丢失,确保最终一致性。例如,秒杀场景中,订单请求先进入队列,再逐步处理,避免系统崩溃。


问题七:本地缓存框架Caffeine的使用方法和优势是什么?

问题描述:Caffeine是高性能的Java本地缓存库,请说明其基本用法,并对比Guava Cache的优势。

为什么易出错:开发者可能忽略缓存过期策略或内存管理,导致内存泄漏或缓存雪崩。

详细答案解析
Caffeine基于Window TinyLFU算法,提供高命中率和低内存占用。优势包括:

  • 高性能:读写操作接近O(1),适合高并发场景。
  • 灵活配置:支持基于大小、时间、引用等的缓存淘汰策略。
  • 异步刷新:自动刷新缓存值,避免过期数据。
    常用API:Cache接口用于手动管理,LoadingCache自动加载数据。

案例代码
在SpringBoot项目中集成Caffeine:
第一添加依赖(Maven):

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version>
</dependency>

Java配置和使用:

@Configuration
public class CacheConfig {
    @Bean
    public Cache<String, User> userCache() {
        return Caffeine.newBuilder()
                .maximumSize(1000) // 最大缓存条目
                .expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期
                .build();
    }
}

@Service
public class UserService {
    @Autowired
    private Cache<String, User> userCache;

    public User getUser(String id) {
        return userCache.get(id, key -> {
            // 缓存未命中时,从数据库加载
            return userRepository.findById(id);
        });
    }
}

应用场景描述
在网关或API服务中,Caffeine可用于缓存频繁访问的配置数据或用户令牌。例如,在微服务架构中,每个实例本地缓存路由信息,减少对配置中心的请求,提升响应速度。相比Guava Cache,Caffeine在高并发下性能更优,且内存效率更高。


问题八:Java中synchronized和ReentrantLock的区别是什么?如何选择?

问题描述:synchronized和ReentrantLock都是Java中的锁机制,请对比它们的特性和适用场景。

为什么易出错:开发者可能误用锁导致死锁,或忽略ReentrantLock的高级功能(如公平锁、条件变量)。

详细答案解析

  • synchronized
    • 关键字,JVM内置锁,自动获取和释放。
    • 支持可重入,但不支持中断和超时。
    • 简单易用,但功能有限。
  • ReentrantLock
    • 类,需手动加锁和解锁,灵活性高。
    • 支持公平锁、非公平锁、可中断、超时尝试等。
    • 需在finally块中解锁,避免死锁。
      选择原则:简单场景用synchronized,复杂并发控制(如需要条件变量)用ReentrantLock。

案例代码
使用ReentrantLock实现线程安全的计数器:

public class Counter {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock(); // 可重入锁

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock(); // 确保解锁
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

对比synchronized版本:

public class SynchronizedCounter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

应用场景描述
在银行转账系统中,synchronized适合简单的账户余额操作,而ReentrantLock可用于复杂交易,如需要超时回滚(tryLock)或条件等待(Condition)。例如,转账时如果目标账户被锁定,ReentrantLock可以设置超时,避免无限等待。


问题九:SpringBoot中如何通过注解实现接口权限控制?

问题描述:在Web应用中,接口权限控制是常见需求,请说明如何使用Spring Security或自定义注解实现。

为什么易出错:开发者可能混淆角色和权限,或忽略注解的继承性,导致权限验证漏洞。

详细答案解析
Spring Security提供注解如@PreAuthorize、@PostAuthorize进行方法级权限控制。

  • @PreAuthorize:在方法执行前验证权限,常用SpEL表达式。
  • @Secured:基于角色验证,但功能较弱。
  • 自定义注解:结合AOP实现灵活控制,例如@RequiresPermission。

案例代码
使用Spring Security注解的示例:
第一添加依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

配置SecurityConfig:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // 配置用户和角色
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("admin").password("{noop}123").roles("ADMIN")
            .and()
            .withUser("user").password("{noop}123").roles("USER");
    }
}

在Controller中使用注解:

@RestController
public class AdminController {
    // 只有ADMIN角色可访问
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/admin/data")
    public String getAdminData() {
        return "管理员数据";
    }

    // 自定义权限表达式
    @PreAuthorize("hasAuthority('READ')")
    @GetMapping("/user/data")
    public String getUserData() {
        return "用户数据";
    }
}

自定义注解示例:

// 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasPermission(#id, 'READ')")
public @interface RequiresReadPermission {
}

// 在Controller中使用
@RestController
public class CustomController {
    @RequiresReadPermission
    @GetMapping("/resource/{id}")
    public String getResource(@PathVariable Long id) {
        return "资源数据";
    }
}

应用场景描述
在企业OA系统中,不同角色(如管理员、普通员工)访问不同接口。使用@PreAuthorize可以精细控制权限,例如只有管理员才能删除用户。自定义注解简化了重复配置,提升代码可读性。


问题十:Mybatis中一级缓存和二级缓存的详细对比?

问题描述:Mybatis的缓存机制是性能优化重点,请详细比较一级缓存和二级缓存的Scope、生命周期和适用场景。

为什么易出错:开发者可能混淆两者范围,导致缓存数据不一致,或错误配置二级缓存引发脏读。

详细答案解析

  • 一级缓存
    • Scope:SqlSession级别,同一个SqlSession内共享。
    • 生命周期:随SqlSession创建而创建,关闭而清除。
    • 默认开启,无法关闭。
    • 适用场景:短时操作,如单个请求中的多次一样查询。
  • 二级缓存
    • Scope:Mapper级别,多个SqlSession共享。
    • 生命周期:应用级别,需手动配置。
    • 通过<cache/>标签开启,可自定义策略。
    • 适用场景:跨请求的共享数据,但需注意数据一致性。
      对比总结:一级缓存轻量但范围小,二级缓存范围大但需管理。

案例代码
演示一级缓存行为:

// 在同一个SqlSession中,查询一样数据只执行一次SQL
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user1 = mapper.findById(1L); // 执行SQL
User user2 = mapper.findById(1L); // 从一级缓存获取,不执行SQL
sqlSession.close(); // 缓存清除

二级缓存配置见问题四。

应用场景描述
在Web应用中,用户在一次会话中多次查看个人资料,一级缓存可减少数据库访问。而文章详情等公共数据,使用二级缓存跨会话共享,提升整体性能。但更新操作需及时清除缓存,避免脏读。


结语

通过以上十个问题的解析,我们覆盖了JAVA面试中的高并发、数据库、框架集成和缓存等核心模块。每个问题都结合案例代码和应用场景,协助大家深入理解。面试时,不仅要记住答案,更要理解背后的原理和适用条件。提议多动手实践,积累项目经验。如果觉得有用,请收藏分享,关注我获取更多技术干货!如有疑问,欢迎在评论区讨论。

© 版权声明

相关文章

4 条评论

  • 头像
    韩解放 读者

    💗感谢分享

    无记录
    回复
  • 头像
    韩路 投稿者

    向你学习👍

    无记录
    回复
  • 头像
    玫瑰先行词 投稿者

    大神💪

    无记录
    回复
  • 头像
    海伦 读者

    加油👏

    无记录
    回复