京东物流面试汇总(四)

1. 为什么有了 int 还需要 Integer

答:

  • int 是 Java 的基本数据类型,直接存储数值,效率高。
  • Integerint 的包装类(对象),提供了以下优势:
    • 面向对象: 可以参与面向对象编程,例如放入集合中。
    • null 值: Integer 可以为 null,表示数值缺失,而 int 不行。
    • 额外方法: Integer 类提供了很多实用方法,例如 parseInt()compareTo() 等。
    • 泛型支持: 泛型集合只能存储对象,不能直接存储基本类型。

2. 多线程编程:如何做死锁的检查和预防?如何避免死锁?

答:

死锁检查:

  • 代码审查: 仔细检查代码,特别是涉及多个锁的部分。
  • 工具:
    • jstack: 用于生成 Java 线程转储,可以分析线程状态和锁持有情况。
    • VisualVM、JConsole: 图形化监控工具,可以检测死锁。
  • 日志: 记录锁的获取和释放,便于事后分析。

死锁预防/避免:

  • 破坏互斥条件: 尽量避免使用锁,例如使用无锁数据结构(ConcurrentHashMap)。
  • 破坏请求与保持条件: 一次性申请所有需要的资源(锁)。 如果资源无法同时获取,则释放已获取的资源。
  • 破坏不剥夺条件: 允许操作系统剥夺进程持有的资源(不常用)。
  • 破坏循环等待条件: 对资源进行排序,按照固定的顺序申请资源。 这是一种常用的方法。
  • 使用超时机制: tryLock(timeout) 尝试在指定时间内获取锁,如果超时则放弃。
  • 避免嵌套锁: 尽量不要在一个锁的范围内获取另一个锁。
  • 降低锁的粒度: 将大锁分解为多个小锁,减少锁竞争。
  • 使用并发工具类: 例如 ReentrantLockSemaphoreCountDownLatch 等,它们提供了更灵活的锁机制。

3. 为什么很多公司对 Java 线程池很谨慎?

答:

线程池使用不当可能导致严重的问题,因此公司会比较谨慎:

  • 资源耗尽:
    • OOM(OutOfMemoryError): 线程过多导致内存溢出。
    • CPU 负载过高: 过多的线程竞争 CPU 资源,导致系统响应缓慢。
  • 死锁: 线程池中的线程互相等待,导致所有任务都无法完成。
  • 线程泄漏: 线程执行完任务后没有正确回收,导致线程池中的线程越来越少。
  • 性能问题:
    • 频繁的线程创建和销毁: 消耗大量系统资源。
    • 上下文切换: 线程切换的开销。
  • 任务拒绝: 线程池已满,新的任务无法提交。

谨慎的原因:

  • 配置复杂: 线程池的参数(核心线程数、最大线程数、队列类型、拒绝策略等)需要根据具体应用场景进行调整,配置不当容易出现问题。
  • 排查困难: 线程池的问题通常难以定位,需要深入理解线程池的工作原理。
  • 影响面广: 线程池的问题可能会影响整个系统的性能和稳定性。

4. SQL 代码:有一个学生信息表 students(id, student_id, course, score),写一个 SQL 语句来查找总分最高的前 10 名同学。

答:

```sql
SELECT student_id, SUM(score) AS total_score
FROM students
GROUP BY student_id
ORDER BY total_score DESC
LIMIT 10;
```

5. 建表过程中的索引规范

答:

  • 索引列选择:
    • 仅为 WHEREORDER BYJOIN 中使用的列创建索引。考虑查询频率和重要性。
  • 索引类型:
    • B-Tree: 默认索引类型,适用于范围查询、排序等。适用性最广。
    • Hash: 适用于等值查询,速度快,但不支持范围查询,不常用。
    • Fulltext: 用于全文搜索,适用于文本字段。
    • 空间索引 (GIS): 用于地理位置数据,例如查找附近的地点。
  • 组合索引:
    • 多个列经常一起出现在查询条件中时,创建组合索引比多个单列索引更有效。
    • 最左前缀原则: 查询必须从组合索引的最左边的列开始,否则无法使用索引。
    • 索引顺序: 将选择性最高的列(区分度最高的列)放在组合索引的最左边。
  • 索引数量:
    • 每个表上的索引数量应该控制在 5 个以内。索引会占用存储空间,并且会降低 INSERTUPDATEDELETE 的性能。
    • 避免创建过多冗余索引。
  • 索引维护:
    • 定期分析和优化: 数据库系统会自动收集索引的使用情况,并提供优化建议。
    • 删除不使用的索引: 及时删除不再使用的索引,可以减少存储空间和提高写入性能。
    • 重建索引: 对于频繁修改的表,索引可能会变得碎片化,定期重建索引可以提高查询性能。
  • 避免在 WHERE 子句中使用函数/表达式:
    • 例如:WHERE DATE(create_time) = '2023-10-27' 应该改为 WHERE create_time >= '2023-10-27 00:00:00' AND create_time < '2023-10-28 00:00:00', 避免在索引列上使用函数或表达式,这会导致索引失效。
  • 使用 EXPLAIN 分析查询:
    • EXPLAIN 命令可以显示 SQL 语句的执行计划,可以帮助你判断是否使用了索引,以及索引的使用效率,从而进行索引优化。 关注 typekeyrows 等字段。
  • 考虑业务场景:
    • 根据实际业务场景选择合适的索引策略。
    • 例如,对于读多写少的场景,可以适当增加索引,以提高查询性能。 对于写多读少的场景,应该尽量减少索引,以提高写入性能。
  • 监控索引使用情况:
    • 监控索引的使用情况,及时发现和解决索引相关的问题。

