小菜鸟

java菜鸟号正在起航

Scala 流程控制:顺序、分支与循环的优雅实现

流程控制是编程语言的基础,用于控制代码的执行顺序。Scala 作为一门多范式语言,在流程控制上既保留了与 Java 相似的语法,又融入了函数式编程的特性,尤其在循环和中断处理上有独特设计。本文详细介绍 Scala 的三大流程控制语句。

顺序控制

顺序控制是最基础的流程模式,代码按照从上到下的顺序依次执行,没有跳转或分支。这是所有编程语言的默认执行方式,Scala 也不例外。

// 顺序执行示例
val a = 10
val b = 20
val sum = a + b
println(s"a + b = $sum")  // 先计算sum,再打印结果

特点

  • 无特殊关键字,代码自然流淌。
  • 适用于简单的步骤化操作(如变量定义、计算、输出)。

分支控制(if…else)

Scala 的分支控制通过 if...else if...else 实现,与 Java 语法相似,但更灵活 ——if 表达式有返回值,可直接赋值给变量。

基本语法

阅读全文 »

HBase 写数据流程:又快又不丢,靠的是”先记日志、再写内存、最后刷盘”

HBase 写入数据,面临两个矛盾的要求:

  • 要快:高吞吐写入,不能等磁盘 IO
  • 要可靠:RegionServer 宕机了,数据不能丢

HBase 的解法是三步走

  1. 先写日志(WAL)——记下来,保证不丢
  2. 再写内存(MemStore)——快,写完就返回客户端
  3. 最后异步刷盘——攒够了再写到磁盘

核心逻辑:写日志(慢但可靠)+ 写内存(快但易失)→ 两者结合 = 又快又不丢。

写一条数据,HBase 经历了什么?

1. 客户端发请求 → RegionServer
2. 写 WAL(预写日志,HDFS 持久化)
3. 写 MemStore(内存)
4. 返回客户端"成功"
5. (异步)MemStore 满了 → 刷盘成 StoreFile
6. (异步)小 StoreFile 合并成大文件(Compaction)

客户端在第 4 步就收到成功了,第 5、6 步是后台慢慢做的。

graph TD
    A[客户端] -->|1. 发送写请求| B[目标 RegionServer]
    B -->|2. 写入预写日志WAL/HLog<br/>同步到 HDFS 多副本| C[HLog 持久化]
    C -->|3. 写入内存缓存MemStore<br/>按 RowKey 排序| D[MemStore]
    D -->|4. 立即反馈| A
    D -->|5. 触发刷盘条件?<br/>大小/时间阈值等| E{判断}
    E -->|是| F[MemStore 刷盘<br/> 生成 StoreFile]
    E -->|否| D
    F -->|6. 小文件数量/大小达标?| G{判断}
    G -->|是| H[StoreFile 合并<br/>Minor/Major Compaction]
    G -->|否| F
    H -->|7. Region 大小超限?| I{判断}
    I -->|是| J[Region 拆分<br/> 子 Region 负载均衡]
    I -->|否| H
    J -->|8. 新 Region 分配到其他 RegionServer| B

第一步:定位 RegionServer

写数据之前,客户端要先知道”这条数据写到哪个 RegionServer 上”。

流程跟读数据一样:

  1. 客户端连 ZooKeeper,找 hbase:meta 表的位置
  2. hbase:meta 表,根据 RowKey 定位目标 Region 和 RegionServer
  3. 客户端直连那个 RegionServer

定位完成之后,后面的写操作都在这个 RegionServer 上完成。

第二步:写 WAL——“我先记下来,保证不丢”

WAL(Write-Ahead Log)是 HBase 数据可靠性的第一道防线。

阅读全文 »

FastDateFormat 线程安全的底层原理:从 SimpleDateFormat 的缺陷说起

在 Java 日期处理中,SimpleDateFormat 因线程不安全常导致诡异的并发问题,而 Apache Commons Lang 库的 FastDateFormat 则以线程安全为核心设计目标。本文将深入对比两者的实现差异,解析 FastDateFormat 如何通过设计规避线程安全问题,以及其缓存机制带来的性能优势。

SimpleDateFormat 的线程不安全根源

SimpleDateFormat 是 Java 原生的日期格式化工具,但在多线程环境下使用同一个实例会导致数据错乱,其根本原因在于共享的 Calendar 成员变量

核心问题:共享状态的并发修改

SimpleDateFormat 内部维护了一个 Calendar 实例(成员变量),用于解析和格式化日期。在多线程场景下,多个线程会同时操作这个共享的 Calendar,导致以下问题:

