戴老师的博客 - 面试
type: channel
link: https://tushu.info/tag/%E9%9D%A2%E8%AF%95/
items: Java并发之synchronized关键字深度解析
items: 双亲委派
items: MVCC
items: 面试时，如何向公司提问？
items: 订单表的分库分表方案设计(大数据.html)
items: 面试问题总结02
items: 面试问题总结03
items: Java锁总结

Java并发之synchronized关键字深度解析
type: item
date: 2026-07-07T08:37:09+00:00
description: 前言本文承接【Java 并发之 synchronized 关键字深度解析（一）】，着重介绍 synchronized 几种锁的特性及其底层实现原理。一、对象头结构及锁状态标识synchronized 关键字是如何实现给对象加锁的？首先我们需要了解 Java 中对象的组成。Java 对象在内存中主要由三部分组成：对象头（Object Header）、实例数据（Instance Data）和对齐填充（Padding）。对齐填充：JVM 规定对象的起始内存地址必须是 8 字节的整数倍，如果不够则用占位符填充，此部分即为对齐填充。实例数据：对象存储的真正有效信息，即对象的成员变量信息（包括继承自父类的）。对象头：由两部分组成。第一部分是对象的运行时数据（Mark Word），包括哈希码、锁偏向标识、锁类型、GC 分代年龄、偏向线程 ID 等；第二部分是对象的类型指针（Klass Word），用于在堆中定位对象的实例数据和方法区中的类型数据。Java 对象的公共特性都存放在对象头中。对象头存储内容如下所示（以 64 位操作系统为例）：|--------------------------------------------------------------------------------------------------------------| | Object Header (128 bits.html) | |--------------------------------------------------------------------------------------------------------------| | Mark Word (64 bits.html) | Klass Word (64 bits.html) | |--------------------------------------------------------------------------------------------------------------| | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 无锁 |----------------------------------------------------------------------|--------|------------------------------| | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 偏向锁 |----------------------------------------------------------------------|--------|------------------------------| | ptr_to_lock_record:62 | lock:2 | OOP to metadata object | 轻量锁 |----------------------------------------------------------------------|--------|------------------------------| | ptr_to_heavyweight_monitor:62 | lock:2 | OOP to metadata object | 重量锁 |----------------------------------------------------------------------|--------|------------------------------| | | lock:2 | OOP to metadata object | GC |--------------------------------------------------------------------------------------------------------------|其中 lock:2 表示有 2 bit 控制锁类型，biased_lock:1 表示 1 bit 控制偏向锁状态，对应关系如下所示：01：无锁（前面偏向锁状态为 0 时表示未锁定）01：可偏向（前面偏向锁状态为 1 时表示可偏向）00：轻量级锁10：重量级锁11：GC 标记看到前两种状态时读者可能会有些疑惑…

双亲委派
type: item
date: 2026-07-07T08:36:44+00:00
description: 背景知识在介绍双亲委派机制之前，首先需要了解 ClassLoader（类加载器） 的相关概念。Java 程序运行在 Java 虚拟机（JVM）之中。我们在 IDE 中编写的 Java 源代码被编译器编译成 .class 字节码文件，随后由 ClassLoader 负责将这些 class 文件加载到 JVM 中执行。JVM 中主要提供了三层 ClassLoader：Bootstrap ClassLoader：主要负责加载核心类库（如 java.lang.* 等），并负责构造 ExtClassLoader 和 AppClassLoader。ExtClassLoader：主要负责加载 jre/lib/ext 目录下的一些扩展 jar 包。AppClassLoader：主要负责加载应用程序的主函数类及classpath 下的类。那么，一个由 Hello.java 编译成的 Hello.class 文件，究竟是如何被加载到 JVM 中的呢？双亲委派机制详解查看 JDK 源码中 java.lang 包下的 ClassLoader 类，重点关注其 loadClass 方法，核心逻辑如下：public Class<?> loadClass(String name.html) throws ClassNotFoundException { return loadClass(name, false.html); } // ----- 核心逻辑 ----- protected Class<?> loadClass(String name, boolean resolve.html) throws ClassNotFoundException { // 首先，检查是否已经被类加载器加载过 Class<?> c = findLoadedClass(name.html); if (c == null.html) { try { // 存在父加载器，递归地交由父加载器处理 if (parent != null.html) { c = parent.loadClass(name, false.html); } else { // 直到最上面的 Bootstrap 类加载器 c = findBootstrapClassOrNull(name.html); } } catch (ClassNotFoundException e.html) { // 如果父加载器无法找到该类，则捕获异常 // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null.html) { // 如果仍未找到，则调用 findClass 方法尝试自己加载 c = findClass(name.html); } } return c; }这段代码清晰地解释了双亲委派机制。为了更直观地理解，我们可以通过下图来描述上述代码的执行流程：结合上图与源码，当一个类（如 Hello.class）需要被加载时，流程如下：检查是否已加载：首先会在当前的 ClassLoader（例如 AppClassLoader）中检查该类是否已经被加载过，如果有则无需再次加载。委托父加载器：如果没有加载过，则获取父加载器，调用父加载器的 loadClass 方法。递归向上：父加载器同理，先检查自己是否加载过，如果没有则继续向上委托。这是一个类似递归的过程，直到到达 Bootstrap ClassLoader。向下查找：在到达 Bootstrap ClassLoader 之前，各级加载器都只是检查是否加载过，并不会尝试自己去加载。直到 Bootstrap ClassLoader（它没有父加载器），如果它无法加载，则会下沉到子加载器去尝试加载（调用 findClass），一直到最底层。抛出异常：如果没有任何加载器能加载该类，最终将抛出 ClassNotFoundException。机制设计的意义双亲委派机制的设计主要有以下好处：防止核心类库被篡改。例如，如果有人试图替换系统级别的类（如 String.java）并篡改其实现，在这种机制下，由于系统类已经被 Bootstrap ClassLoader 加载过了（当一个类需要加载时，最先尝试加载的就是 Bootstrap ClassLoader），其他类加载器并没有机会再去加载这些类。这在一定程度上防止了危险代码的植入，保证了 Java 核心 API 的安全性。参考文章https://tushu.info/archives/java-de-classloader-yu-package-ji-zhi.html> 说明：本文基于 J…