6. 10 亿手机号,如何判断一个手机号是否存在其中?

答:

  • Bloom Filter(首选):
    • 原理: 一种概率型数据结构,用于快速判断一个元素是否存在于一个集合中。 通过多个哈希函数将元素映射到位数组中。
    • 优点: 空间效率高,查询速度快。
    • 缺点: 存在误判率(False Positive),即可能会将不存在的手机号判断为存在。 误判率可以通过调整 Bloom Filter 的参数来控制。 不能删除元素。
    • 适用场景: 允许一定的误判率,对空间要求比较高的场景,例如缓存穿透的解决。
    • 实现: 可以使用 Guava 库中的 BloomFilter 类。
  • HashSet (如果内存足够):
    • 原理: 基于哈希表实现,用于存储不重复的元素。
    • 优点: 查询速度快,没有误判率。
    • 缺点: 占用内存大。
    • 适用场景: 内存足够,对查询准确性要求高的场景。
  • BitSet:
    • 原理: 使用位数组来存储数据,每个位表示一个元素是否存在。
    • 优点: 空间效率高,适用于存储整数类型的数据。
    • 缺点: 只能存储整数类型的数据,不适合存储字符串类型的数据。
    • 适用场景: 手机号是连续的整数的情况。
  • 分治法 + HashSet/Bloom Filter:
    • 原理: 将 10 亿手机号分成多个小文件,分别加载到 HashSet/Bloom Filter 中进行判断。
    • 优点: 可以降低内存占用。
    • 缺点: 需要多次读取文件,效率较低。
  • 数据库索引:
    • 原理: 将手机号存储到数据库中,使用索引进行查询。
    • 优点: 可以保证数据的一致性和可靠性。
    • 缺点: 效率较低,不推荐。

示例(Bloom Filter - Guava):

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import java.nio.charset.Charset;

public class PhoneNumberChecker {
    public static void main(String[] args) {
        // 创建 Bloom Filter,设置预期插入数量和误判率
        BloomFilter<String> bloomFilter = BloomFilter.create(
                Funnels.stringFunnel(Charset.forName("UTF-8")),
                1000000000L, // 预期插入 10 亿个元素
                0.01 // 误判率 1%
        );

        // 假设已经将 10 亿个手机号添加到 Bloom Filter 中

        // 检查手机号是否存在
        String phoneNumber = "13800000000";
        boolean mightContain = bloomFilter.mightContain(phoneNumber);

        if (mightContain) {
            System.out.println("手机号可能存在");
        } else {
            System.out.println("手机号不存在");
        }
    }
}

7. 一根香烧完需要30分钟,怎么样得到15分钟内计时,怎样得到7.5分钟的计时?

答:

  • 15 分钟:
    • 同时点燃香的两端。 因为两端同时燃烧,燃烧速度是原来的两倍,所需时间减半。
  • 7.5 分钟:
    1. 点燃香 A 的两端。
    2. 同时,点燃香 B 的一端。
    3. 当香 A 烧完时(15 分钟),立即点燃香 B 的另一端。 此时香 B 已经燃烧了 15 分钟,剩余的长度为香 B 的一半。
    4. 香 B 现在两端同时燃烧,剩余的长度会在 7.5 分钟内烧完。

8. Redis 数据结构和适用场景

