京东物流面试汇总(四)
1. 为什么有了 int 还需要 Integer?
答:
int是 Java 的基本数据类型,直接存储数值,效率高。Integer是int的包装类(对象),提供了以下优势:- 面向对象: 可以参与面向对象编程,例如放入集合中。
- null 值:
Integer可以为null,表示数值缺失,而int不行。 - 额外方法:
Integer类提供了很多实用方法,例如parseInt()、compareTo()等。 - 泛型支持: 泛型集合只能存储对象,不能直接存储基本类型。
2. 多线程编程:如何做死锁的检查和预防?如何避免死锁?
答:
死锁检查:
- 代码审查: 仔细检查代码,特别是涉及多个锁的部分。
- 工具:
- jstack: 用于生成 Java 线程转储,可以分析线程状态和锁持有情况。
- VisualVM、JConsole: 图形化监控工具,可以检测死锁。
- 日志: 记录锁的获取和释放,便于事后分析。
死锁预防/避免:
- 破坏互斥条件: 尽量避免使用锁,例如使用无锁数据结构(ConcurrentHashMap)。
- 破坏请求与保持条件: 一次性申请所有需要的资源(锁)。 如果资源无法同时获取,则释放已获取的资源。
- 破坏不剥夺条件: 允许操作系统剥夺进程持有的资源(不常用)。
- 破坏循环等待条件: 对资源进行排序,按照固定的顺序申请资源。 这是一种常用的方法。
- 使用超时机制:
tryLock(timeout)尝试在指定时间内获取锁,如果超时则放弃。 - 避免嵌套锁: 尽量不要在一个锁的范围内获取另一个锁。
- 降低锁的粒度: 将大锁分解为多个小锁,减少锁竞争。
- 使用并发工具类: 例如
ReentrantLock、Semaphore、CountDownLatch等,它们提供了更灵活的锁机制。
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. 建表过程中的索引规范
答:
- 索引列选择:
- 仅为
WHERE、ORDER BY、JOIN中使用的列创建索引。考虑查询频率和重要性。
- 仅为
- 索引类型:
- B-Tree: 默认索引类型,适用于范围查询、排序等。适用性最广。
- Hash: 适用于等值查询,速度快,但不支持范围查询,不常用。
- Fulltext: 用于全文搜索,适用于文本字段。
- 空间索引 (GIS): 用于地理位置数据,例如查找附近的地点。
- 组合索引:
- 多个列经常一起出现在查询条件中时,创建组合索引比多个单列索引更有效。
- 最左前缀原则: 查询必须从组合索引的最左边的列开始,否则无法使用索引。
- 索引顺序: 将选择性最高的列(区分度最高的列)放在组合索引的最左边。
- 索引数量:
- 每个表上的索引数量应该控制在 5 个以内。索引会占用存储空间,并且会降低
INSERT、UPDATE、DELETE的性能。 - 避免创建过多冗余索引。
- 每个表上的索引数量应该控制在 5 个以内。索引会占用存储空间,并且会降低
- 索引维护:
- 定期分析和优化: 数据库系统会自动收集索引的使用情况,并提供优化建议。
- 删除不使用的索引: 及时删除不再使用的索引,可以减少存储空间和提高写入性能。
- 重建索引: 对于频繁修改的表,索引可能会变得碎片化,定期重建索引可以提高查询性能。
- 避免在 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 语句的执行计划,可以帮助你判断是否使用了索引,以及索引的使用效率,从而进行索引优化。 关注type、key、rows等字段。
- 考虑业务场景:
- 根据实际业务场景选择合适的索引策略。
- 例如,对于读多写少的场景,可以适当增加索引,以提高查询性能。 对于写多读少的场景,应该尽量减少索引,以提高写入性能。
- 监控索引使用情况:
- 监控索引的使用情况,及时发现和解决索引相关的问题。
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 分钟:
- 点燃香 A 的两端。
- 同时,点燃香 B 的一端。
- 当香 A 烧完时(15 分钟),立即点燃香 B 的另一端。 此时香 B 已经燃烧了 15 分钟,剩余的长度为香 B 的一半。
- 香 B 现在两端同时燃烧,剩余的长度会在 7.5 分钟内烧完。
8. Redis 数据结构和适用场景
答:
- String(字符串):
- 内部编码:
int、embstr、raw - 适用场景:
- 缓存: 缓存热点数据,例如用户信息、配置信息等。
- 计数器: 例如文章阅读数、点赞数等。 可以使用
INCR、DECR等命令实现原子计数。 - 分布式锁: 使用
SETNX命令实现分布式锁。 - Session 共享: 存储 Session 信息。
- 存储对象: 将对象序列化成 JSON 字符串存储。
- 内部编码:
- List(列表):
- 内部编码:
ziplist、linkedlist - 适用场景:
- 消息队列: 使用
LPUSH和RPOP命令实现简单的消息队列。 - 最新列表: 例如朋友圈的时间线、新闻的最新列表等。 可以使用
LPUSH命令将最新的元素添加到列表头部,然后使用LTRIM命令限制列表的长度。 - 栈/队列: 可以使用
LPUSH和LPOP命令实现栈,使用LPUSH和RPOP命令实现队列。
- 消息队列: 使用
- 内部编码:
- Set(集合):
- 内部编码:
intset、hashtable - 适用场景:
- 标签: 例如给用户添加兴趣标签。
- 社交关系: 存储用户的粉丝、关注等信息。 可以使用
SADD、SREM等命令添加和删除元素。 - 共同好友: 可以使用
SINTER命令计算多个集合的交集,例如计算多个用户的共同好友。 - UV(Unique Visitor)统计: 可以使用
SADD命令将每个访问用户的 ID 添加到集合中,然后使用SCARD命令获取集合的大小,即 UV 值。
- 内部编码:
- Sorted Set(有序集合):
- 内部编码:
ziplist、skiplist - 适用场景:
- 排行榜: 例如游戏积分排行榜、销售额排行榜等。 可以使用
ZADD命令添加元素,并设置分数,然后使用ZRANGE命令按照分数排序获取元素。 - 优先级队列: 按照优先级处理任务。
- 带权重的消息队列: 按照权重发送消息。
- 排行榜: 例如游戏积分排行榜、销售额排行榜等。 可以使用
- 内部编码:
- Hash(哈希表):
- 内部编码:
ziplist、hashtable - 适用场景:
- 对象存储: 存储对象的字段和值。
- 购物车: 存储用户的购物车信息。 可以使用
HSET命令设置字段的值,使用HGET命令获取字段的值。 - 存储用户资料: 相比 String,更节省空间,也更直观。
- 内部编码:
- Bitmap(位图):
- 适用场景:
- 用户签到: 记录用户的签到状态,可以使用
SETBIT命令设置某一位的值,表示用户是否签到。 - 活跃用户统计: 统计用户的活跃度,可以使用
BITCOUNT命令统计位图中值为 1 的位的个数。 - 统计用户在线状态: 记录用户是否在线。
- 用户签到: 记录用户的签到状态,可以使用
- 适用场景:
- HyperLogLog:
- 适用场景:
- UV 统计(近似值): 海量数据的 UV 统计,允许一定的误差。 适用于对统计精度要求不高的场景。
- 适用场景:
- Geospatial:
- 适用场景:
- 附近的人: 查找附近的人。
- 地理围栏: 判断用户是否进入某个区域。
- 地图应用: 存储地理位置信息。
- 适用场景:
9. Linux awk 和 grep 命令是什么?如何用正则表达式匹配 AxxxxAxxx?
答:
grep命令:- 作用: 用于在文件中查找匹配指定模式的行,并将结果输出到标准输出。
- 常用选项:
-i:忽略大小写。-v:反向查找,只输出不匹配的行。-n:显示行号。-r:递归查找,在目录中查找所有文件。-E:使用扩展正则表达式。
awk命令:- 作用: 用于处理文本文件,可以按照指定的规则对文件进行分割、过滤、计算和格式化输出。
- 基本语法:
awk 'pattern {action}' filepattern:用于匹配行的正则表达式。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 中的一个特殊的存储区域,用于存储字符串字面量。 它的目的是为了避免重复创建相同的字符串对象,节省内存空间。
- 共享性: 多个字符串字面量如果值相同,则指向字符串常量池中的同一个对象,实现了共享。
- 内存分配: 首先在字符串常量池中查找是否存在字符串 "a",如果存在,则将
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()创建字符串对象。 - 如果需要拼接字符串,可以使用
StringBuilder或StringBuffer类。
- 避免使用
11. 用 Redis 做二级缓存的时候,如何保证高并发的数据一致性?
答:
在高并发场景下,保证 Redis 作为二级缓存的数据一致性是一个挑战。 常用的策略和方案如下:
-
Cache-Aside Pattern(最常用,推荐):
- 读取数据流程:
- 应用程序首先尝试从 Redis 缓存中读取数据。
- 缓存命中(Cache Hit): 如果 Redis 中存在所需数据,则直接返回。
- 缓存未命中(Cache Miss): 如果 Redis 中不存在所需数据,则应用程序从数据库中读取数据。
- 将从数据库读取的数据写入 Redis 缓存,并设置合理的过期时间。
- 更新数据流程:
- 应用程序首先更新数据库中的数据。
- 删除 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. 如果订单表特别大,如何处理这个表格,如何优化?
-
问题本质: 订单表数据量巨大,可能导致查询缓慢、存储压力大等问题。
-
优化目标: 提高查询效率,降低存储成本,提升系统整体性能。
-
优化策略:
-
数据库层面优化:
- 分库分表 (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 BY和ORDER BY子句: 尽量使用索引。 - 避免在
WHERE子句中使用函数或表达式: 这会导致索引失效。 - 使用
EXPLAIN分析 SQL 语句的执行计划: 找出性能瓶颈。
- 避免
- 读写分离:
- 原理: 将数据库分为主库 (Master) 和从库 (Slave)。 主库负责写操作,从库负责读操作。
- 优点: 可以提高读操作的并发能力,减轻主库的压力。
- 缺点: 可能存在数据延迟问题。
- 缓存:
- 使用缓存: 将常用的查询结果缓存起来,例如使用 Redis 或 Memcached。
- 缓存失效策略: 选择合适的缓存失效策略,例如 LRU (Least Recently Used)、LFU (Least Frequently Used)。
- 分库分表 (Sharding):
-
应用层面优化:
- 批量操作: 将多个数据库操作合并成一个批量操作,减少数据库交互次数。 例如,批量插入订单数据。
- 异步处理: 将一些非核心业务逻辑异步处理,例如发送短信通知、更新统计数据。 可以使用消息队列 (例如 Kafka, RabbitMQ) 实现。
- 代码优化: 优化代码逻辑,减少不必要的计算和 I/O 操作.
- 分页查询: 对于大量数据的查询,使用分页查询,避免一次性加载所有数据。
-
硬件层面优化:
- 升级硬件: 增加内存、CPU、磁盘 I/O 性能。
- 使用 SSD: 使用固态硬盘 (SSD) 替代机械硬盘 (HDD),提高 I/O 性能。
-
数据归档:
- 定期归档历史数据: 将不再频繁访问的历史订单数据归档到其他存储介质中,例如归档到 Hadoop 集群或对象存储服务 (例如 Amazon S3, Azure Blob Storage)。
- 数据压缩: 对归档数据进行压缩,减少存储空间占用。
-
-
具体步骤 (示例):
- 评估当前系统性能: 使用监控工具 (例如 Prometheus, Grafana) 收集数据库的性能指标,例如 CPU 使用率、内存使用率、磁盘 I/O 延迟、查询响应时间。
- 分析性能瓶颈: 根据性能指标,找出性能瓶颈。 例如,查询响应时间过长,可能是由于没有合适的索引或 SQL 语句没有优化。
- 选择合适的优化策略: 根据性能瓶颈,选择合适的优化策略。 例如,如果查询响应时间过长,可以考虑添加索引或优化 SQL 语句。 如果数据库压力过大,可以考虑分库分表或读写分离。
- 实施优化: 按照选择的优化策略,实施优化。
- 测试和验证: 在测试环境中测试优化效果,验证优化是否达到预期目标。
- 部署上线: 将优化后的系统部署上线。
- 监控和维护: 持续监控系统性能,及时发现和解决问题。
-
注意事项:
- 充分了解业务场景: 在进行优化之前,充分了解业务场景,例如订单量、查询频率、数据增长速度。
- 循序渐进: 不要一次性进行大量的优化,应该循序渐进,逐步优化。
- 备份数据: 在进行任何优化之前,务必备份数据,以防止数据丢失。
- 监控和报警: 建立完善的监控和报警机制,及时发现和解决问题。

浙公网安备 33010602011771号