MVCC
type: item
date: 2026-07-07T08:35:51+00:00
description: 前提概要什么是 MVCC什么是当前读和快照读当前读、快照读和 MVCC 的关系MVCC 实现原理隐式字段Undo 日志Read View（读视图）整体流程MVCC 相关问题RR 是如何在 RC 级的基础上解决不可重复读的RC、RR 级别下的 InnoDB 快照读有什么不同前提概要什么是 MVCC？MVCC（Multi-Version Concurrency Control，多版本并发控制）是一种并发控制的方法，一般在数据库管理系统中实现对数据库的并发访问，或在编程语言中实现事务内存。MVCC 在 MySQL InnoDB 中的实现主要是为了提高数据库并发性能，用更好的方式去处理读 - 写冲突。其核心目标是做到即使存在读写冲突时，也能实现不加锁、非阻塞的并发读。什么是当前读和快照读？在学习 MVCC 多版本并发控制之前，必须先了解 MySQL InnoDB 下的当前读和快照读。当前读（Current Read）像 SELECT ... LOCK IN SHARE MODE（共享锁）、SELECT ... FOR UPDATE、UPDATE、INSERT、DELETE（排他锁）这些操作都属于当前读。特点：读取的是记录的最新版本。机制：读取时还要保证其他并发事务不能修改当前记录，因此会对读取的记录进行加锁。快照读（Snapshot Read）像不加锁的 SELECT 操作就是快照读，即不加锁的非阻塞读。前提：隔离级别不是串行级别（Serializable）。串行级别下的快照读会退化成当前读。目的：基于提高并发性能的考虑。实现：基于多版本并发控制（MVCC）。可以认为 MVCC 是行锁的一个变种，它在很多情况下避免了加锁操作，降低了开销。版本：既然是基于多版本，快照读可能读到的并不一定是数据的最新版本，而有可能是之前的历史版本。简而言之，MVCC 就是为了实现读 - 写冲突不加锁，而这个“读”指的就是快照读，而非当前读。当前读实际上是一种加锁的操作，是悲观锁的实现。当前读、快照读和 MVCC 的关系概念层面：准确地说，MVCC 多版本并发控制指的是“维持一个数据的多个版本，使得读写操作没有冲突”这么一个概念，仅仅是一个理想模型。实现层面：在 MySQL 中，实现这么一个 MVCC 理想模型，需要具体的功能支持。快照读就是 MySQL 为我们实现 MVCC 理想模型的其中一个具体非阻塞读功能。而相对而言，当前读就是悲观锁的具体功能实现。底层细节：快照读本身也是一个抽象概念。MVCC 模型在 MySQL 中的具体实现则是由 3 个隐式字段、Undo 日志、Read View 等去完成的（具体原理见下文）。MVCC 能解决什么问题，好处是？数据库并发场景主要有三种：读 - 读：不存在任何问题，也不需要并发控制。读 - 写：有线程安全问题，可能会造成事务隔离性问题，可能遇到脏读、幻读、不可重复读。写 - 写：有线程安全问题，可能会存在更新丢失问题，比如第一类更新丢失、第二类更新丢失。MVCC 带来的好处多版本并发控制（MVCC）是一种用来解决读 - 写冲突的无锁并发控制。它为事务分配单向增长的时间戳，为每个修改保存一个版本，版本与事务时间戳关联，读操作只读该事务开始前的数据库快照。所以 MVCC 可以为数据库解决以下问题：在并发读写数据库时，可以做到在读操作时不用阻塞写操作，写操作也不用阻塞读操作，提高了数据库并发读写的性能。同时还可以解决脏读、幻读、不可重复读等事务隔离问题（但不能解决更新丢失问题）。小结总之，MVCC 是因为设计者不满意只让数据库采用悲观锁这样性能不佳的形式去解决读 - 写冲突问题，而提出的解决方案。在数据库中，因为有了 MVCC，我们可以形成两个组合：MVCC + 悲观锁：MVCC 解决读写冲突，悲观锁解决写写冲突。MVCC + 乐观锁：MVCC 解决读写冲突，乐观锁解决写写冲突。这种组合的方式就可以最大程度地提高数据库并发性能，并解决读写冲突和写写冲突导致的问题。MVCC 实现原理MVCC 的目的就是多版本并发控制，在数据库中的实现，就是为了解决读写冲突。它的实现原理主要是依赖记录中的 3 个隐式字段、Undo 日志、Read View 来实现的。隐式字段每行记录除了我们自定义的字段外，还有数据库隐式定义的 DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID 等字段。DB_TRX_ID：6 byte，最近修改（修改/插入）事务 ID。记录创建这条记录或最后一次修改该记录的事务 ID。DB_ROLL_PTR：7 byte，回滚指针。指向这条记录的上一个版本（存储于 rollback segment 里）。DB_ROW_ID：6 byte，隐含的自增 ID（隐藏主键）。如果数据表没有主键，InnoDB 会自动以…