答:

  • String(字符串):
    • 内部编码: intembstrraw
    • 适用场景:
      • 缓存: 缓存热点数据,例如用户信息、配置信息等。
      • 计数器: 例如文章阅读数、点赞数等。 可以使用 INCRDECR 等命令实现原子计数。
      • 分布式锁: 使用 SETNX 命令实现分布式锁。
      • Session 共享: 存储 Session 信息。
      • 存储对象: 将对象序列化成 JSON 字符串存储。
  • List(列表):
    • 内部编码: ziplistlinkedlist
    • 适用场景:
      • 消息队列: 使用 LPUSHRPOP 命令实现简单的消息队列。
      • 最新列表: 例如朋友圈的时间线、新闻的最新列表等。 可以使用 LPUSH 命令将最新的元素添加到列表头部,然后使用 LTRIM 命令限制列表的长度。
      • 栈/队列: 可以使用 LPUSHLPOP 命令实现栈,使用 LPUSHRPOP 命令实现队列。
  • Set(集合):
    • 内部编码: intsethashtable
    • 适用场景:
      • 标签: 例如给用户添加兴趣标签。
      • 社交关系: 存储用户的粉丝、关注等信息。 可以使用 SADDSREM 等命令添加和删除元素。
      • 共同好友: 可以使用 SINTER 命令计算多个集合的交集,例如计算多个用户的共同好友。
      • UV(Unique Visitor)统计: 可以使用 SADD 命令将每个访问用户的 ID 添加到集合中,然后使用 SCARD 命令获取集合的大小,即 UV 值。
  • Sorted Set(有序集合):
    • 内部编码: ziplistskiplist
    • 适用场景:
      • 排行榜: 例如游戏积分排行榜、销售额排行榜等。 可以使用 ZADD 命令添加元素,并设置分数,然后使用 ZRANGE 命令按照分数排序获取元素。
      • 优先级队列: 按照优先级处理任务。
      • 带权重的消息队列: 按照权重发送消息。
  • Hash(哈希表):
    • 内部编码: ziplisthashtable
    • 适用场景:
      • 对象存储: 存储对象的字段和值。
      • 购物车: 存储用户的购物车信息。 可以使用 HSET 命令设置字段的值,使用 HGET 命令获取字段的值。
      • 存储用户资料: 相比 String,更节省空间,也更直观。
  • Bitmap(位图):
    • 适用场景:
      • 用户签到: 记录用户的签到状态,可以使用 SETBIT 命令设置某一位的值,表示用户是否签到。
      • 活跃用户统计: 统计用户的活跃度,可以使用 BITCOUNT 命令统计位图中值为 1 的位的个数。
      • 统计用户在线状态: 记录用户是否在线。
  • HyperLogLog:
    • 适用场景:
      • UV 统计(近似值): 海量数据的 UV 统计,允许一定的误差。 适用于对统计精度要求不高的场景。
  • Geospatial:
    • 适用场景:
      • 附近的人: 查找附近的人。
      • 地理围栏: 判断用户是否进入某个区域。
      • 地图应用: 存储地理位置信息。

9. Linux awkgrep 命令是什么?如何用正则表达式匹配 AxxxxAxxx

答:

  • grep 命令:
    • 作用: 用于在文件中查找匹配指定模式的行,并将结果输出到标准输出。
    • 常用选项:
      • -i:忽略大小写。
      • -v:反向查找,只输出不匹配的行。
      • -n:显示行号。
      • -r:递归查找,在目录中查找所有文件。
      • -E:使用扩展正则表达式。
  • awk 命令:
    • 作用: 用于处理文本文件,可以按照指定的规则对文件进行分割、过滤、计算和格式化输出。
    • 基本语法: awk 'pattern {action}' file
      • pattern:用于匹配行的正则表达式。
      • action:匹配行后执行的动作。
    • 常用内置变量:
      • $0:表示整行。
      • $1:表示第一个字段。
      • $2:表示第二个字段。
      • NF:表示字段的数量。
      • NR:表示行号。

用正则表达式匹配 AxxxxAxxx

  • grep 命令:

    grep "A....A..." file.txt
    
    • 也可以使用扩展正则表达式:
    grep -E "A....A..." file.txt
    
    • 或者使用 {} 指定重复次数 (注意转义):
    grep "A\{4\}A\{3\}" file.txt
    
  • awk 命令:

    awk '/A....A.../' file.txt
    

    解释:

    • A: 匹配字符 "A"。
    • .: 匹配任意单个字符。
    • ....: 匹配任意四个字符。
    • ...: 匹配任意三个字符。

10. str1 = "a"str2 = new String("a") 的区别在内存分配上有什么区别?会造成什么影响?如何解决?

答:

  • str1 = "a"
    • 内存分配: 首先在字符串常量池中查找是否存在字符串 "a",如果存在,则将 str1 指向字符串常量池中的 "a";如果不存在,则在字符串常量池中创建 "a",然后将 str1 指向 "a"。
    • 字符串常量池: 字符串常量池是 JVM 中的一个特殊的存储区域,用于存储字符串字面量。 它的目的是为了避免重复创建相同的字符串对象,节省内存空间。
    • 共享性: 多个字符串字面量如果值相同,则指向字符串常量池中的同一个对象,实现了共享。
  • str2 = new String("a")
    • 内存分配: 首先在堆中创建一个新的 String 对象,然后在字符串常量池中查找是否存在字符串 "a",如果存在,则将 String 对象中的 value 数组指向字符串常量池中的 "a";如果不存在,则在字符串常量池中创建 "a",然后将 String 对象中的 value 数组指向 "a"。
    • 堆: 堆是 JVM 中的一个存储区域,用于存储对象。
    • 独占性: 每次都会在堆中创建一个新的 String 对象,即使字符串的值相同,也会创建不同的对象,不会共享。

区别:

  • str1 指向字符串常量池中的对象, str2 指向堆中的对象。
  • str1 节省内存空间, str2 浪费内存空间。
  • str1 的创建效率高, str2 的创建效率低。

