From 7f426ccfdfb7c25886b0ea397486110b102594ec Mon Sep 17 00:00:00 2001 From: "guangxin.yuan" Date: Tue, 13 May 2025 19:31:08 +0800 Subject: [PATCH] update --- Rocket.md | 15 +++ src/Solution.java | 81 ++++++++++++++++ src/其他/lru实现/LRUCache.java | 93 +++++++++++++++++++ .../查找第k大的数字/QuickSelect.java | 47 ++++++++++ 4 files changed, 236 insertions(+) create mode 100644 src/Solution.java create mode 100644 src/其他/lru实现/LRUCache.java create mode 100644 src/其他/查找第k大的数字/QuickSelect.java diff --git a/Rocket.md b/Rocket.md index 52d8740..add3805 100644 --- a/Rocket.md +++ b/Rocket.md @@ -125,6 +125,14 @@ Redis中setnx不支持设置过期时间,做分布式锁时要想避免某一 1. 服务端缓存:即将热点数据缓存至服务端的内存中.(利用Redis自带的消息通知机制来保证Redis和服务端热点Key的数据一致性,对于热点Key客户端建立一个监听,当热点Key有更新操作的时候,服务端也随之更新。) 2. 备份热点Key:即将热点Key+随机数,随机分配至Redis其他节点中。这样访问热点key的时候就不会全部命中到一台机器上了。 +### Redis的RedLock算法 + +1. RedLock算法‌是由Redis的作者Salvatore Sanfilippo(也称为Antirez)提出的一种分布式锁的实现方案,旨在解决在分布式系统中实现可靠锁的问题,特别是解决单个Redis实例作为分布式锁时可能出现的单点故障问题。 +2. 过程1:‌获取当前时间戳‌:客户端在尝试获取锁时,首先记录当前的时间戳t0。 +3. 过程2:‌在多个Redis实例上尝试获取锁‌:客户端会向N个(通常建议是奇数个,如5个)独立的Redis实例发送请求,尝试获取相同的锁。每个请求都会设置相同的锁名、唯一标识符(如UUID)和过期时间。 +4. 过程3:‌统计获取锁成功的实例数量‌:客户端统计在多少个Redis实例上成功获取了锁。 +5. 过程4:‌判断锁是否获取成功‌:如果在过半数(即N/2+1)的Redis实例上成功获取了锁,并且从获取第一个锁到最后一个锁的总时间小于锁的过期时间的一半,那么认为锁获取成功。否则,认为锁获取失败,客户端需要释放已经获取的锁(如果有的话)‌ + ### 如何解决 Redis 缓存雪崩问题 1. 使用 Redis 高可用架构:使用 Redis 集群来保证 Redis 服务不会挂掉 @@ -834,6 +842,13 @@ Spring使用了三级缓存解决了循环依赖的问题。在populateBean()给 5. 异常被 catch 捕获导致@Transactional失效。 6. 数据库引擎不支持事务。 +### @Transactional配合分布式锁的使用 +1. 在分布式系统中,先获取分布式锁还是先开始事务,取决于具体的应用场景和业务需求。这两种方式各有优缺点,选择哪种方式需要根据实际需求权衡。 +2. 先获取分布式锁,再开始事务:如果锁的持有者在事务中发生故障,可能会导致锁无法释放,从而引发死锁。如果锁的粒度过大,可能会降低系统的并发性能。 +3. 先获取分布式锁,再开始事务适用场景:需要严格互斥访问共享资源的场景如对数据库中的某个表或某个记录进行更新操作,操作复杂且需要多个步骤的场景 +4. 先开始事务,再获取分布式锁:这种方式较少见,通常用于一些特殊的场景,例如事务的执行时间非常短,或者锁的获取成本较高。需要在事务中处理锁的获取和释放逻辑,增加了实现的复杂性。如果事务在获取锁之前提交,可能会导致数据不一致。 +5. 先开始事务,再获取分布式锁适用场景:事务执行时间非常短的场景:例如,简单的数据库更新操作。锁的获取成本较高的场景:例如,锁服务的响应时间较长,或者锁的粒度非常细。 + ### Spring中的事务传播机制 1. REQUIRED(默认,常用):支持使用当前事务,如果当前事务不存在,创建一个新事务。eg:方法B用REQUIRED修饰,方法A调用方法B,如果方法A当前没有事务,方法B就新建一个事务(若还有C则B和C在各自的事务中独立执行),如果方法A有事务,方法B就加入到这个事务中,当成一个事务。 diff --git a/src/Solution.java b/src/Solution.java new file mode 100644 index 0000000..ce72957 --- /dev/null +++ b/src/Solution.java @@ -0,0 +1,81 @@ +public class Solution { + + // 找到旋转数组的最小值位置 + private static int findMinIndex(int[] nums) { + int left = 0; + int right = nums.length - 1; + + while (left < right) { + int mid = left + (right - left) / 2; + if (nums[mid] > nums[right]) { + left = mid + 1; + } else { + right = mid; + } + } + return left; // 最小值的位置 + } + + // 快速选择算法(类似快速排序的分区操作) + private static int quickSelect(int[] nums, int start, int end, int k) { + if (start == end) { + return nums[start]; + } + + int pivot = partition(nums, start, end); + + if (pivot == k) { + return nums[pivot]; + } else if (pivot > k) { + return quickSelect(nums, start, pivot - 1, k); + } else { + return quickSelect(nums, pivot + 1, end, k); + } + } + + // 快速排序的分区操作 + private static int partition(int[] nums, int start, int end) { + int pivot = nums[end]; + int i = start - 1; + + for (int j = start; j < end; j++) { + if (nums[j] >= pivot) { // 从大到小排序 + i++; + swap(nums, i, j); + } + } + + swap(nums, i + 1, end); + return i + 1; + } + + // 交换数组中的两个元素 + private static void swap(int[] nums, int i, int j) { + int temp = nums[i]; + nums[i] = nums[j]; + nums[j] = temp; + } + + // 主函数:在旋转数组中找到第 k 大的数字 + public static int findKthLargest(int[] nums, int k) { + int minIndex = findMinIndex(nums); // 找到最小值的位置 + int n = nums.length; + + // 将数组分为两部分,分别查找第 k 大的数字 + if (k <= n - minIndex) { + // 第 k 大的数字在后半部分 + return quickSelect(nums, minIndex, n - 1, k - 1); + } else { + // 第 k 大的数字在前半部分 + return quickSelect(nums, 0, minIndex - 1, k - (n - minIndex) - 1); + } + } + + public static void main(String[] args) { + int[] nums = {4, 5, 6, 7, 0, 1, 2}; + int k = 3; + + int result = findKthLargest(nums, k); + System.out.println("第 " + k + " 大的数字是: " + result); + } +} \ No newline at end of file diff --git a/src/其他/lru实现/LRUCache.java b/src/其他/lru实现/LRUCache.java new file mode 100644 index 0000000..91228f5 --- /dev/null +++ b/src/其他/lru实现/LRUCache.java @@ -0,0 +1,93 @@ +package 其他.lru实现; + +import java.util.HashMap; +import java.util.Map; + +public class LRUCache { + class DLinkedNode { + int key; + int value; + DLinkedNode prev; + DLinkedNode next; + + public DLinkedNode() { + } + + public DLinkedNode(int _key, int _value) { + key = _key; + value = _value; + } + } + + private Map cache = new HashMap<>(); + private int size; + private int capacity; + private DLinkedNode head, tail; + + public LRUCache(int capacity) { + this.size = 0; + this.capacity = capacity; + // 使用伪头部和伪尾部节点 + head = new DLinkedNode(); + tail = new DLinkedNode(); + head.next = tail; + tail.prev = head; + } + + public int get(int key) { + DLinkedNode node = cache.get(key); + if (node == null) { + return -1; + } + // 如果 key 存在,先通过哈希表定位,再移到头部 + moveToHead(node); + return node.value; + } + + public void put(int key, int value) { + DLinkedNode node = cache.get(key); + if (node == null) { + // 如果 key 不存在,创建一个新的节点 + DLinkedNode newNode = new DLinkedNode(key, value); + // 添加进哈希表 + cache.put(key, newNode); + // 添加至双向链表的头部 + addToHead(newNode); + ++size; + if (size > capacity) { + // 如果超出容量,删除双向链表的尾部节点 + DLinkedNode tail = removeTail(); + // 删除哈希表中对应的项 + cache.remove(tail.key); + --size; + } + } else { + // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部 + node.value = value; + moveToHead(node); + } + } + + private void addToHead(DLinkedNode node) { + node.prev = head; + node.next = head.next; + head.next.prev = node; + head.next = node; + } + + private void removeNode(DLinkedNode node) { + node.prev.next = node.next; + node.next.prev = node.prev; + } + + private void moveToHead(DLinkedNode node) { + removeNode(node); + addToHead(node); + } + + private DLinkedNode removeTail() { + DLinkedNode res = tail.prev; + removeNode(res); + return res; + } +} diff --git a/src/其他/查找第k大的数字/QuickSelect.java b/src/其他/查找第k大的数字/QuickSelect.java new file mode 100644 index 0000000..076c1de --- /dev/null +++ b/src/其他/查找第k大的数字/QuickSelect.java @@ -0,0 +1,47 @@ +package 其他.查找第k大的数字; + +public class QuickSelect { + public static int findKthLargest(int[] nums, int k) { + return quickSelect(nums, 0, nums.length - 1, nums.length - k); + } + + private static int quickSelect(int[] nums, int left, int right, int kSmallest) { + if (left == right) { // 如果只剩一个元素,那么它就是第k小的 + return nums[left]; + } + + int pivotIndex = partition(nums, left, right); + + if (kSmallest == pivotIndex) { + return nums[kSmallest]; + } else if (kSmallest < pivotIndex) { + return quickSelect(nums, left, pivotIndex - 1, kSmallest); + } else { + return quickSelect(nums, pivotIndex + 1, right, kSmallest); + } + } + + private static int partition(int[] nums, int left, int right) { + int pivot = nums[right]; // 选择最右边的元素作为pivot + int i = left; // i是小于pivot的元素的最后一个位置 + for (int j = left; j < right; j++) { + if (nums[j] < pivot) { + swap(nums, i++, j); + } + } + swap(nums, i, right); // 把pivot放到中间位置 + return i; // 返回pivot的正确位置 + } + + private static void swap(int[] nums, int i, int j) { + int temp = nums[i]; + nums[i] = nums[j]; + nums[j] = temp; + } + + public static void main(String[] args) { + int[] nums = {3, 2, 1, 5, 6, 4}; + int k = 2; // 找第2大的数字(即第k大的数字) + System.out.println("第 " + k + " 大的数字是: " + findKthLargest(nums, k)); // 输出应该是5 + } +}