面试时，如何向公司提问？
type: item
date: 2026-07-07T08:35:08+00:00
description: 前言近日，笔者阅读了硅谷招聘经理 Steve Buckley 的一篇文章，该文从应聘者的视角探讨了面试中的同一话题：如何向公司提问。许多求职者往往将面试视为一种单向选择，事实上，面试是一种双向选择：不仅是公司挑选你，也是你挑选公司。面试旨在为双方提供互相了解的机会——公司在评估你，你也在评估公司。面试官通常对此心知肚明，所以他们有心理准备期待你提出问题，并会做出回答。因此，面试时切勿浪费向公司提问的机会。主动提问不仅表明你比较重视这个职位，还能加深面试官对你的印象，可能会提高面试的成功率。面试提问的注意事项在提问环节，以下几点注意事项值得参考：事前准备：面试之前，一定要做准备，多了解公司的情况。围绕核心：你提出的问题，应该围绕“这份工作是否合适我”这个中心点，其他与应聘关系不大的问题，不宜多问。自然放松：提问的时候，要自然放松，不要害羞，就把它当作普通的聊天。你要表现出对公司的真诚兴趣。直截了当：提问要直截了当，不要绕圈子。提出问题之后，你要保持安静，让面试官多说话。专注倾听：面试官回答的时候，你可以做笔记，或者事先询问能不能做。笔记必须简短，你的大部分时间，要用来全神贯注倾听面试官的回答，并与其有眼神的交流。后续跟进：面试结束后一周内，最好打一个电话或发一封邮件，了解公司对你的反馈意见。即使面试失败，你不妨也问一下原因，这会有助于你以后的面试。典型问题示例下面是一些你可以问的典型问题，供参考。问题一：你们为什么要招聘这个职位？Q1: Why are you currently recruiting for this position?这个问题会使得面试官开始谈论当前的项目，或者谈论前一位离职人员。无论哪种情况，都会让你了解一些与你最密切相关的公司情况。问题二：你们的新员工多吗？Q2: Do you have many new staffs?这个问题起一个过渡作用，使得谈话导向公司内部的情况。但是，它本身也能说明一些问题。如果公司成立已经超过四年，又没有新项目，但是新员工却很多，这往往说明公司文化不是很健康。问题三：你们公司（团队）目前面临的最大挑战是什么？Q3: What are the biggest challenges your team are facing right now?如果面试官开始谈论一些具体的技术问题，这很好；如果他的回答是项目时间紧迫，或者需要更多的资金，那你就要小心一点了，公司管理上面可能有问题。问题四：什么新技术（编程语言）是你们未来希望采用的？Q4: What technologies/languages would you like to see your team adapt to that aren't currently being utilised?如果你申请的是技术职位，面试官恰巧又是技术负责人，那么这个问题将会非常合适。你会对公司的技术路线有所了解和准备，一旦入职，就能更好地适应公司的需要。问题五：在业务方面，有没有什么地方是你们不满意的，未来想要改进的？Q5: Few companies, if any, are 100% satisfied with the way their business is operating. If you could simply flick a switch to fix it, what one thing would you change?很少有公司会百分之百满意自身的现状，即使那些状况很良好的公司也是如此。这个问题可以让你对公司管理层的关注重点和担忧之处，有所了解。问题六：我申请的这个职位，对公司的业务有何影响？Q6: If you struggle to fill the position I have applied for, what impact would that have on the business?这个问题会让你了解自己在公司的角色，以及你的岗位对公司是否重要。说明本文引用的原始文章及图片资源发布于 2011-2012 年间。虽然面试的具体形式和技术栈可能随时代演变，但文中关于双向选择、提问策略及沟通心态的核心建议依然具有参考价值。