影响:

  • str2 可能会造成内存浪费,特别是在大量创建字符串对象时。
  • str2 可能会导致性能下降,因为每次创建对象都需要分配内存和进行垃圾回收。
  • == 比较时, str1 == str2 的结果为 false ,因为它们指向不同的对象。 equals() 比较时, str1.equals(str2) 的结果为 true ,因为它们的值相同。

解决:

  • 使用 String.intern() 方法:

    • str2.intern() 方法会在字符串常量池中查找是否存在与 str2 值相同的字符串,如果存在,则返回字符串常量池中的对象;如果不存在,则将 str2 的值添加到字符串常量池中,并返回字符串常量池中的对象。
    • 使用 intern() 方法可以将堆中的字符串对象放入字符串常量池中,实现共享,节省内存空间。
    String str1 = "a";
    String str2 = new String("a");
    String str3 = str2.intern();
    System.out.println(str1 == str2); // false
    System.out.println(str1 == str3); // true
    
  • 尽量使用字符串字面量:

    • 避免使用 new String() 创建字符串对象。
    • 如果需要拼接字符串,可以使用 StringBuilderStringBuffer 类。

11. 用 Redis 做二级缓存的时候,如何保证高并发的数据一致性?

答:

在高并发场景下,保证 Redis 作为二级缓存的数据一致性是一个挑战。 常用的策略和方案如下:

  • Cache-Aside Pattern(最常用,推荐):

    • 读取数据流程:
      1. 应用程序首先尝试从 Redis 缓存中读取数据。
      2. 缓存命中(Cache Hit): 如果 Redis 中存在所需数据,则直接返回。
      3. 缓存未命中(Cache Miss): 如果 Redis 中不存在所需数据,则应用程序从数据库中读取数据。
      4. 将从数据库读取的数据写入 Redis 缓存,并设置合理的过期时间。
    • 更新数据流程:
      1. 应用程序首先更新数据库中的数据。
      2. 删除 Redis 缓存中的数据 (而不是更新缓存)。 这是保证数据一致性的关键!
    • 优点:
      • 实现简单,易于理解。
      • 能够保证最终一致性。
    • 缺点:
      • 存在缓存不一致的风险: 在更新数据库和删除缓存之间存在时间差,可能导致脏数据。
      • 首次请求未命中的数据时,需要从数据库加载数据,性能略有损耗。
    • 缓存不一致风险的缓解方案:
      • 设置合理的缓存过期时间: 尽量保证缓存中的数据在一段时间内与数据库中的数据保持一致。 过期时间不宜过长,避免长时间的数据不一致。
      • 延迟双删策略: 在更新数据库后,先删除缓存,然后延迟一段时间(例如几百毫秒)再次删除缓存。 这种方式可以尽可能地消除缓存不一致的窗口期。
      • 异步更新缓存: 使用消息队列或 Change Data Capture (CDC) 技术,异步地更新缓存。
      • 加锁: 在更新数据库和删除缓存时,使用分布式锁,防止并发请求导致的数据不一致。
  • Read-Through/Write-Through Pattern:

    • 应用程序只与缓存交互,缓存组件负责与数据库进行同步。
    • Read-Through: 当应用程序从缓存中读取数据时,如果缓存不存在,则缓存组件自动从数据库加载数据,并将其添加到缓存中。
    • Write-Through: 当应用程序向缓存中写入数据时,缓存组件同时将数据写入数据库。
    • 优点: 应用程序无需关心缓存和数据库的同步问题。
    • 缺点: 实现复杂,性能不如 Cache-Aside Pattern。 通常需要定制缓存组件。
  • Read-Through/Write-Behind Pattern (也称为 Write-Back):

*   应用程序只与缓存交互,缓存组件先异步更新数据库。
*   **优点:** 写入性能高,因为写入缓存后立即返回。
*   **缺点:** 数据一致性风险非常高。 可能存在数据丢失的风险。 不常用。
  • 同步更新数据库和缓存(不推荐):

    • 同时更新数据库和缓存。
    • 缺点: 性能差,因为需要同时更新数据库和缓存。 数据一致性问题仍然存在,因为无法保证更新数据库和缓存的原子性。
  • 基于 Canal 的数据同步:

    • Canal 是阿里巴巴开源的 MySQL binlog 增量订阅 & 消费组件。
    • Canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave,向 MySQL master 发送 dump 协议。
    • MySQL master 收到 dump 请求,会向 Canal 发送 binlog。
    • Canal 解析 binlog,并将数据变更推送到 Redis 缓存。
    • 优点: 能够保证缓存和数据库的最终一致性。
    • 缺点: 增加了系统的复杂性。
  • 解决缓存并发问题:

    • 缓存击穿: 某个热点缓存过期,大量请求同时访问数据库。
      • 解决方案:
        • 互斥锁 (SETNX): 只允许一个请求访问数据库,并将结果写入缓存。 其他请求等待锁释放后,直接从缓存读取数据。
        • 永不过期: 将热点缓存设置为永不过期。
    • 缓存雪崩: 大量缓存同时过期,导致大量请求同时访问数据库。
      • 解决方案:
        • 随机过期时间: 为每个缓存设置随机的过期时间,避免它们同时过期。
        • 互斥锁: 同缓存击穿。
        • 服务降级: 暂停部分服务,减轻数据库压力。
    • 缓存穿透: 查询不存在的数据,导致每次请求都访问数据库。
      • 解决方案:
        • 缓存空对象: 如果数据库查询结果为空,则将空对象写入缓存。 并设置较短的过期时间,避免缓存占用过多空间。
        • Bloom Filter: 在缓存之前,使用 Bloom Filter 过滤掉不存在的 key。