// SimpleDateFormat 的关键成员变量  
protected Calendar calendar;  

// 格式化方法的核心逻辑(简化版)  
private StringBuffer format(Date date, StringBuffer toAppendTo) {  
    // 步骤1:将日期设置到共享的 calendar 中  
    calendar.setTime(date);  // 线程A执行到此处,尚未完成格式化  

    // 步骤2:基于 calendar 格式化字符串  
    // 若此时线程B调用 setTime 修改了 calendar,线程A的结果会被污染  
    // ... 格式化逻辑 ...  
}
  • 并发冲突:线程 A 在执行 calendar.setTime(dateA) 后,尚未完成格式化,线程 B 调用 calendar.setTime(dateB) 覆盖了 calendar 的状态,导致线程 A 最终格式化的是 dateB 的值。
  • 表现症状:格式化结果出现随机的日期错乱(如年份、月份错误),且难以复现(与线程调度时机相关)。

常见错误用法

开发者常将 SimpleDateFormat 定义为静态变量以复用,这会放大线程安全问题:

阅读全文 »

Scala 数据类型:面向对象的类型系统

Scala 作为纯粹的面向对象语言,其数据类型系统与 Java 有显著差异 ——所有类型都是对象,不存在 Java 中的 “基本数据类型” 与 “引用类型” 的严格区分。Scala 类型系统以 Any 为根,分为 AnyVal(值类型)和 AnyRef(引用类型)两大分支,形成层次清晰的类型体系。

类型体系概览

Scala 类型系统的核心层次结构如下:

数据类型

  • 根类型:Any(所有类型的父类)
    • 值类型AnyVal(包括数值类型、布尔型等)
    • 引用类型AnyRef(包括类、特质、数组等,对应 Java 的 Object
  • 特殊类型Null(所有 AnyRef 的子类)、Nothing(所有类型的子类)

AnyVal:值类型

AnyVal 代表值类型,对应 Java 中的 “基本数据类型”,但在 Scala 中仍是对象,拥有方法和属性。常见的 AnyVal 类型包括:

类型 描述 示例
Int 32 位整数 val a: Int = 10
Long 64 位整数(后缀 L) val b: Long = 100L
Float 32 位浮点数(后缀 f) val c: Float = 3.14f
Double 64 位浮点数(默认小数类型) val d: Double = 3.14159
Boolean 布尔值(true/false) val e: Boolean = true
Char 16 位字符(单引号包裹) val f: Char = 'A'
Byte 8 位整数 val g: Byte = 0x10
Short 16 位整数 val h: Short = 32767
Unit 无返回值标记(类似 void) def hello(): Unit = println("hi")

值类型的特性

  1. 默认类型

    • 整数默认是 Int(超出范围自动推断为 Long
    • 小数默认是 Double(需显式加 f 声明 Float
    val num1 = 100      // 推断为Int
       val num2 = 10000000000L  // 显式声明Long
    val num3 = 3.14     // 推断为Double
       val num4 = 3.14f    // 显式声明Float
  2. 自动类型转换

    • 低精度类型可自动转换为高精度类型(类似 Java 的 “拓宽转换”)
    • 转换方向:Byte → Short → Int → Long → Float → Double
阅读全文 »

Scala 变量:声明、特性与命名规则

变量是编程语言的基础构建块,Scala 作为一门多范式语言,其变量声明方式与 Java 有显著差异,尤其体现在不可变性和类型推断上。本文详细介绍 Scala 变量的核心特性。

变量声明语法

Scala 变量声明必须初始化,基本语法如下:

// 完整语法:关键字 变量名[: 类型] = 初始值
var|val 变量名[: 变量类型] = 变量值

示例解析

// 1. 显式指定类型
var age: Int = 10  // 可变变量,类型为Int
val score: Double = 95.5  // 不可变变量,类型为Double

// 2. 省略类型(依赖编译器的类型推导)
var name = "Alice"  // 推导为String类型
val pi = 3.14159    // 推导为Double类型
  • 类型推导:Scala 编译器能根据初始值自动推断变量类型,通常无需显式声明,简化代码。
  • 必须初始化:与 Java 不同,Scala 不允许声明未初始化的变量(如 var x: Int; 会报错),确保变量始终有明确的值。

var 与 val:可变与不可变

Scala 变量分为两种类型,核心区别在于是否可重新赋值:

var(可变变量)

  • 允许后续重新赋值
  • 类似 Java 中未用 final 修饰的变量
阅读全文 »
0%