订单表的分库分表方案设计(大数据.html)
type: item
date: 2026-07-07T08:35:05+00:00
description: 订单表的分库分表方案设计在业界的大数据场景下，订单表的分库分表（Sharding）设计通常主要有两类思路：按照订单号切分或按照用户 ID 切分。以下是对这两种方案的详细分析、优缺点对比以及常见问题的解决方案。主流分库分表方案方案一：按照订单号 Hash 分散该方案将订单号视为字符串，通过 Hash 算法将数据分散到多个数据库服务器中。路由规则：具体存储到哪个库、哪张表，通常由订单号中的数字特征决定。典型案例：例如微信红包的订单分库分表，是对订单号进行 Hash 计算（非简单的取模或取整），以实现数据的均匀分散。订单号末尾的 3 位数字往往包含了库名和表名的信息。查询挑战：如果需要查询某用户的所有订单，由于数据是根据订单号分散的，该用户的订单可能分布在多个库、多张表中。若全库全表扫描，效率极低。对比：即便是按照用户 ID（UID）切分，查询特定订单号时也会面临类似问题（不知道数据在哪个库）。方案二：按照用户 ID 分散以用户 ID（UID）作为切分键，业界使用较为普遍。主要有两种思路：范围切分：例如 0-2000 万 UID 对应库 A，2000 万 -4000 万 UID 对应库 B。缺点：容易出现数据热点。若某个范围内的用户下单量特别大，会导致对应库压力过高，而其他库闲置。取模运算（推荐）：使用 UID % N 的方式。优点：处理简单，数据能均匀分散到多个库，不易出现单库性能瓶颈。缺点：扩容麻烦。扩容通常需要进行数据迁移。扩容策略：为了减少迁移量，一般以倍数形式增加库数量（例如从 8 库扩容到 16 库，再到 32 库）。这种方式虽不能彻底避免迁移，但能显著减少迁移数据量，且一次扩容可支撑较长时间的业务增长。基于用户 ID 分片的详细设计以下以“按照用户 ID 取模”为例，分析具体的库表定位规则。1. 路由算法逻辑假设使用用户 ID 末尾 4 位数字进行计算：库名称定位：用户 ID 末尾 4 位 % 32Mod 表示取余数。例如 15 % 4 = 3。余数范围 0-31，对应 32 个数据库实例。库名示例：order_db_0 至 order_db_31。表名称定位：(用户 ID 末尾 4 位 / 32.html) % 32Dev 表示取整数商（向下取整）。例如 15 / 4 = 3.75，取整为 3。代码表示：floor(15/4.html)。表名示例：order_tb_0 至 order_tb_31。每个库内包含 32 张表。2. 容量估算按照上述规则：32 个库 * 32 张表 = 1024 张表。若单表数据量控制在 1000 万至 5000 万行，理论上可支撑海量数据。若希望表数量更少，可调整取模的基数（如将 32 改小）。查询场景与挑战方案一的查询问题若按照订单号分散数据，查询某用户的所有订单时，必须进行跨库、跨表查询，效率低下，通常不可行。方案二的查询问题若按照用户 ID 分散数据，虽然查询用户订单列表很方便，但在实际应用中会面临以下高频查询需求：按订单号查询：前台或后台输入订单号查询详情（Select）。按订单号更新：修改订单状态（Update）。由于数据是按用户 ID 分散的，仅凭订单号无法直接定位到具体的库和表。若全库扫描，性能无法接受。解决方案：建立索引映射表没有一种技术方案是十全十美的。针对“按用户 ID 分片后无法通过订单号查询”的问题，常见的解决思路是建立订单号与用户 ID 的索引关系表。1. 核心思路映射表设计：建立一张 order_user_idx 表，存储两项核心数据：订单号 、 用户 ID。查询流程：输入订单号，查询索引表获取用户 ID。根据用户 ID 利用路由算法定位到具体的库和表。查询订单详情。写入流程：创建订单时，需同步插入关系记录。为降低用户等待时间，该操作可异步化（如加入消息队列处理）。2. 索引表的性能优化随着时间推移，一个用户的订单量可能达到几百甚至更多，索引表本身也可能成为瓶颈。索引表分片：order_user_idx 表本身也需要分库分表。建议直接根据订单号取模进行分片。缓存策略：订单与用户的绑定关系创建后通常不会变更。可将该关系表放入内存缓存（如 Redis）。查询时优先走缓存，未命中再查数据库，以提升速度。复杂业务场景思考思考一：B2B 平台的分片键选择在 B2C 平台，卖家通常是平台自身。但在 B2B 平台，买家和卖家均需查看订单。按买家 ID 分片：卖家查询自己的所有订单时，因商品被多个买家购买，订单分散在不同库表，导致跨库查询性能低下。按卖家 ID 分片：买家在多个店铺下单，订单同样分散，买家查询自身订单时性能低下。行业实践（如淘宝）：采用数据冗余方案，拆分买家库和卖家库。买家库：按买家 ID 分库分表。卖家库：按卖家 ID 分库分表。同步机制：下单时写两份数据。…