总结:
Cache-Aside 模式是最常用的二级缓存解决方案。 合理设置缓存过期时间、使用延迟双删策略、并结合互斥锁、随机过期时间、缓存空对象、Bloom Filter 等技术,可以有效地解决缓存一致性问题和并发问题,保证系统的高性能和高可用性。

12. Redis 如何做持久化?

  • RDB (Redis Database):

    • 原理: 快照,定期将内存数据保存到磁盘。
    • 触发:
      • 自动: redis.conf (save 指令)
      • 手动: SAVE (阻塞, 禁用), BGSAVE (后台, 推荐)
    • 文件: dump.rdb (可配置)
    • 优点: 备份恢复快,性能影响小.
    • 缺点: 可能丢失数据, RDB创建耗时。
  • AOF (Append Only File):

    • 原理: 记录每个写命令。
    • 文件: appendonly.aof (可配置)
    • 优点: 数据安全(不同fsync策略), 可读易分析。
    • 缺点: 文件大, 恢复慢, 性能影响(取决于fsync)。
    • fsync 策略:
      • always: 每次写都同步 (最安全, 最慢)
      • everysec: 每秒同步 (推荐)
      • no: 操作系统决定 (最快, 最不安全)
    • AOF 重写: 减少AOF文件大小。BGREWRITEAOF
  • 选择: 同时启用(高安全), RDB(备份), 不启用(高性能). AOF优先恢复.

13. 如何查找订单表中对应用户的订单?

SELECT *
FROM orders
WHERE user_id = 用户ID; -- 替换为实际用户ID
## 14. 数据库事务隔离机制?

*   **读未提交 (Read Uncommitted):**
    *   **问题:** 脏读
    *   **隔离级别:** 最低
    *   **性能:** 最高
*   **读已提交 (Read Committed):**
    *   **问题:** 不可重复读
*   **可重复读 (Repeatable Read):**
    *   **问题:** 幻读
*   **串行化 (Serializable):**
    *   **隔离级别:** 最高
    *   **性能:** 最低
*   **总结:**

    | 隔离级别           | 脏读 | 不可重复读 | 幻读 |
    | ------------------ | ---- | -------- | ---- |
    | 读未提交           | 是   | 是       | 是   |
    | 读已提交           | 否   | 是       | 是   |
    | 可重复读           | 否   | 否       | 是   |
    | 串行化             | 否   | 否       | 否   |

*   **默认隔离级别:**
    *   MySQL (InnoDB): 可重复读
    *   其他: 读已提交
*   **设置隔离级别:** `SET TRANSACTION ISOLATION LEVEL ...`

## 15. Redis 加锁命令

*   **`SETNX key value`:**
    *   **原理:** key不存在时设置值, 返回1成功,0失败。
    *   **释放锁:** `DEL key`
    *   **问题:** 死锁 (锁过期),手动设置过期时间。
*   **`SET key value EX seconds NX` (Redis 2.6.12+):**
    *   **原子性:** 设置值和过期时间。
    *   **解决:** 死锁问题。
*   **Redlock 算法:**
    *   **原理:** 在多个Redis实例上尝试加锁, 大多数成功才算成功。
    *   **解决:** 单点故障。
    *   **复杂:** 实现复杂。
*   **加锁流程:** SET NX -> 执行业务 -> DEL.
*   **Lua 脚本:** 保证释放锁的原子性。

    ```lua
    if redis.call("get",KEYS[1]) == ARGV[1] then
        return redis.call("del",KEYS[1])
    else
        return 0
    end
    ```

## 16. 讲述下浏览器输入 URL 之后的过程?

1.  **URL 解析:** 提取协议, 域名, 端口, 路径等。
2.  **DNS 查询:**
    *   本地缓存 -> 本地DNS -> 根DNS -> 顶级DNS -> 权威DNS.
3.  **建立 TCP 连接:** 三次握手。
4.  **TLS 握手 (HTTPS, 如果是 HTTPS):** 协商加密算法和密钥。
5.  **发送 HTTP 请求:** 请求方法, URL, Headers, Body.
6.  **服务器处理请求:** 访问数据库, 调用API等。
7.  **服务器返回 HTTP 响应:** 状态码, Headers, Body。
8.  **浏览器渲染页面:**
    *   HTML -> DOM树
    *   CSS -> CSSOM树
    *   DOM + CSSOM -> 渲染树
    *   布局 (Layout), 绘制 (Paint)。
    *   JavaScript 下载和执行。
