文章目录
- @FunctionInterface default并不会被计数
- Lambda使用
- **1. 集合操作**
- **(1) 遍历集合**
- **(2) 过滤集合**
- **(3) 转换集合**
- **(4) 排序集合**
- **2. 多线程编程**
- **(1) 创建线程**
- **(2) 使用线程池**
- **3. 比较器**
- **4. 事件监听器**
- **5. 函数式接口**
- **(1) Function 示例**
- **(2) Predicate 示例**
- **(3) Consumer 示例**
- **6. 方法引用**
- **(1) 静态方法引用**
- **(2) 实例方法引用**
- **总结**
- java.lang.reflect
- **1. 核心类**
- **(1) `Class<T>`**
- **(2) `Field`**
- 常用方法:
- **(3) `Method`**
- 常用方法:
- **(4) `Constructor<T>`**
- 常用方法:
- **(5) `Array`**
- 常用方法:
- **2. 接口**
- **(1) `Member`**
- 常用方法:
- **(2) `AnnotatedElement`**
- 常用方法:
- **3. 异常类**
- **(1) `IllegalAccessException`**
- **(2) `IllegalArgumentException`**
- **(3) `InvocationTargetException`**
- **(4) `NoSuchFieldException`**
- **(5) `NoSuchMethodException`**
- **4. 其他工具类**
- **(1) `Modifier`**
- 常用方法:
- **5. 总结**
- Linux 安装jdk/ tomcat/ mysql8/ redis
- Redis特点
- 1. **高性能**
- 2. **丰富的数据结构**
- 3. **持久化能力**
- 4. **事务支持**
- 5. **发布订阅模式**
- 6. **分片与集群**
- 7. **过期时间设置**
- 8. **Lua脚本支持**
- 9. **安全性**
- 10. **多种语言客户端支持**
- i++为什么不是原子性(存在并发问题)
- 多线程什么时候会快?线程间通信会拖慢速度吗?
- 多线程什么时候会快?
- 线程间通信是否会拖慢速度?
- Redis 单线程模型 + IO 多路复用 (异步IO/阻塞IO)
- Redis 中的数据操作
- **1. Key 的相关操作**
- **2. String 类型相关操作**
- **3. List 类型相关操作**
- **4. Set 类型相关操作**
- **5. Hash 类型相关操作**
- **6. Sorted Set 类型相关操作**
- Redis处理关系型数据的三种办法
- **场景:用户信息管理**
- **设计思路**
- **示例代码**
- **(1) 插入用户数据**
- **(2) 查询用户数据**
- **(3) 更新用户数据**
- **(4) 删除用户数据**
- **批量操作**
- **(1) 批量查询**
- **(2) 批量插入**
- **扩展:范围查询**
- **(1) 使用集合存储用户 ID**
- **(2) 遍历用户信息**
- **优点**
- **缺点**
- Redis配置文件常见属性及其功能
- 1. **网络相关配置**
- 2. **持久化相关配置**
- 3. **内存管理**
- 4. **安全**
- 5. **复制**
- 6. **慢查询日志**
- 7. **其他**
- SpringData Redis使用
- RedisTemplate 中的序列化器
- 常见的序列化器
- 配置 `RedisTemplate` 的序列化器
- 不同序列化器的选择场景
- 注意事项
- 如何切换jedis
- LUA脚本
- 基础语法
- 表(Table)
- 模块和包
- 与 C/C++ 集成
- 示例:简单的 Lua 脚本
- Redis持久化策略
- Redis内存淘汰策略
- 什么是哈希桶
- 缓存三件套
- 什么是扰动函数,哪些设计考虑到了扰动函数
- 扰动函数的定义与设计应用
- 1. **扰动函数的定义**
- **(1) 化学中的“扰动力函数”(Forging Function)**
- **(2) 计算机科学中的“扰动函数”**
- **Java HashMap 的扰动函数**
- **2. 设计中考虑扰动函数的场景**
- **(1) 哈希表(如 HashMap)的设计**
- **(2) 缓存系统的设计(如 Redis)**
- **(3) 密码学与安全协议**
- **(4) 机器学习与优化算法**
- **3. 设计扰动函数的核心考量**
- HashMap中的精妙设计
- ConcurrentHashMap的优化
- **1. 分段锁(Segment Locking)机制(Java 7 及之前)**
- **基本思想**
- **工作原理**
- **优点**
- **缺点**
- **2. CAS + 细粒度锁(Java 8 及之后)**
- **优化点**
- **3. 读操作的优化**
- **无锁读**
- **volatile 保证可见性**
- **4. 写操作的优化**
- **CAS 和锁结合**
- **批量插入**
- **5. 扩容优化**
- **渐进式扩容**
- **多线程协作**
- **6. 其他优化**
- **计数器优化**
- **弱一致性迭代器**
- **7. 性能对比**
- **8. 使用场景**
- **9. 注意事项**
- **总结**
- LongAdder思想
- **1. 基本思想**
- **分段锁机制(Striped Locking)**
- **惰性初始化(Lazy Initialization)**
- **2. 工作流程**
- **3. 优势与劣势**
- **优势**
- **劣势**
- **4. 使用场景**
@FunctionInterface default并不会被计数
函数式接口中限制1个抽象接口,其他默认实现和静态实现不会被计数
Lambda使用
在 Java 中,Lambda 表达式是一种简洁的方式来实现函数式接口(只有一个抽象方法的接口)。它广泛应用于集合操作、多线程编程和事件处理等场景。
1. 集合操作
(1) 遍历集合
使用 forEach
方法遍历集合。
import java.util.Arrays;
import java.util.List;public class Main {public static void main(String[] args) {List<String> names = Arrays.asList("Alice", "Bob", "Charlie");// 使用 Lambda 表达式遍历names.forEach(name -> System.out.println(name));}
}
(2) 过滤集合
使用 Stream
API 的 filter
方法过滤集合中的元素。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;public class Main {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);// 使用 Lambda 表达式过滤偶数List<Integer> evenNumbers = numbers.stream().filter(n -> n % 2 == 0).collect(Collectors.toList());System.out.println(evenNumbers); // 输出 [2, 4, 6]}
}
(3) 转换集合
使用 map
方法对集合中的元素进行转换。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;public class Main {public static void main(String[] args) {List<String> names = Arrays.asList("Alice", "Bob", "Charlie");// 使用 Lambda 表达式将名字转为大写List<String> upperCaseNames = names.stream().map(name -> name.toUpperCase()).collect(Collectors.toList());System.out.println(upperCaseNames); // 输出 [ALICE, BOB, CHARLIE]}
}
(4) 排序集合
使用 sorted
方法对集合进行排序。
import java.util.Arrays;
import java.util.List;public class Main {public static void main(String[] args) {List<String> names = Arrays.asList("Charlie", "Alice", "Bob");// 使用 Lambda 表达式按字典顺序排序names.sort((a, b) -> a.compareTo(b));System.out.println(names); // 输出 [Alice, Bob, Charlie]}
}
2. 多线程编程
(1) 创建线程
使用 Lambda 表达式简化线程创建。
public class Main {public static void main(String[] args) {// 使用 Lambda 表达式创建并启动线程Thread thread = new Thread(() -> System.out.println("Thread is running"));thread.start();}
}
(2) 使用线程池
通过 ExecutorService
和 Lambda 表达式提交任务。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Main {public static void main(String[] args) {ExecutorService executor = Executors.newFixedThreadPool(2);// 提交任务executor.submit(() -> System.out.println("Task 1 is running"));executor.submit(() -> System.out.println("Task 2 is running"));executor.shutdown();}
}
3. 比较器
使用 Lambda 表达式实现 Comparator
接口。
import java.util.Arrays;
import java.util.List;public class Main {public static void main(String[] args) {List<String> names = Arrays.asList("Charlie", "Alice", "Bob");// 使用 Lambda 表达式按长度排序names.sort((a, b) -> Integer.compare(a.length(), b.length()));System.out.println(names); // 输出 [Bob, Alice, Charlie]}
}
4. 事件监听器
在 GUI 编程中,使用 Lambda 表达式替代匿名内部类。
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;public class Main {public static void main(String[] args) {JButton button = new JButton("Click Me");// 使用 Lambda 表达式添加事件监听器button.addActionListener(e -> System.out.println("Button clicked!"));JFrame frame = new JFrame();frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);frame.add(button);frame.pack();frame.setVisible(true);}
}
5. 函数式接口
Java 提供了一些内置的函数式接口,例如 Function
、Predicate
、Consumer
等,它们可以与 Lambda 表达式结合使用。
(1) Function 示例
Function<T, R>
表示一个接受参数类型为 T
并返回结果类型为 R
的函数。
import java.util.function.Function;public class Main {public static void main(String[] args) {Function<Integer, String> convert = num -> "Number: " + num;System.out.println(convert.apply(42)); // 输出 Number: 42}
}
(2) Predicate 示例
Predicate<T>
表示一个接受参数类型为 T
并返回布尔值的函数。
import java.util.function.Predicate;public class Main {public static void main(String[] args) {Predicate<Integer> isEven = num -> num % 2 == 0;System.out.println(isEven.test(4)); // 输出 trueSystem.out.println(isEven.test(5)); // 输出 false}
}
(3) Consumer 示例
Consumer<T>
表示一个接受参数类型为 T
并不返回任何结果的操作。
import java.util.function.Consumer;public class Main {public static void main(String[] args) {Consumer<String> print = message -> System.out.println(message);print.accept("Hello, World!"); // 输出 Hello, World!}
}
6. 方法引用
Lambda 表达式还可以通过方法引用进一步简化代码。
(1) 静态方法引用
import java.util.function.Function;public class Main {public static void main(String[] args) {Function<String, Integer> stringLength = String::length;System.out.println(stringLength.apply("Hello")); // 输出 5}
}
(2) 实例方法引用
import java.util.function.Consumer;public class Main {public static void main(String[] args) {Consumer<String> print = System.out::println;print.accept("Hello, Lambda!"); // 输出 Hello, Lambda!}
}
总结
Lambda 表达式在 Java 中的应用非常广泛,特别是在以下场景:
- 集合操作(如
forEach
、filter
、map
等)。 - 多线程编程(如
Runnable
、Callable
、ExecutorService
等)。 - 比较器(如
Comparator
)。 - 事件监听器(如 GUI 编程中的按钮点击事件)。
- 函数式接口(如
Function
、Predicate
、Consumer
等)。
通过 Lambda 表达式,代码变得更加简洁、易读且高效。
java.lang.reflect
Java 反射机制主要位于 java.lang.reflect
包中,它提供了一系列类和接口,用于在运行时动态地获取类的信息(如类、方法、字段、构造器等)并进行操作。以下是 java.lang.reflect
包中的主要内容及其功能概述:
1. 核心类
(1) Class<T>
- 位于
java.lang
包中,是反射的核心类。 - 表示一个类或接口的运行时类型信息。
- 提供了获取类的构造器、方法、字段等功能。
Class<?> clazz = String.class;
(2) Field
- 表示类中的字段(成员变量)。
- 提供了获取和设置字段值的功能。
常用方法:
getName()
:返回字段的名称。getType()
:返回字段的类型。get(Object obj)
:获取指定对象实例中该字段的值。set(Object obj, Object value)
:设置指定对象实例中该字段的值。
Field field = clazz.getDeclaredField("fieldName");
field.setAccessible(true); // 如果字段是私有的
field.set(instance, "new value");
(3) Method
- 表示类中的方法。
- 提供了调用方法的功能。
常用方法:
getName()
:返回方法的名称。getReturnType()
:返回方法的返回类型。getParameterTypes()
:返回方法的参数类型数组。invoke(Object obj, Object... args)
:调用指定对象实例上的方法。
Method method = clazz.getMethod("methodName", String.class);
Object result = method.invoke(instance, "argument");
(4) Constructor<T>
- 表示类的构造器。
- 提供了创建类实例的功能。
常用方法:
newInstance(Object... initargs)
:使用指定的参数创建类的新实例。
Constructor<?> constructor = clazz.getConstructor(String.class);
Object instance = constructor.newInstance("constructor argument");
(5) Array
- 提供了操作数组的静态方法。
- 主要用于动态创建和访问数组。
常用方法:
newInstance(Class<?> componentType, int length)
:创建一个指定类型的数组。get(Object array, int index)
:获取数组中指定索引位置的值。set(Object array, int index, Object value)
:设置数组中指定索引位置的值。
Object array = Array.newInstance(Integer.class, 5);
Array.set(array, 0, 10);
System.out.println(Array.get(array, 0)); // 输出 10
2. 接口
(1) Member
- 表示类的成员(字段、方法、构造器)。
- 是
Field
、Method
和Constructor
的共同父接口。
常用方法:
getName()
:返回成员的名称。getDeclaringClass()
:返回声明该成员的类。
(2) AnnotatedElement
- 表示可以被注解修饰的元素(类、方法、字段等)。
- 提供了检查注解的功能。
常用方法:
isAnnotationPresent(Class<? extends Annotation> annotationClass)
:检查是否包含指定的注解。getAnnotation(Class<T> annotationClass)
:获取指定类型的注解。getAnnotations()
:获取所有注解。
if (clazz.isAnnotationPresent(MyAnnotation.class)) {MyAnnotation annotation = clazz.getAnnotation(MyAnnotation.class);System.out.println(annotation.value());
}
3. 异常类
(1) IllegalAccessException
- 当试图通过反射访问无法访问的成员(例如私有字段或方法)时抛出。
(2) IllegalArgumentException
- 当传递给反射方法的参数不合法时抛出。
(3) InvocationTargetException
- 当通过反射调用的方法本身抛出异常时,会包装成此异常抛出。
(4) NoSuchFieldException
- 当试图获取不存在的字段时抛出。
(5) NoSuchMethodException
- 当试图获取不存在的方法时抛出。
4. 其他工具类
(1) Modifier
- 提供了处理修饰符(如
public
、private
、static
等)的工具方法。 - 常用于判断类、方法或字段的访问权限。
常用方法:
isPublic(int mod)
:判断是否为public
。isPrivate(int mod)
:判断是否为private
。isStatic(int mod)
:判断是否为static
。toString(int mod)
:将修饰符转换为字符串表示形式。
int modifiers = field.getModifiers();
System.out.println(Modifier.toString(modifiers));
5. 总结
java.lang.reflect
包的主要内容如下:
类/接口 | 功能描述 |
---|---|
Class<T> | 获取类的元信息 |
Field | 访问和修改字段 |
Method | 调用方法 |
Constructor<T> | 创建类的实例 |
Array | 操作数组 |
Member | 表示类的成员(字段、方法、构造器) |
AnnotatedElement | 检查注解 |
Modifier | 处理修饰符 |
通过这些类和接口,Java 反射机制允许开发者在运行时动态地分析和操作类及其成员,从而实现更灵活的程序设计。反射可能会带来性能开销,并且破坏封装性,因此应谨慎使用。
Linux 安装jdk/ tomcat/ mysql8/ redis
- 通过本地 / 远程连接进行文件上传
- 防火墙和Net配置完毕可以通过yum镜像下载
- 通过tar相关命令解压解包 以及 redis需要的gcc环境,编译安装
Redis特点
Redis 是一种开源的、基于内存的数据结构存储系统,它可以用作数据库、缓存和消息中间件。
1. 高性能
- 基于内存:Redis 主要将数据存储在内存中,这使得它的读写速度非常快。
- 异步复制:支持主从异步复制,保证了高可用性和数据冗余,同时不影响性能。
- 非阻塞I/O:使用多路复用技术处理网络I/O,确保在处理大量客户端连接时依然保持高效。
2. 丰富的数据结构
- 字符串(String):最基本的数据类型,可用于存储文本或二进制数据。
- 列表(List):链表结构,支持从两端插入和移除元素。
- 集合(Set):无序且不重复的元素集合,支持集合间的交集、并集、差集等操作。
- 有序集合(Sorted Set):每个元素关联一个分数,依据分数排序,适用于排行榜等场景。
- 哈希(Hash):键值对的集合,适合存储对象。
- 位图(Bitmaps)、HyperLogLogs 和 地理空间(Geospatial) 等特殊用途的数据结构。
3. 持久化能力
- RDB快照:定期将内存中的数据集以快照形式保存到磁盘上。
- AOF日志:记录服务器接收到的所有修改性指令序列,并将其追加到文件中。通过重放这些命令来恢复数据。
4. 事务支持
- 虽然没有传统关系型数据库那样的复杂事务机制,但Redis提供了
MULTI
、EXEC
、DISCARD
和WATCH
等命令实现简单的事务控制,保证一系列命令执行的原子性。
5. 发布订阅模式
- 支持发布/订阅模式的消息通信机制,便于构建实时应用如聊天室或事件驱动架构。
6. 分片与集群
- 分片(Sharding):通过客户端实现数据分布于多个Redis实例间的技术。
- Redis Cluster:官方提供的分布式解决方案,支持自动分片和故障转移功能。
7. 过期时间设置
- 可以为键设置生存时间(TTL),过期后键会自动被删除,非常适合做缓存。
8. Lua脚本支持
- 允许用户编写Lua脚本来扩展Redis的功能,实现复杂的业务逻辑。
9. 安全性
- 提供密码验证机制,可以限制访问;还可以通过配置防火墙规则进一步加强安全防护。
10. 多种语言客户端支持
- Redis 拥有广泛的社区支持,几乎所有的主流编程语言都有对应的客户端库。
这些特性使得 Redis 在众多应用场景中表现出色,无论是作为缓存解决方案还是直接用作数据库都非常合适。
i++为什么不是原子性(存在并发问题)
i++ 汇编其实是 赋值 增加 赋值 三步,每一步是原子性的,每一步之间会被插队,所以i++整体不是原子的。
多线程什么时候会快?线程间通信会拖慢速度吗?
多线程什么时候会快?
-
I/O 密集型任务:
- 当程序需要频繁地进行 I/O 操作(如文件读写、网络请求等),这些操作通常会导致线程阻塞等待外部资源。在这种情况下,使用多线程可以让其他线程在某个线程等待时继续执行任务,从而提高整体效率。
-
CPU 密集型任务:
- 对于需要大量计算的任务,如果可以将任务分解为多个独立的小任务,并且这些小任务可以在不同的 CPU 核心上并行执行,那么使用多线程或多个进程可以显著加速处理过程。然而,需要注意的是,过多的线程创建可能会导致上下文切换开销增加,反而降低性能。
-
异步处理:
- 在某些场景下,比如服务器端开发,可能需要同时处理大量的客户端请求。通过使用多线程或者异步非阻塞I/O模型,可以使服务器同时服务于更多的用户,提高吞吐量。
线程间通信是否会拖慢速度?
线程间通信确实有可能成为性能瓶颈,尤其是在以下几个方面:
-
锁竞争:
- 如果多个线程需要访问共享资源,则必须确保对这些资源的访问是同步的,这通常涉及到加锁机制。频繁的锁争用会导致线程被阻塞,等待获取锁,从而降低了并发度和性能。
-
上下文切换:
- 频繁的线程间通信可能导致更多的上下文切换,即操作系统需要保存当前线程的状态并将控制权转移给另一个线程。这种切换是有代价的,它消耗了额外的CPU周期,特别是在高频率发生时。
-
数据传输成本:
- 线程之间传递消息或数据也需要时间,尤其是当数据量较大时。此外,如果设计不当,可能会引入不必要的内存复制或其他低效的操作。
-
复杂性增加:
- 增加了代码的复杂性和潜在的错误来源,例如死锁、竞态条件等问题,这些问题不仅影响性能,还可能导致程序行为异常。
为了减轻线程间通信带来的负面影响,可以考虑以下策略:
- 减少锁的粒度:尽量缩小临界区范围,只锁定必要的部分。
- 使用无锁算法:在某些情况下,可以采用原子操作或其他无锁技术来避免传统意义上的锁。
- 优化通信模式:选择合适的线程间通信机制,比如队列、管道等,并尽量减少不必要的通信。
- 批处理:合并多个小的数据传输请求为一个大的请求,以减少实际的交互次数。
Redis 单线程模型 + IO 多路复用 (异步IO/阻塞IO)
-
单线程: 负责 命令的执行工作
-
异步IO: 负责 连接客户端 执行命令
-
命令执行等待区:所有命令在这里排队执行,相当于火车的检票口
Redis 中的数据操作
1. Key 的相关操作
命令 | 功能描述 |
---|---|
DEL key | 删除指定的 key。 |
EXISTS key | 检查 key 是否存在,存在返回 1,否则返回 0。 |
EXPIRE key seconds | 设置 key 的过期时间(单位:秒)。 |
TTL key | 查看 key 的剩余生存时间(单位:秒)。如果 key 没有设置过期时间,返回 -1。 |
PERSIST key | 移除 key 的过期时间,使其变为持久化。 |
TYPE key | 返回 key 所存储的值的类型(如 string、list、set 等)。 |
RENAME key newkey | 将 key 重命名为 newkey。 |
KEYS pattern | 查找所有符合给定模式的 key(例如 KEYS user:* )。 |
2. String 类型相关操作
命令 | 功能描述 |
---|---|
SET key value | 设置 key 的值为 value。 |
GET key | 获取 key 的值。 |
INCR key | 将 key 中存储的数字值自增 1。 |
DECR key | 将 key 中存储的数字值自减 1。 |
INCRBY key increment | 将 key 中存储的数字值增加指定的增量值。 |
DECRBY key decrement | 将 key 中存储的数字值减少指定的减量值。 |
APPEND key value | 如果 key 已经存在并且是一个字符串,则将 value 追加到 key 原来的值后面。 |
STRLEN key | 返回 key 所存储的字符串值的长度。 |
MSET key1 value1 key2 value2 | 同时设置多个 key-value 对。 |
MGET key1 key2 | 同时获取多个 key 的值。 |
3. List 类型相关操作
命令 | 功能描述 |
---|---|
LPUSH key value | 将一个值插入到列表头部(左侧)。 |
RPUSH key value | 将一个值插入到列表尾部(右侧)。 |
LPOP key | 移除并返回列表的第一个元素(左侧)。 |
RPOP key | 移除并返回列表的最后一个元素(右侧)。 |
LRANGE key start stop | 返回列表中指定范围内的元素(索引从 0 开始)。 |
LLEN key | 返回列表的长度。 |
LREM key count value | 从列表中移除指定数量的值等于 value 的元素(count > 0 从头开始,count < 0 从尾开始)。 |
LINDEX key index | 返回列表中指定索引位置的元素。 |
LTRIM key start stop | 修剪列表,只保留指定范围内的元素。 |
4. Set 类型相关操作
命令 | 功能描述 |
---|---|
SADD key member | 向集合中添加一个成员。 |
SMEMBERS key | 返回集合中的所有成员。 |
SREM key member | 从集合中移除一个成员。 |
SCARD key | 返回集合中成员的数量。 |
SISMEMBER key member | 判断 member 是否是集合的成员,是则返回 1,否则返回 0。 |
SINTER key1 key2 | 返回两个集合的交集。 |
SUNION key1 key2 | 返回两个集合的并集。 |
SDIFF key1 key2 | 返回第一个集合与第二个集合的差集。 |
SRANDMEMBER key [count] | 随机返回集合中的一个或多个成员。 |
5. Hash 类型相关操作
命令 | 功能描述 |
---|---|
HSET key field value | 设置哈希表中字段的值。 |
HGET key field | 获取哈希表中字段的值。 |
HMSET key field1 value1 field2 value2 | 同时设置哈希表中多个字段的值。 |
HMGET key field1 field2 | 同时获取哈希表中多个字段的值。 |
HGETALL key | 返回哈希表中所有的字段和值。 |
HDEL key field | 删除哈希表中的一个字段。 |
HEXISTS key field | 判断哈希表中是否存在指定字段,存在返回 1,否则返回 0。 |
HLEN key | 返回哈希表中字段的数量。 |
HINCRBY key field increment | 将哈希表中字段的值增加指定的增量值。 |
6. Sorted Set 类型相关操作
命令 | 功能描述 |
---|---|
ZADD key score member | 向有序集合中添加一个成员,并指定分数。 |
ZRANGE key start stop [WITHSCORES] | 返回有序集合中指定范围内的成员(按分数从小到大排序)。 |
ZREVRANGE key start stop [WITHSCORES] | 返回有序集合中指定范围内的成员(按分数从大到小排序)。 |
ZCARD key | 返回有序集合中成员的数量。 |
ZSCORE key member | 返回有序集合中指定成员的分数。 |
ZREM key member | 从有序集合中移除一个成员。 |
ZRANK key member | 返回成员在有序集合中的排名(按分数从小到大排序)。 |
ZREVRANK key member | 返回成员在有序集合中的排名(按分数从大到小排序)。 |
ZINCRBY key increment member | 将有序集合中成员的分数增加指定的增量值。 |
Redis处理关系型数据的三种办法
以数据行为主,字段+对应数据行数据是一个kv,一个数据行有n个kv。
String格式有两种:
- 可以用特殊格式的key :v ,比如 id name age 将 两个半kv放在key中,v放最后一个值
- k : 序列化 v
Hash中 直接用 k : 多个 kv
以下是一个具体的例子,展示如何使用 Redis 的 String
类型和长 key 格式来处理类似关系型数据库的数据。
场景:用户信息管理
假设我们需要存储用户的基本信息,包括用户的 ID、姓名、年龄和邮箱地址。在一个关系型数据库中,这些信息可能存储在一个名为 users
的表中,表的结构如下:
id | name | age | |
---|---|---|---|
1 | Alice | 25 | alice@example.com |
2 | Bob | 30 | bob@example.com |
我们可以使用 Redis 的 String
类型和长 key 格式来模拟这种结构。
设计思路
- 每个用户的信息可以通过一个唯一的
key
来表示。 - 使用长 key 格式设计规则:
user:<id>:<field>
,其中:<id>
是用户的唯一标识(类似于主键)。<field>
是用户的某个属性字段(如name
、age
、email
等)。
例如:
- 用户 ID 为 1 的姓名存储为
user:1:name
。 - 用户 ID 为 1 的年龄存储为
user:1:age
。 - 用户 ID 为 1 的邮箱存储为
user:1:email
。
每个 key
对应的值就是该字段的具体内容。
示例代码
(1) 插入用户数据
# 用户 1 的信息
SET user:1:name "Alice"
SET user:1:age "25"
SET user:1:email "alice@example.com"# 用户 2 的信息
SET user:2:name "Bob"
SET user:2:age "30"
SET user:2:email "bob@example.com"
(2) 查询用户数据
# 获取用户 1 的姓名
GET user:1:name
# 返回 "Alice"# 获取用户 2 的邮箱
GET user:2:email
# 返回 "bob@example.com"
(3) 更新用户数据
# 更新用户 1 的年龄
SET user:1:age "26"# 验证更新是否成功
GET user:1:age
# 返回 "26"
(4) 删除用户数据
# 删除用户 2 的信息
DEL user:2:name
DEL user:2:age
DEL user:2:email
批量操作
Redis 支持批量操作,可以通过 MGET
和 MSET
实现对多个字段的读取或写入。
(1) 批量查询
MGET user:1:name user:1:age user:1:email
# 返回 ["Alice", "25", "alice@example.com"]
(2) 批量插入
MSET user:3:name "Charlie" user:3:age "35" user:3:email "charlie@example.com"
扩展:范围查询
由于 Redis 的 String
类型本身不支持范围查询,但我们可以通过其他方式实现类似的查询功能。例如:
(1) 使用集合存储用户 ID
可以使用 Redis 的 Set
数据类型来存储所有用户的 ID,方便后续遍历。
SADD users 1 2 3
(2) 遍历用户信息
结合 SMEMBERS
和 MGET
可以实现对所有用户信息的遍历。
# 获取所有用户 ID
SMEMBERS users
# 返回 [1, 2, 3]# 构造批量查询 key
# 假设需要查询所有用户的姓名
MGET user:1:name user:2:name user:3:name
# 返回 ["Alice", "Bob", "Charlie"]
优点
- 简单直观:通过长 key 格式,可以非常清晰地表达数据之间的关系。
- 灵活性高:可以根据需要动态添加或删除字段,无需固定的表结构。
- 高效查询:单个字段的查询和更新都非常高效,因为 Redis 的
String
类型是基于内存操作的。
缺点
- 缺乏复杂查询能力:Redis 的
String
类型本身不支持复杂的 SQL 查询(如 JOIN、GROUP BY 等)。如果需要复杂查询,可能需要额外的逻辑处理。 - 存储冗余:长 key 格式可能会导致存储冗余,尤其是当字段较多时,
key
的长度会增加存储开销。 - 难以管理大规模数据:如果用户数量庞大,手动构造和管理长 key 可能会变得复杂。
Redis配置文件常见属性及其功能
Redis 配置文件(通常名为 redis.conf
)包含了许多配置选项,用于定制 Redis 实例的行为。以下是 Redis 配置文件中一些常见的属性及其功能说明:
1. 网络相关配置
-
bind
- 功能:指定 Redis 服务器监听的 IP 地址,默认情况下是
127.0.0.1
,这意味着 Redis 只接受来自本机的连接。如果要让 Redis 接受外部连接,可以设置为服务器的实际 IP 地址或使用0.0.0.0
来监听所有可用接口。 - 示例:
bind 0.0.0.0
- 功能:指定 Redis 服务器监听的 IP 地址,默认情况下是
-
port
- 功能:定义 Redis 监听的端口号,默认是
6379
。 - 示例:
port 6379
- 功能:定义 Redis 监听的端口号,默认是
-
timeout
- 功能:客户端空闲多少秒后关闭连接(以秒为单位)。默认值为
0
,表示不超时。 - 示例:
timeout 300
- 功能:客户端空闲多少秒后关闭连接(以秒为单位)。默认值为
2. 持久化相关配置
-
save
- 功能:RDB 持久化的触发条件,格式为
save <seconds> <changes>
,即在指定的时间内至少发生了多少次修改时会触发一次快照保存。 - 示例:
save 900 1
表示如果在 900 秒内至少有 1 次更改,则执行 BGSAVE。
- 功能:RDB 持久化的触发条件,格式为
-
appendonly
- 功能:是否开启 AOF(Append Only File)日志记录模式,默认是
no
。开启后,每个写操作都会被追加到.aof
文件中。 - 示例:
appendonly yes
- 功能:是否开启 AOF(Append Only File)日志记录模式,默认是
-
appendfsync
- 功能:控制 AOF 文件同步策略。可选值包括
always
、everysec
和no
,分别代表每次修改都同步、每秒钟同步一次和依赖操作系统同步。 - 示例:
appendfsync everysec
- 功能:控制 AOF 文件同步策略。可选值包括
3. 内存管理
-
maxmemory
- 功能:设置 Redis 实例的最大内存使用量。当达到此限制时,Redis 将根据
maxmemory-policy
的设定来移除键。 - 示例:
maxmemory 512mb
- 功能:设置 Redis 实例的最大内存使用量。当达到此限制时,Redis 将根据
-
maxmemory-policy
- 功能:决定当达到最大内存限制时,Redis 如何选择要移除的键。常见的策略包括
volatile-lru
、allkeys-lru
等。 - 示例:
maxmemory-policy allkeys-lru
- 功能:决定当达到最大内存限制时,Redis 如何选择要移除的键。常见的策略包括
4. 安全
-
requirepass
- 功能:设置访问 Redis 服务器所需的密码。这有助于保护 Redis 实例免受未经授权的访问。
- 示例:
requirepass yourpassword
-
rename-command
- 功能:重命名或禁用某些命令,增强安全性。例如,可以禁用 FLUSHALL 命令。
- 示例:
rename-command FLUSHALL ""
5. 复制
-
slaveof
- 功能:配置当前实例作为另一个 Redis 实例的从库。需要指定主库的 IP 地址和端口。
- 示例:
slaveof master-ip 6379
-
masterauth
- 功能:如果主库设置了密码,则在此处指定密码以便从库能够成功连接。
- 示例:
masterauth master-password
6. 慢查询日志
-
slowlog-log-slower-than
- 功能:定义一个命令执行时间超过多少微秒会被记录到慢查询日志中。默认值是
10000
微秒(即 10 毫秒)。 - 示例:
slowlog-log-slower-than 5000
- 功能:定义一个命令执行时间超过多少微秒会被记录到慢查询日志中。默认值是
-
slowlog-max-len
- 功能:慢查询日志的最大条目数。一旦超过这个数量,旧的日志条目将被删除。
- 示例:
slowlog-max-len 128
7. 其他
-
databases
- 功能:设置数据库的数量,默认是 16。不同的数据库之间数据是隔离的。
- 示例:
databases 16
-
logfile
- 功能:指定 Redis 日志文件的位置。如果设置为空字符串,则日志输出到标准输出设备。
- 示例:
logfile /var/log/redis/redis-server.log
-
dir
- 功能:指定工作目录,在该目录下 Redis 将创建 RDB 文件和其他临时文件。
- 示例:
dir ./
以上列出了一些常用的 Redis 配置项,实际应用中可能还需要根据具体的业务需求调整更多参数。正确配置这些参数可以帮助优化 Redis 性能,并确保其安全稳定地运行。
SpringData Redis使用
-
引入场景启动器
-
设置redis配置
- host
- port
- lettuce (默认连接池):pool : 参数
- client-type
-
启动redis
RedisTemplate 中的序列化器
在使用 Spring Data Redis 时,RedisTemplate
是一个核心组件,用于与 Redis 进行交互。RedisTemplate
提供了对 Redis 数据的序列化和反序列化的支持,而这些操作是由不同的**序列化器(Serializer)**来完成的。
默认情况下,RedisTemplate
使用 Java 的 JdkSerializationRedisSerializer
,它会将对象序列化为二进制数据存储到 Redis 中。然而,这种序列化方式可能会导致存储的数据不可读,并且与其他语言或工具的兼容性较差。因此,在实际开发中,我们通常会根据需求自定义序列化器。
常见的序列化器
Spring Data Redis 提供了多种内置的序列化器,以下是一些常用的序列化器:
-
JdkSerializationRedisSerializer
- 默认使用的序列化器。
- 使用 Java 的序列化机制(
ObjectOutputStream
和ObjectInputStream
)。 - 序列化后的数据是二进制格式,占空间较大,且不易读。
- 示例:
redisTemplate.setKeySerializer(new JdkSerializationRedisSerializer());
-
StringRedisSerializer
- 用于处理字符串数据。
- 将键或值直接作为字符串进行存储和读取。
- 示例:
redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new StringRedisSerializer());
-
GenericJackson2JsonRedisSerializer
- 使用 Jackson 库将对象序列化为 JSON 格式。
- 序列化后的数据是可读的 JSON 字符串,适合跨语言使用。
- 示例:
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
-
OxmSerializer
- 使用 XML 格式进行序列化。
- 适用于需要以 XML 格式存储数据的场景。
-
ByteArrayRedisSerializer
- 直接存储原始字节数组。
- 示例:
redisTemplate.setValueSerializer(new ByteArrayRedisSerializer());
-
自定义序列化器
- 可以通过实现
org.springframework.data.redis.serializer.RedisSerializer<T>
接口来自定义序列化逻辑。
- 可以通过实现
配置 RedisTemplate
的序列化器
在 Spring 中,可以通过配置 RedisTemplate
来指定键和值的序列化器。以下是一个完整的示例:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<String, Object> template = new RedisTemplate<>();// 设置键的序列化器template.setKeySerializer(new StringRedisSerializer());template.setHashKeySerializer(new StringRedisSerializer());// 设置值的序列化器template.setValueSerializer(new GenericJackson2JsonRedisSerializer());template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());// 设置连接工厂template.setConnectionFactory(redisConnectionFactory);return template;}
}
不同序列化器的选择场景
-
StringRedisSerializer
:- 适用于键或值是简单字符串的情况。
- 如果你的 Redis 数据只需要存储简单的字符串,可以使用此序列化器。
-
GenericJackson2JsonRedisSerializer
:- 适用于需要存储复杂对象并且希望数据可读的场景。
- 如果你需要跨语言使用 Redis 数据,或者希望 Redis 中的数据是人类可读的 JSON 格式,推荐使用此序列化器。
-
JdkSerializationRedisSerializer
:- 适用于纯 Java 环境下,不需要考虑跨语言兼容性的场景。
- 如果你对性能要求较高,同时不介意存储二进制数据,可以选择此序列化器。
-
自定义序列化器:
- 如果内置的序列化器无法满足需求,例如需要加密或压缩数据,可以实现自定义序列化器。
注意事项
-
一致性:
- 在同一个 Redis 实例中,确保键和值的序列化方式一致,否则可能会导致反序列化失败。
-
数据迁移:
- 如果更改了序列化器,旧数据可能无法被正确解析,因此需要清理或迁移数据。
-
性能:
- 不同的序列化器对性能的影响不同。例如,JSON 序列化器比 JDK 序列化器更耗时,但生成的数据更小、更易读。
-
安全性:
- 如果存储的数据是敏感信息,建议使用加密序列化器或在存储前对数据进行加密。
通过合理选择和配置 RedisTemplate
的序列化器,可以让 Redis 更好地适应你的应用场景,同时提高数据的可读性和兼容性。
如何切换jedis
排除lettuce依赖,导入jedis
LUA脚本
Lua 是一种轻量级、高性能的嵌入式脚本语言,广泛用于游戏开发、嵌入式系统、Web 开发以及其他需要脚本支持的应用程序中。它的设计目标是简单、小巧且易于集成到 C 和 C++ 程序中。
基础语法
-
变量定义
在 Lua 中,你可以使用=
来给变量赋值。Lua 支持动态类型,这意味着你不需要在声明时指定变量的类型。message = "Hello, World!" number = 42
-
注释
单行注释以--
开始,多行注释则使用--[[
和]]
。-- 这是一个单行注释 --[[ 这是一个多行注释 ]]
-
控制结构
包括条件语句(如if
)、循环语句(如while
,for
)等。if number > 40 thenprint("The number is greater than 40") endfor i = 1, 5 doprint(i) end
-
函数
函数使用function
关键字定义,并可以返回多个结果。function greet(name)return "Hello, " .. name .. "!" end print(greet("Lua"))
表(Table)
表是 Lua 的唯一数据结构,它可以用来表示数组、集合、记录和其他数据结构。
-- 创建一个表
local array = {"Lua", "Scripting", "Language"}-- 访问表中的元素
print(array[1]) -- 输出: Lua-- 使用表作为字典
local dict = {name = "Lua", type = "Scripting Language"}
print(dict["name"]) -- 输出: Lua
模块和包
Lua 支持模块化编程,通过 require
函数来加载模块。
-- 定义一个模块
local myModule = {}function myModule.sayHello()print("Hello from module!")
endreturn myModule-- 在另一个文件中使用该模块
local myModule = require("myModule")
myModule.sayHello() -- 输出: Hello from module!
与 C/C++ 集成
Lua 设计之初就考虑到了与其他语言的互操作性,特别是 C 和 C++。通过 Lua 提供的 C API,可以轻松地将 Lua 脚本嵌入到 C/C++ 应用程序中,或者反过来从 Lua 调用 C/C++ 编写的函数。
示例:简单的 Lua 脚本
下面是一个简单的 Lua 脚本示例,它定义了一个函数,然后调用这个函数并打印输出。
-- 定义一个函数
function add(a, b)return a + b
end-- 调用函数并打印结果
result = add(3, 5)
print(result) -- 输出: 8
Redis持久化策略
Redis的持久化策略主要包括**RDB(快照持久化)和AOF( append-only file)**两种方式,核心要点如下:
-
RDB(基于快照)
- 原理:在指定时间间隔内将内存数据快照写入磁盘,生成
.rdb
文件。 - 优点:文件紧凑,适合全量备份;恢复速度快,适合灾难恢复。
- 缺点:可能丢失快照间隔内的数据;生成快照时CPU占用较高。
- 触发:可配置
save
指令(如save 900 1
)或手动执行SAVE/BGSAVE
。
- 原理:在指定时间间隔内将内存数据快照写入磁盘,生成
-
AOF(基于日志追加)
- 原理:将每个写操作以协议格式追加到
.aof
文件,记录所有写命令。 - 优点:支持
appendfsync
策略(always/每秒/无)平衡性能与安全性,数据丢失风险更低。 - 缺点:文件体积较大;需定期重写(AOF Rewrite)压缩。
- 触发:实时记录写命令,通过
bgrewriteaof
优化文件。
- 原理:将每个写操作以协议格式追加到
-
混合策略
- 组合使用:同时开启RDB和AOF,利用RDB快速恢复和AOF数据可靠性,但需权衡性能。
- 配置建议:根据业务需求选择:
- 高性能低损耗:优先RDB+合理间隔。
- 数据强一致性:AOF+
appendfsync everysec
(每秒同步)。
核心目标:在数据持久性、性能损耗和恢复效率间平衡,确保数据安全与系统稳定性。
Redis内存淘汰策略
Redis的内存淘汰策略是一种用于管理Redis实例中数据生命周期的机制,当Redis使用的内存达到预设的最大限制时,这些策略决定了哪些键值对应该被删除以释放空间。以下是对Redis内存淘汰策略的具体介绍:
- noeviction:这是Redis的默认策略(≥v3.0)。当内存使用达到最大限制时,Redis会拒绝新的写入操作,并返回错误,此时只响应读操作。这种策略适用于数据保留非常重要且不能丢失的场景,或者在内存充足的环境下使用。
- allkeys-lru:在所有键中使用LRU(最近最少使用)算法进行淘汰。Redis会维护一个近似的LRU列表,虽然不完全精确,但对大多数使用场景来说是足够的。这种策略适用于缓存应用,其中需要保留最近被访问的数据以便快速响应后续的读取请求。
- allkeys-lfu:在所有键中使用LFU(最不经常使用)算法进行淘汰。LFU算法根据键的访问频率来淘汰数据,访问次数最少的键优先被淘汰。这种策略适用于有明显热点数据的应用场景,可以确保热点数据不被轻易淘汰。
- volatile-lru:仅在设置了过期时间的键中,基于LRU算法淘汰数据。这种策略适用于部分数据有时效性要求的场景,只针对设置了过期时间的键进行淘汰。
- volatile-lfu:仅在设置了过期时间的键中,基于LFU算法淘汰数据。同样只针对设置了过期时间的键,但淘汰依据是访问频率。
- allkeys-random:随机从所有key中淘汰数据。这种策略适用于对数据淘汰无特定要求的场景。
- volatile-random:随机从设置了ttl key中淘汰数据。只针对设置了过期时间的键进行随机淘汰。
- volatile-ttl:根据键的剩余过期时间进行淘汰,越早过期的键越先被淘汰。这种策略适用于缓存数据时效性要求严格的场景。
总的来说,Redis的内存淘汰策略提供了多种方式来管理和优化内存使用,用户可以根据具体需求选择合适的策略。在实际使用中,还需要注意监控Redis的内存使用情况,并根据需要调整相关参数以优化性能和资源利用。
什么是哈希桶
存储某个数的最终哈希值,但是哈希值或者说哈希码应该是一个序列,这个序列需要被映射到一个链表或者数组下标,直接映射可能会使得空间利用效率很低。JDK HashMap中是通过 hash&(length-1)来映射到目标下标,这个下标就被称为哈希桶,保证最终哈希桶在 容量之内。
缓存三件套
缓存三件套指的是数据访问越过缓存直接访问数据库的三种情况:
-
缓存击穿:当热Key过期时,大量请求访问数据库
- 解决办法:互斥锁 / 永不过期 / 双缓存
-
缓存穿透:访问不存在的数据
- 解决办法:Bool(布隆)过滤器,用多个哈希槽确定某个key是否存在,有小几率误判。 / 缓存数据
-
缓存雪崩:多个Key过期时,大量请求访问数据库
- 解决办法:随机TTL、多级缓存、限流降级、集群
那么如何记忆呢? 顾名思义即可。击穿就是 多发子弹击穿标靶(热key过期),穿透就是 穿过了掩体(访问缓存不存在的数据),雪崩(好多掩体没了)大量key过期。
什么是扰动函数,哪些设计考虑到了扰动函数
扰动函数的定义与设计应用
1. 扰动函数的定义
扰动函数(Disturbance Function)在不同领域有不同的含义,以下是两种主要场景的解释:
(1) 化学中的“扰动力函数”(Forging Function)
根据知识库中的化学术语描述:
- 定义:扰动力函数是描述化学反应平衡受扰动时的数学表达式。
- 核心思想:
- 化学反应体系在宏观约束条件(如温度、压力、浓度等)固定时,存在稳定的化学平衡。
- 当外部条件改变(如添加反应物、改变温度等),平衡被打破,系统会从原平衡状态向新平衡状态过渡。
- 扰动力函数通过量化浓度偏移(如
ΔC = C - C_eq
)和反应动力学参数,描述这一动态过程。
- 数学表达:
- 根据化学弛豫原理,扰动力函数(如
Δ
)可以将高阶速率方程简化为一级反应动力学形式:
[
-\frac{dI}{dt} = \Delta - \frac{\Delta_e}{\tau}
] - 其中,
Δ
表示扰动引起的浓度偏移,τ
是弛豫时间常数。
- 根据化学弛豫原理,扰动力函数(如
(2) 计算机科学中的“扰动函数”
在编程和数据结构中,扰动函数通常指对哈希码(Hash Code)进行二次处理的算法,目的是优化哈希值的分布,减少哈希冲突。
Java HashMap 的扰动函数
根据知识库中的描述(CSDN博客内容):
- 作用:对对象的
hashCode()
值进行扰动处理,提高哈希值的均匀性。 - 实现原理:
- 步骤:
- 输入对象的
hashCode()
值(32位int
)。 - 高位与低位异或:将哈希码的高16位与低16位进行异或操作,打乱原始位分布:
int hash = hashCode(); hash ^= (hash >>> 16); // 高16位右移16位后与低16位异或
- 最终哈希值:通过
(n - 1) & hash
计算数组下标(n
为数组长度)。
- 输入对象的
- 设计目的:
- 减少哈希冲突:某些对象的
hashCode()
实现可能质量较低(如只使用低几位),扰动函数通过混合高低位,增强低位的随机性。 - 均匀分布:确保哈希值在数组索引范围内更均匀地分布,避免链表过长导致查询性能下降。
- 减少哈希冲突:某些对象的
- 步骤:
2. 设计中考虑扰动函数的场景
(1) 哈希表(如 HashMap)的设计
- 问题:哈希冲突(不同对象的哈希码映射到同一索引)可能导致链表过长,降低查询效率。
- 解决方案:
- 扰动函数:通过位操作(如异或、移位)对哈希码进行二次处理,提升分布均匀性。
- 扩展机制:当链表长度超过阈值(如8),转换为红黑树(JDK 1.8+),进一步优化查询效率。
- 知识库引用:
“HashMap的hash方法也就是扰动函数是为了防止一些实现比较差hashCode()方法…减少碰撞。”
(2) 缓存系统的设计(如 Redis)
- 问题:缓存雪崩(大量缓存同时失效,导致数据库压力激增)。
- 解决方案:
- 随机过期时间(扰动 TTL):在设置缓存过期时间时,对基础时间(如
24小时
)进行随机扰动(如 ±10%),避免大量缓存同时失效。 - 设计目的:通过时间分布的扰动,平滑请求峰值,防止数据库雪崩。
- 随机过期时间(扰动 TTL):在设置缓存过期时间时,对基础时间(如
- 知识库关联:
“随机过期时间(Random TTL)…避免key同时过期”(缓存雪崩解决方案)。
(3) 密码学与安全协议
- 场景:生成随机数或密钥时,避免可预测性。
- 扰动函数应用:
- 对种子值进行扰动(如异或系统时间、内存地址等),增加随机性,防止被破解。
(4) 机器学习与优化算法
- 场景:优化模型训练时的参数初始化或梯度下降路径。
- 扰动函数应用:
- 在初始参数或梯度方向中引入随机扰动,避免陷入局部最优解。
3. 设计扰动函数的核心考量
无论在哪个领域,设计扰动函数时通常需要考虑以下原则:
- 均匀性:确保扰动后的结果分布更均匀。
- 不可预测性:避免被恶意利用(如哈希攻击或缓存穿透)。
- 计算效率:扰动操作需简单高效,不影响系统性能。
- 适应性:能应对不同场景的输入(如哈希码的多样性或缓存失效模式)。
HashMap中的精妙设计
HashMap的精妙设计体现在以下几个核心点:
- 哈希与扰动:通过
hashCode()
计算键的哈希值,并采用高位异或低位的扰动函数(如hash = hash ^ (hash >>> 16)
),提升哈希值分布均匀性,减少冲突。 - 链表与红黑树:当链表长度超过阈值(默认8)时,自动转为红黑树,将查询时间复杂度从O(n)降至O(log n),平衡存储与效率。
- 动态扩容:容量达到阈值(负载因子默认0.75)时扩容为原容量的2倍,通过分摊机制保证均摊O(1)复杂度;扩容时通过“雨水灌溉”法(交替分配元素)减少哈希碰撞。
- 键值特性:允许一个null键和多个null值,键的
equals()
与hashCode()
确保唯一性,值可自由存储。 - 性能优化:通过高位运算和结构化存储(数组+链表/红黑树)实现高效存取,兼顾空间与时间,成为Java中最常用的键值对容器。
ConcurrentHashMap的优化
ConcurrentHashMap
是 Java 并发编程中非常重要的一个线程安全的哈希表实现。相比传统的 Hashtable
或通过 Collections.synchronizedMap
包装的 HashMap
,ConcurrentHashMap
提供了更高的并发性能和更好的扩展性。以下是 ConcurrentHashMap
的优化设计思想与实现细节。
1. 分段锁(Segment Locking)机制(Java 7 及之前)
基本思想
在 Java 7 中,ConcurrentHashMap
使用了分段锁机制来实现线程安全。整个哈希表被划分为多个段(Segment),每个段独立加锁。这样,不同线程可以同时操作不同的段,从而减少锁的竞争。
工作原理
- 每个 Segment 是一个独立的小哈希表。
- 写操作时,只需锁定相关的 Segment,而不是整个哈希表。
- 读操作通常是无锁的,因为数据本身是线程安全的。
优点
- 减少了锁的粒度,提升了并发性能。
- 支持一定程度的并行写操作。
缺点
- Segment 数量固定(默认为 16),无法动态调整,可能限制扩展性。
- 在高并发场景下,某些热点 Segment 仍可能成为瓶颈。
2. CAS + 细粒度锁(Java 8 及之后)
Java 8 对 ConcurrentHashMap
进行了重大改进,摒弃了分段锁机制,转而使用更高效的 CAS(Compare-And-Swap)操作和细粒度锁。
优化点
-
基于 Node 的链表和红黑树
- 每个桶(Bucket)由一个
Node
表示,存储键值对。 - 当链表长度超过阈值(默认为 8)时,会将链表转换为红黑树,以提高查找效率。
- 每个桶(Bucket)由一个
-
CAS 操作
- 插入、更新等操作尽量使用无锁的 CAS 操作完成。
- 如果 CAS 失败(说明有竞争),再升级为锁。
-
细粒度锁
- 锁定的是具体的桶(Node),而不是整个段或哈希表。
- 不同线程可以同时操作不同的桶,进一步提升了并发性能。
-
动态扩容
- 扩容时采用分批迁移的方式,避免一次性迁移所有数据导致的性能问题。
- 每次只迁移一部分桶的数据,其他线程可以继续访问未迁移的部分。
3. 读操作的优化
无锁读
- 读操作不涉及锁,直接从哈希表中获取数据。
- 即使在扩容过程中,读操作仍然可以通过旧表和新表协作完成。
volatile 保证可见性
- 哈希表中的值使用
volatile
修饰,确保多线程间的可见性。 - 这样,即使没有显式的锁,读线程也能看到最新的写入结果。
4. 写操作的优化
CAS 和锁结合
- 写操作优先尝试使用 CAS 完成,只有在 CAS 失败时才加锁。
- 这种方式减少了锁的使用频率,降低了线程阻塞的可能性。
批量插入
- 在初始化或批量插入数据时,
ConcurrentHashMap
会尽量减少锁的争用,提升性能。
5. 扩容优化
渐进式扩容
- 扩容时,
ConcurrentHashMap
不会一次性迁移所有数据,而是逐步进行。 - 每次迁移一个桶的数据,其他线程可以继续访问未迁移的部分。
- 迁移完成后,旧表会被丢弃。
多线程协作
- 扩容过程中允许多个线程协作完成数据迁移,充分利用多核 CPU 的优势。
6. 其他优化
计数器优化
ConcurrentHashMap
使用了类似LongAdder
的思想来维护元素数量,避免了对全局计数器的频繁竞争。- 计数器被分成多个部分,每个部分独立更新,最终通过累加得到总数。
弱一致性迭代器
ConcurrentHashMap
提供了弱一致性的迭代器,允许在遍历时继续修改数据。- 迭代器不会抛出
ConcurrentModificationException
,但可能看不到最新的修改。
7. 性能对比
特性 | Hashtable | Collections.synchronizedMap | ConcurrentHashMap |
---|---|---|---|
线程安全性 | 是 | 是 | 是 |
锁粒度 | 整个表 | 整个表 | 桶级锁或无锁 |
并发性能 | 差 | 差 | 高 |
读操作是否加锁 | 是 | 是 | 否 |
扩容机制 | 全局锁 | 全局锁 | 渐进式扩容 |
8. 使用场景
ConcurrentHashMap
适用于以下场景:
- 高并发环境:需要频繁地进行读写操作,且要求较高的并发性能。
- 统计与计数:例如统计访问次数、记录事件等。
- 缓存系统:作为线程安全的缓存存储结构。
9. 注意事项
-
弱一致性
ConcurrentHashMap
的迭代器和一些方法(如size()
)提供的是弱一致性,可能无法反映最新的状态。
-
不支持 null 键或值
ConcurrentHashMap
不允许插入null
键或值,以避免歧义。
-
内存消耗
- 由于采用了红黑树和细粒度锁等机制,
ConcurrentHashMap
的内存开销可能比普通HashMap
更高。
- 由于采用了红黑树和细粒度锁等机制,
总结
ConcurrentHashMap
的优化主要体现在以下几个方面:
- 分段锁到细粒度锁:从粗粒度的段锁进化为细粒度的桶锁,甚至无锁操作。
- CAS 操作:减少了锁的使用,提升了并发性能。
- 动态扩容:采用渐进式扩容和多线程协作,避免了全表锁。
- 数据结构优化:链表转红黑树,提升了查询效率。
这些优化使得 ConcurrentHashMap
成为了高并发场景下最常用的线程安全哈希表实现之一。
LongAdder思想
LongAdder
是 Java 8 引入的一个类,位于 java.util.concurrent.atomic
包中。它主要用于高并发环境下高效地执行计数操作。LongAdder
的设计理念旨在解决在高竞争条件下 AtomicLong
的性能瓶颈问题。
1. 基本思想
分段锁机制(Striped Locking)
- 基本概念:
LongAdder
采用了一种称为“分段”的策略来减少线程间的竞争。它将计数器内部划分为多个部分(或称段),每个部分可以独立进行加减操作。 - 原理:当有多个线程尝试更新计数值时,它们会被分配到不同的段上进行操作,从而减少了直接的竞争和锁的争用。最终的结果是通过累加所有段的值得到的。
惰性初始化(Lazy Initialization)
LongAdder
在创建时不会立即分配所有的段,而是根据实际需要动态增加段的数量。这种惰性初始化的方式有助于节省内存资源。
2. 工作流程
- 初始状态:
LongAdder
初始化时只有一个基础单元(base unit),此时的操作类似于AtomicLong
。 - 低竞争情况:如果当前只有少量线程或者没有竞争发生,所有的增减操作都会直接作用于这个基础单元。
- 高竞争情况:一旦检测到较高的竞争度(例如,多个线程试图同时修改同一个基础单元),
LongAdder
会创建额外的单元,并将新的增减请求分散到这些新创建的单元上。 - 结果汇总:当需要获取总计数值时,
LongAdder
会对所有单元(包括基础单元和其他附加单元)的值进行求和。
3. 优势与劣势
优势
- 高并发性能:相较于
AtomicLong
,在高并发场景下,LongAdder
能够提供更好的性能表现,因为它减少了线程之间的竞争。 - 可扩展性:随着竞争加剧,
LongAdder
可以自动扩展其内部结构以适应更高的并发量。
劣势
- 空间成本:为了支持多线程并行操作,
LongAdder
需要维护更多的内部状态,这可能会导致比AtomicLong
更高的内存消耗。 - 复杂性增加:由于引入了分段机制,逻辑相对复杂了一些,但这主要是对开发者透明的,用户无需关心其实现细节。
4. 使用场景
LongAdder
特别适用于那些读写频繁且对精确度要求不是特别严格的统计场景,如:
- 统计网站访问次数
- 监控系统中的指标收集
- 并发任务完成数量统计等
对于不需要实时精确计数的应用场景,LongAdder
提供了一个非常有效的解决方案。然而,在需要精确一致性的场合(比如财务计算),可能仍需选择 AtomicLong
或其他同步机制。