面试问题总结02
type: item
date: 2026-07-07T08:33:20+00:00
description: 面试问题总结 021. 请简单介绍一下你在 Netty 方面的项目经验我曾开发了一套基于 Netty 的 Web 容器，Netty 在该项目中发挥了核心作用。具体实践如下：解码与编码机制：利用 Netty 的 ChannelPipeline 机制，我们设计了一系列解码器（Decoder）和编码器（Encoder），实现了对不同数据格式的高效处理。例如，针对二进制数据进行了快速的反序列化和序列化操作，确保了数据在传输过程中的准确性与完整性。数据安全与校验：通过添加自定义过滤器（Filter），实现了对数据的实时校验和封装，显著提升了数据的安全性和可靠性。高并发处理能力：得益于 Netty 高性能的异步非阻塞 I/O（Async Non-blocking I/O）模型，系统能够轻松应对数以万计的并发连接，大幅提升了吞吐量和响应速度。在压力测试中，即使在高并发场景下，系统依然保持稳定，响应时间控制在极低范围内。可扩展性与定制开发：利用 Netty 良好的可扩展性，我们根据项目特定需求进行了定制化开发。例如，开发了专门的业务处理器（Handler）来处理特定类型的业务逻辑，进一步提高了系统的灵活性和可维护性。Netty 的使用不仅解决了项目中的实际技术难题，也为未来的业务扩展奠定了坚实基础。2. 请说说你在使用 Spring Boot 时，遇到过哪些挑战，以及你是如何解决的？在使用 Spring Boot 的过程中，我主要遇到了以下挑战并逐一解决：自动配置原理的理解挑战：初期阶段，Spring Boot 的自动配置（Auto Configuration）和自动注解功能虽然提高了开发效率，但其背后的原理令人困惑。解决：我深入研究了 Spring Boot 源代码，分析其实现机制。发现 Spring Boot 通过巧妙使用条件注解（Conditional Annotations）和配置文件，根据环境和依赖自动调整配置。同时，通过查阅文档和实际项目演练，我逐渐掌握了各种注解的用法，能够熟练运用它们简化开发过程。内嵌容器的替换（Tomcat 转 Netty）挑战：在深入使用阶段，我需要将内嵌的 Tomcat 容器替换为 Netty 容器。这需要深入理解 Spring Boot 的容器启动机制和 Netty 的工作原理，具有一定难度。解决：查阅资料了解到 Spring Boot 通过自动配置类启动内嵌容器。实现了一个自定义的服务器启动类，并将其配置到 Spring Boot 的自动配置体系中。研究 Netty 文档和示例代码，实现基于 Netty 的服务器，并通过实现 Spring Boot 相关接口和配置类将其整合到项目中。在过程中解决了配置参数调整、线程模型优化等问题，最终成功替换容器，满足了项目的特定需求。通过这些挑战的解决，我对 Spring Boot 的理解更加深入，技术水平和解决问题的能力也得到了提升。3. 请谈谈你对微服务架构的理解，以及在实际项目中如何运用 Spring Boot 来实现微服务对微服务架构的理解微服务架构代表了一种现代化的软件架构理念，它将原本庞大复杂的单体应用拆分为多个独立运行、功能单一的小型服务。其核心优势包括：技术选型灵活：每个微服务可根据业务需求选择最适合的技术栈（如 Java、Python 等），充分发挥不同技术的优势。独立部署与扩展：各个微服务可独立部署，更新互不影响；同时可根据负载情况单独扩展，提高资源利用率。易于维护与故障隔离：某个微服务出现问题时，只会影响该服务自身，不会导致整个系统崩溃，便于快速定位和解决问题。基于 Spring Boot 的微服务实现在实际项目中，我们通常结合 Spring Cloud 框架来实现微服务架构：服务注册与发现：使用组件（如 Eureka 或 Consul），微服务启动时将信息注册到注册中心，其他服务可从中获取可用地址。新服务上线时无需更改配置即可被发现和调用。服务通信与负载均衡：服务间通信可采用 RESTful API 或消息队列。利用 Spring Cloud 的 Feign 客户端可方便地进行服务调用并实现负载均衡。容错处理：利用 Hystrix 进行服务熔断和降级，当某个服务故障时快速响应，避免故障扩散。配置管理：使用 Spring Cloud Config 实现集中式配置管理，微服务可从配置中心获取信息，方便更新和维护。监控与管理：利用 Spring Cloud Admin 等功能，实时监控微服务的运行状态和性能指标，便于及时发现和处理问题。通过 Spring Boot 与 Spring Cloud 的结合，能够高效地构建、部署和管理微服务架构，为企业业务发展提供强大的技术支撑。4. 在微服务架构中，如何进行服务的监控和故障排查？在微服务架构中，有效的服…