9.  **关闭 TCP 连接:**

## 17. 设计一个秒杀系统,库存10件商品,先用 redis.get 取数量,数量 - 1,再用 set 更新数据,如果 get 为 0,商品卖完了,这种情况安全吗?有什么问题?如何解决?

*   **安全性问题:** 不安全。 `get + -1 + set` 不是原子操作,高并发下会导致超卖。
*   **问题:**
    *   **非原子性:** 并发情况下,多个请求可能同时 `get` 到相同的值,导致库存扣减错误。
    *   **竞态条件:** 多个线程同时修改库存,可能导致数据不一致。
*   **解决方案:**
    *   **`DECR key`:** 原子减 1 操作。
        *   **Lua 脚本:** 封装原子操作,判断库存是否足够。

        ```lua
        local stock = redis.call("get", KEYS[1])
        if (stock and tonumber(stock) > 0) then
            redis.call("decr", KEYS[1])
            return 1
        else
            return 0
        end
        ```

    *   **Redis 乐观锁 (WATCH):**
        *   `WATCH key`, `GET key`, `MULTI`, `DECR key`, `EXEC`。
        *   `EXEC` 返回 `nil` 表示失败。
    *   **结合数据库事务:**
        *   Redis 预扣,数据库最终扣减。

*   **其他优化:**
    *   限流
    *   异步处理 (消息队列)
    *   缓存预热

## 18. 如果你有多个 IP 如何寻找访问最多前三的地址?

*   **MapReduce (海量数据):** Map -> Reduce -> 排序.
*   **HashMap + 堆 (内存足够):**

    1.  HashMap 统计次数.
    2.  小顶堆 (大小为 3) 维护 Top 3.

    ```java
    import java.util.*;

    public class Top3IPFinder {

        public static void main(String[] args) {
            String[] ipAddresses = {
                    "192.168.1.1", "192.168.1.2", "192.168.1.1", "192.168.1.3",
                    "192.168.1.1", "192.168.1.2", "192.168.1.4", "192.168.1.4",
                    "192.168.1.1", "192.168.1.5", "192.168.1.3", "192.168.1.3"
            };

            HashMap<String, Integer> ipCounts = new HashMap<>();
            for (String ip : ipAddresses) {
                ipCounts.put(ip, ipCounts.getOrDefault(ip, 0) + 1);
            }

            PriorityQueue<Map.Entry<String, Integer>> minHeap = new PriorityQueue<>(
                    Comparator.comparingInt(Map.Entry::getValue)
            );

            for (Map.Entry<String, Integer> entry : ipCounts.entrySet()) {
                if (minHeap.size() < 3) {
                    minHeap.offer(entry);
                } else if (entry.getValue() > minHeap.peek().getValue()) {
                    minHeap.poll();
                    minHeap.offer(entry);
                }
            }

            System.out.println("Top 3 IP Addresses:");
            while (!minHeap.isEmpty()) {
                Map.Entry<String, Integer> entry = minHeap.poll();
                System.out.println(entry.getKey() + ": " + entry.getValue());
            }
        }
    }
    ```

*   **分治法:** 分割成小文件处理。
*   **BitMap:** IP范围小。
*   **Spark/Flink:** 分布式计算框架.
## 19. 如果有 100G IP 地址,机器只有 5G 内存,如何寻找次数最多的前三 IP 地址? (Java 代码实现)

*   **问题本质:** 大数据量,内存限制。

*   **解决方案:** 分治法 (拆分 + 合并)。

*   **具体步骤:**

    1.  **哈希分割 (Hash Partitioning):**
        *   将 100G IP 地址文件分割成多个小文件。
        *   使用哈希函数将每个 IP 地址映射到其中的一个文件。
        *   **哈希函数:** `ip.hashCode() % N` (N 是文件数量)。
        *   **目标:** 保证相同的 IP 地址被分配到同一个文件中。
        *   **文件大小:** 选择合适的 N,使得每个文件的大小 <= 5G。
        *   例如,分成 20 个 5G 的文件。
        *   **注意:** 需要考虑哈希冲突,避免某个文件过大。 选择更好的哈希函数:guva的MurmurHash CityHash 或者检查文件大小 进行再次hash

    2.  **局部统计:**
        *   逐个读取每个小文件。
        *   使用 HashMap 统计每个 IP 地址出现的次数。
        *   因为每个文件 <= 5G,所以可以在内存中完成。
        *   对每个文件,找到出现次数最多的前三 IP 地址。
        *   可以使用小顶堆 (大小为 3) 来维护。
        *   将每个文件的前三 IP 地址及其计数保存下来。
        *   得到 20 个 (IP, count) 三元组的集合。

    3.  **全局合并:**
        *   将 20 个小文件的前三 IP 地址进行合并。
        *   再次使用 HashMap 统计这些 IP 地址出现的总次数。
        *   使用小顶堆 (大小为 3) 找到全局出现次数最多的前三 IP 地址。

*   **Java 代码示例:**

```java
import java.io.*;
import java.util.*;
import java.util.Map.Entry;

public class Top3IPAddresses {

    private static final int NUM_FILES = 20;
    private static final String INPUT_FILE = "ip_addresses.txt"; // 假设包含100G IP地址的文件
    private static final String TEMP_FILE_PREFIX = "temp_";

    public static void main(String[] args) throws IOException {
        splitFile(INPUT_FILE, NUM_FILES);

        List<List<Map.Entry<String, Integer>>> top3Lists = new ArrayList<>();
        for (int i = 0; i < NUM_FILES; i++) {
            String filePath = TEMP_FILE_PREFIX + i + ".txt";
            List<Map.Entry<String, Integer>> top3 = findTop3(filePath);
            top3Lists.add(top3);
        }

        List<Map.Entry<String, Integer>> finalTop3 = mergeTop3(top3Lists);
        System.out.println("Top 3 IP Addresses: " + finalTop3);

        // Clean up temporary files (optional)
        for (int i = 0; i < NUM_FILES; i++) {
            File tempFile = new File(TEMP_FILE_PREFIX + i + ".txt");
            tempFile.delete();
        }
    }

    // 1. 哈希分割
    private static void splitFile(String inputFile, int numFiles) throws IOException {
        List<BufferedWriter> writers = new ArrayList<>();
        for (int i = 0; i < numFiles; i++) {
            writers.add(new BufferedWriter(new FileWriter(TEMP_FILE_PREFIX + i + ".txt")));
        }

        try (BufferedReader reader = new BufferedReader(new FileReader(inputFile))) {
            String ip;
            while ((ip = reader.readLine()) != null) {
                int index = Math.abs(ip.hashCode()) % numFiles;
                writers.get(index).write(ip);
                writers.get(index).newLine();
            }
        } finally {
            for (BufferedWriter writer : writers) {
                writer.close();
            }
        }
    }

    // 2. 局部统计
    private static List<Map.Entry<String, Integer>> findTop3(String filePath) throws IOException {
        Map<String, Integer> ipCounts = new HashMap<>();
        try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
            String ip;
            while ((ip = reader.readLine()) != null) {
                ip = ip.trim();
                ipCounts.put(ip, ipCounts.getOrDefault(ip, 0) + 1);
            }
        }

        PriorityQueue<Map.Entry<String, Integer>> minHeap = new PriorityQueue<>(
                Comparator.comparingInt(Map.Entry::getValue)
        );

        for (Map.Entry<String, Integer> entry : ipCounts.entrySet()) {
            if (minHeap.size() < 3) {
                minHeap.offer(entry);
            } else if (entry.getValue() > minHeap.peek().getValue()) {
                minHeap.poll();
                minHeap.offer(entry);
            }
        }

        List<Map.Entry<String, Integer>> top3 = new ArrayList<>(minHeap);
        top3.sort(Comparator.comparingInt(Map.Entry::getValue).reversed()); // Sort in descending order

        return top3;
    }

    // 3. 全局合并
    private static List<Map.Entry<String, Integer>> mergeTop3(List<List<Map.Entry<String, Integer>>> top3Lists) {
        Map<String, Integer> globalCounts = new HashMap<>();
        for (List<Map.Entry<String, Integer>> top3 : top3Lists) {
            for (Map.Entry<String, Integer> entry : top3) {
                globalCounts.put(entry.getKey(), globalCounts.getOrDefault(entry.getKey(), 0) + entry.getValue());
            }
        }

        PriorityQueue<Map.Entry<String, Integer>> minHeap = new PriorityQueue<>(
                Comparator.comparingInt(Map.Entry::getValue)
        );

        for (Map.Entry<String, Integer> entry : globalCounts.entrySet()) {
            if (minHeap.size() < 3) {
                minHeap.offer(entry);
            } else if (entry.getValue() > minHeap.peek().getValue()) {
                minHeap.poll();
                minHeap.offer(entry);
            }
        }

        List<Map.Entry<String, Integer>> finalTop3 = new ArrayList<>(minHeap);
        finalTop3.sort(Comparator.comparingInt(Map.Entry::getValue).reversed()); // Sort in descending order
        return finalTop3;
    }
}

20. 如果订单表特别大,如何处理这个表格,如何优化?

  • 问题本质: 订单表数据量巨大,可能导致查询缓慢、存储压力大等问题。

  • 优化目标: 提高查询效率,降低存储成本,提升系统整体性能。

  • 优化策略:

    1. 数据库层面优化:

      • 分库分表 (Sharding):
        • 垂直分库 (Vertical Sharding): 将不同的业务数据拆分到不同的数据库中。 例如,将订单表和用户表分别放在不同的数据库中。 适用场景: 业务耦合度低,可以根据业务模块进行拆分。
        • 垂直分表 (Vertical Partitioning): 将一个表中的不同字段拆分到不同的表中。 例如,将订单表中的常用字段 (订单号、用户ID、订单金额) 和不常用字段 (订单备注、物流信息) 分别放在不同的表中。 适用场景: 某些字段访问频率低,可以减少 I/O 操作。
        • 水平分表 (Horizontal Partitioning): 将一个表中的数据按照一定的规则拆分到多个表中。 例如,按照订单创建时间将订单表拆分到多个表中 (order_202301, order_202302, ...)。 适用场景: 表数据量巨大,查询性能下降。
        • 分片规则:
          • 范围分片: 按照某个范围进行分片,例如按照订单创建时间 (适用于时间序列数据)。
          • 哈希分片: 使用哈希函数将数据分配到不同的分片中,例如按照用户 ID 进行哈希分片。shard_key.hashCode() % num_shards
          • 列表分片: 根据某个列表的值进行分片,例如根据省份进行分片。
        • 选择: 结合实际业务场景选择合适的分片策略。
      • 索引优化:
        • 添加索引: 对常用的查询字段添加索引,例如 user_id, order_time, order_status
        • 复合索引: 针对多个查询条件的组合,创建复合索引。 例如,INDEX(user_id, order_time)
        • 索引类型: 选择合适的索引类型,例如 B-tree 索引、Hash 索引、全文索引。
        • 避免过度索引: 过多的索引会增加写操作的开销,并且占用额外的存储空间。
      • SQL 优化:
        • 避免 SELECT *: 只查询需要的字段。
        • 使用 WHERE 子句限制查询范围: 避免全表扫描。
        • 使用 JOIN 连接表时,注意连接顺序: 将小表放在前面,大表放在后面。
        • 优化 GROUP BYORDER BY 子句: 尽量使用索引。
        • 避免在 WHERE 子句中使用函数或表达式: 这会导致索引失效。
        • 使用 EXPLAIN 分析 SQL 语句的执行计划: 找出性能瓶颈。
      • 读写分离:
        • 原理: 将数据库分为主库 (Master) 和从库 (Slave)。 主库负责写操作,从库负责读操作。
        • 优点: 可以提高读操作的并发能力,减轻主库的压力。
        • 缺点: 可能存在数据延迟问题。
      • 缓存:
        • 使用缓存: 将常用的查询结果缓存起来,例如使用 Redis 或 Memcached。
        • 缓存失效策略: 选择合适的缓存失效策略,例如 LRU (Least Recently Used)、LFU (Least Frequently Used)。
    2. 应用层面优化:

      • 批量操作: 将多个数据库操作合并成一个批量操作,减少数据库交互次数。 例如,批量插入订单数据。
      • 异步处理: 将一些非核心业务逻辑异步处理,例如发送短信通知、更新统计数据。 可以使用消息队列 (例如 Kafka, RabbitMQ) 实现。
      • 代码优化: 优化代码逻辑,减少不必要的计算和 I/O 操作.
      • 分页查询: 对于大量数据的查询,使用分页查询,避免一次性加载所有数据。
    3. 硬件层面优化:

      • 升级硬件: 增加内存、CPU、磁盘 I/O 性能。
      • 使用 SSD: 使用固态硬盘 (SSD) 替代机械硬盘 (HDD),提高 I/O 性能。
    4. 数据归档:

      • 定期归档历史数据: 将不再频繁访问的历史订单数据归档到其他存储介质中,例如归档到 Hadoop 集群或对象存储服务 (例如 Amazon S3, Azure Blob Storage)。
      • 数据压缩: 对归档数据进行压缩,减少存储空间占用。
  • 具体步骤 (示例):

    1. 评估当前系统性能: 使用监控工具 (例如 Prometheus, Grafana) 收集数据库的性能指标,例如 CPU 使用率、内存使用率、磁盘 I/O 延迟、查询响应时间。
    2. 分析性能瓶颈: 根据性能指标,找出性能瓶颈。 例如,查询响应时间过长,可能是由于没有合适的索引或 SQL 语句没有优化。
    3. 选择合适的优化策略: 根据性能瓶颈,选择合适的优化策略。 例如,如果查询响应时间过长,可以考虑添加索引或优化 SQL 语句。 如果数据库压力过大,可以考虑分库分表或读写分离。
    4. 实施优化: 按照选择的优化策略,实施优化。
    5. 测试和验证: 在测试环境中测试优化效果,验证优化是否达到预期目标。
    6. 部署上线: 将优化后的系统部署上线。
    7. 监控和维护: 持续监控系统性能,及时发现和解决问题。
  • 注意事项:

    • 充分了解业务场景: 在进行优化之前,充分了解业务场景,例如订单量、查询频率、数据增长速度。
    • 循序渐进: 不要一次性进行大量的优化,应该循序渐进,逐步优化。
    • 备份数据: 在进行任何优化之前,务必备份数据,以防止数据丢失。
    • 监控和报警: 建立完善的监控和报警机制,及时发现和解决问题。
posted @ 2025-07-09 16:55  贺艳峰  阅读(17)  评论(0)    收藏  举报