面试问题总结03
type: item
date: 2026-07-07T08:32:42+00:00
description: 请说一说你在使用 Grafana 和 SkyWalking 进行性能监控和追踪时，有哪些具体的实践经验和收获？Grafana 是一个功能强大的数据可视化工具。在实际项目中，我利用它连接多种数据源（如 MySQL、Redis 等），通过配置自定义仪表盘，能够直观地展示系统关键性能指标。例如，我设置了实时监控页面，展示服务器的 CPU、内存使用率以及数据库的连接数等指标。同时，利用其报警功能，当指标超过预设阈值时，能及时通过邮件和短信通知相关人员，大大提高了问题响应速度。SkyWalking 则在链路跟踪方面表现出色。通过它，我可以清晰地看到微服务架构中各个服务之间的调用关系和性能指标。在一次复杂的项目中，当系统出现性能问题时，借助 SkyWalking 的全局 Trace ID 和 Span ID，我快速定位到了问题服务和具体的代码位置。并且，它对各种中间件的拦截功能非常实用，比如对 Redis 和 HTTP 请求的拦截，让我能够深入了解这些关键组件的性能情况。通过对收集到的数据进行分析，我能够针对性地进行性能优化，极大地提升了系统的稳定性和响应速度。在进行数据库设计时，你通常会遵循哪些原则？请结合实际项目经验说明。在进行数据库设计时，我主要遵循以下几个原则：范式化原则严格遵循数据库范式，尤其是第三范式（3NF），确保数据的独立性和减少数据冗余。在我负责的一个大型企业资源管理系统项目中，将员工信息、部门信息、岗位信息分别存储在不同的表中，通过外键关联，避免了重复存储部门名称、岗位名称等信息，极大地提高了数据的一致性和可维护性。当部门名称或岗位名称发生变化时，只需在对应的表中进行修改，而不会影响到员工信息表。性能与反范式平衡原则虽然范式化可以提高数据的规范性，但在某些对性能要求极高的场景下，适当进行反范式设计。例如，在一个电商平台的订单系统中，订单表中会存储商品的名称和价格等信息，虽然这会造成一定的数据冗余，但在查询订单详情时，可以避免频繁的关联查询商品表，大大提高了查询性能。扩展性原则充分考虑未来业务的发展，预留扩展字段。在一个社交平台项目中，用户信息表中预留了几个自定义字段，随着平台功能的不断扩展，这些字段被用于存储用户的个性化标签、兴趣爱好等信息，无需对表结构进行大规模修改，保证了系统的稳定性。索引优化原则合理添加索引，包括普通索引、唯一索引和覆盖索引等。在一个金融交易系统中，对交易流水表的交易时间、交易金额等频繁用于查询和排序的字段添加索引，极大地提高了查询和统计的效率。同时，通过分析业务场景，创建了一些覆盖索引，使得查询可以直接从索引中获取所需数据，无需回表查询，进一步提升了性能。数据校验规则原则设置严格的数据校验规则，确保存入数据库的数据符合规范。在一个医疗管理系统中，对患者年龄字段设置了 0 到 120 的范围限制，对药品剂量字段设置了合理的数值范围和精度要求，有效避免了错误数据的存入。数据安全性原则高度重视数据安全，通过多种方式进行保护。在一个政府部门的信息管理系统中，对敏感数据进行加密存储，同时创建视图和存储过程，严格控制不同用户对数据的访问权限。例如，普通用户只能通过视图查询部分公开信息，而管理员可以通过存储过程进行数据的修改和删除操作，确保了数据的安全性和保密性。总之，在数据库设计过程中，我始终综合考虑这些原则，结合实际项目需求，进行科学合理的设计，以确保数据库的高效、稳定和安全。在团队协作中，你认为最重要的品质是什么？请举例说明。在团队协作中，我认为最重要的品质是：目标一致、责任心强、善于沟通和懂得协作分工。目标一致是团队协作的基石。在一个软件开发项目中，我们的团队面临着紧张的工期和复杂的需求。但大家都清楚地认识到，我们的共同目标是按时交付高质量的软件产品，为客户创造价值。每个人都围绕这个目标努力，即使在遇到困难和分歧时，也始终以实现目标为导向进行决策和行动。比如在需求讨论中，当不同的意见出现时，大家不是固执己见，而是通过深入分析和讨论，找到最符合项目目标的解决方案。责任心强是团队成功的关键。在团队中，每个人都对自己的工作负责，认真对待每一个任务。有一次，一位同事负责的模块出现了一个难以察觉的小问题，但他没有忽视，而是花费大量时间进行排查和修复，确保整个系统的稳定运行。这种责任心不仅保证了自己负责的部分不出差错，也为其他成员提供了可靠的工作基础。善于沟通能让团队高效运转。在项目进行过程中，我们定期召开团队会议，分享工作进展、遇到的问题和解决方案。同时，在日常工作中，大家也随时进行沟通交流，确保信息畅通。比如在设计阶段，开发人员和设计师密切沟通，确保设计方案既满足用户需求又具有技术可行性。这种及时有效的沟通避免了误解和重复工作，提高了工作效率。懂得协作分工可以充分发挥团队成员的优势。我们根据每个人的技能和经验…

Java锁总结
type: item
date: 2026-07-07T08:30:37+00:00
description: 前言Java 提供了种类丰富的锁机制，每种锁因其特性不同，在适当的场景下能够展现出极高的效率。本文旨在结合锁相关源码（本文源码基于 JDK 8 和 Netty 3.10.6）与使用场景进行举例，为读者介绍主流锁的知识点以及不同锁的适用场景。Java 中通常是按照是否含有某一特性来定义锁。我们通过特性将锁进行分组归类，再使用对比的方式进行介绍，帮助大家更快捷地理解相关知识。下面给出本文内容的总体分类目录：1. 乐观锁 VS 悲观锁乐观锁与悲观锁是一种广义上的概念，体现了看待线程同步的不同角度。在 Java 和数据库中都有此概念对应的实际应用。概念简述：悲观锁（Pessimistic Lock）：对于同一个数据的并发操作，悲观锁认为自己在使用数据的时候一定有别的线程来修改数据，因此在获取数据的时候会先加锁，确保数据不会被别的线程修改。Java 中，synchronized 关键字和 Lock 的实现类都是悲观锁。乐观锁（Optimistic Lock）：认为自己在使用数据时不会有别的线程修改数据，所以不会添加锁，只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新，当前线程将自己修改的数据成功写入；如果数据已经被其他线程更新，则根据不同的实现方式执行不同的操作（例如报错或者自动重试）。乐观锁在 Java 中是通过无锁编程来实现，最常采用的是 CAS（Compare And Swap） 算法，Java 原子类中的递增操作就通过 CAS 自旋实现的。根据上面的概念描述我们可以发现：悲观锁适合写操作多的场景，先加锁可以保证写操作时数据正确。乐观锁适合读操作多的场景，不加锁的特点能够使其读操作的性能大幅提升。光说概念有些抽象，我们来看下乐观锁和悲观锁的调用方式示例：// ------------------------- 悲观锁的调用方式 ------------------------- // synchronized public synchronized void testMethod() { // 操作同步资源 } // ReentrantLock private ReentrantLock lock = new ReentrantLock(); // 需要保证多个线程使用的是同一个锁 public void modifyPublicResources() { lock.lock(); try { // 操作同步资源 } finally { lock.unlock(); } } // ------------------------- 乐观锁的调用方式 ------------------------- private AtomicInteger atomicInteger = new AtomicInteger(); // 需要保证多个线程使用的是同一个 AtomicInteger atomicInteger.incrementAndGet(); // 执行自增 1通过调用方式示例，我们可以发现悲观锁基本都是在显式的锁定之后再操作同步资源，而乐观锁则直接去操作同步资源。那么，为何乐观锁能够做到不锁定同步资源也可以正确地实现线程同步呢？我们通过介绍乐观锁的主要实现方式"CAS"的技术原理来为大家解惑。CAS 原理CAS 全称 Compare And Swap（比较与交换），是一种无锁算法。在不使用锁（没有线程被阻塞）的情况下实现多线程之间的变量同步。java.util.concurrent 包中的原子类就是通过 CAS 来实现了乐观锁。CAS 算法涉及到三个操作数：需要读写的内存值 V。进行比较的值 A。要写入的新值 B。当且仅当 V 的值等于 A 时，CAS 通过原子方式用新值 B 来更新 V 的值（“比较 + 更新”整体是一个原子操作），否则不会执行任何操作。一般情况下，“更新”是一个不断重试的操作。之前提到 java.util.concurrent 包中的原子类，就是通过 CAS 来实现了乐观锁，那么我们进入原子类 AtomicInteger 的源码，看一下 AtomicInteger 的定义：根据定义我们可以看出各属性的作用：unsafe：获取并操作内存的数据。valueOffset：存储 value 在 AtomicInteger 中的偏移量。value：存储 AtomicInteger 的 int 值，该属性需要借助 volatile 关键字保证其在线程间是可见的。接下来，我们查看 AtomicInteger 的自增函数 incrementAndGet() 的源码时，发现自增函数底层调用的是 unsafe.getAndAddInt()。但是由于 JDK 本身只有 Unsafe.class，…

订单表的分库分表方案设计(大数据.html)
type: item
date: 2026-07-07T08:25:58+00:00
description: 一、两种方案分库分表一般业界，对订单数据的分库分表，笔者了解，有两类思路：按照订单号来切分、按照用户id来切分。方案一、按照订单号来做hash分散订单数据 把订单号看作是一个字符串，做hash，分散到多个服务器去。 具体到哪个库、哪个表存储数据呢？订单号里面的数字来记录着。 现在的微信红包。它的订单分库分表，是对订单号进行hash计算。不是什么取模、取整数。这样数据是均匀分散的。 然后订单号的末尾3个数，里面是包含了库名称、表名称的。 如果要查询某用户的所有订单呢？ 由于是根据订单号来分散数据的。他的订单分散在了多个库、多个表中。 总不能去所有的库，所有的表扫描吧。这样效率很低。 其实按照uid切分订单，一样会遇到查询的问题。比如要查询a订单的信息。分库分表的规则是按照uid，都不知道数据在哪个库，无从查。 后续说明解决思路。 一般使用方案二的比较多，一个用户的所有订单，都在一张表里面，那么做分页展示的时候，就容易。 方案二、按照用户id打散订单数据。以uid来切分数据，有两种思路：一种是，某个范围的uid订单到哪些库。0到2千万uid，对应的订单数据到a库、a表。2千万到4千万对应的订单到b库。为什么这种方案用得比较少呢？容易出现瓶颈吗。某个范围内的用户，下单量比较多，那么造成这个库的压力特别大。其他库却没什么压力。第二种是，使用uid取模运算。第二种方案业界用得比较多。一方面、处理简单，程序上做取模运算就好了。另一方面、使用取模的方式，数据比较均匀分散到多个库去了。不容易出现单个库性能瓶颈。但是不好处也有：即要扩容的时候，比较麻烦。就需要迁移数据了。要扩容的时候，为了减少迁移的数据量，一般扩容是以倍数的形式增加。比如原来是8个库，扩容的时候，就要增加到16个库，再次扩容，就增加到32个库。这样迁移的数据量，就小很多了。这个问题不算很大问题，毕竟一次扩容，可以保证比较长的时间，而且使用倍数增加的方式，已经减少了数据迁移量。下面笔者，分析一下按照用户id取模的方式分库分表。按照用户id作为key来切分订单数据，具体如下：1、 库名称定位：用户id末尾4位 Mod 32。Mod表示除以一个数后，取余下的数。比如除以32后，余下8，余数就是8。代码符号是用%表示：15%4=3。2、表名称定位：（用户id末尾4位 Dev 32） Mod 32。Dev表示除以一个数，取结果的整数。比如得到结果是25.6，取整就是25。代码用/来表示：$get_int = floor(15/4.html)。15除以4，是一个小数3.75，向下取整就是3。一定是向下取整，向上取整就变成了4了。按照上面的规则：总共可以表示多少张表呢？32个库*每个库32个表=1024张表。如果想表的数量小，就把32改小一些。上面是用计算机术语来表示， 下面用通俗的话描述。1、库名称计算用户id的后4位数，取32的模(取模就是除以这个数后，余多少.html)。余下的数，是0-31之间。这样可以表示从0-31之间，总共32个数字。用这个32个数字代表着32个库名称：order\_db\_0、order\_db\_2.........................order\_db\_312、表名称计算最后要存储定到哪个表名里面去呢？用户id的最后4位数，除以32，取整数。将整数除以32，得到余数，能够表示从0-31之间32个数字，表示表名称。表名称类似这样：order\_tb\_1、order\_tb\_2..........................order\_tb\_31。一个库里面，总共32个表名称。比如用户id：19408064，用最后4位数字8064除以32，得到是251.9，取它的整数是251。接着将251除以32，取余数，余数为27。为了保持性能，每张表的数据量要控制。单表可以维持在一千万-5千万行的数据。1024*一千万。哇，可以表示很多数据了。三、思考优点和缺点优点订单水平分库分表，为什么要按照用户id来切分呢？好处：查询指定用户的所有订单，避免了跨库跨表查询。因为，根据一个用户的id来计算节点，用户的id是规定不变的，那么计算出的值永远是固定的(x库的x表.html)那么保存订单的时候，a用户的所有订单，都是在x库x表里面。需要查询a用户所有订单时，就不用进行跨库、跨表去查询了。缺点这种方式也不是没有缺点。缺点在于：数据分散不均匀，某些表的数据量特别大，某些表的数据量很小。因为某些用户下单量多，打个比方，1000-2000这个范围内的用户，下单特别多，而他们的id根据计算规则，都是分到了x库x表。造成这个表的数据量大，单表的数据量撑到极限后，咋办呢?总结一下：每种分库分表方案也不是十全十美，都是有利有弊的。目前来说，这种使用用户id来切分订单数据…