面试题第三季

面试题第三季

直面困难, 迎难而上, 要想着太阳底下没有新鲜事儿, 过了这个砍就好了, 一年后的今天回头看看都不是事儿, 做其他事儿说实话都是在逃避.

1.Java基础

1.58同城的java字符串常量池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.atguigu.javase;

public class StringPool58Demo {
public static void main(String[] args) {
String str1 = new StringBuilder("58").append("tongcheng").toString();
System.out.println(str1);
System.out.println(str1.intern());
System.out.println(str1 == str1.intern());

System.out.println();

String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2);
System.out.println(str2.intern());
System.out.println(str2 == str2.intern());
}
}

答案是

58tongcheng
58tongcheng
true
java
java
false

只有java是false, 其他都是true

java8没有永久代, 叫元空间

按照代码结果, java字符串为false
必然是两个不同的java, 那另外一个java字符串如何加载进来的?
有一个初始化的java字符串(JDK出娘胎自带的), 在加载sun.misc.Version这个类的时候进入常量池

sun.misc.Version.init();

一道练习题

1
2
3
4
5
6
7
String s1 = new String("11"); // 创建了String对象, 和 常量"11" 两个对象
String s2 = s1.intern();
String s3 = "11";

System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s2 == s3);

答案是 false false true
1
2
3
4
5
String s4 = String.valueOf(22); // 只构造了一个String对象
String s5 = s4.intern(); // 常量池中保存的是String对象的引用地址
String s6 = "22";
System.out.println(s4 == s5);
System.out.println(s4 == s6);

答案是 true true

2.字节跳动两数求和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package com.atguigu.javase;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
* 两数之和: https://leetcode.cn/problems/two-sum/
* 给定一个整数数组 nums 和一个整数目标值 target, 请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
* <p>
* 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
* <p>
* 你可以按任意顺序返回答案。
* <p>
* 示例 1:
* <p>
* 输入:nums = [2,7,11,15], target = 9
* 输出:[0,1]
* 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
* 示例 2:
* <p>
* 输入:nums = [3,2,4], target = 6
* 输出:[1,2]
* 示例 3:
* <p>
* 输入:nums = [3,3], target = 6
* 输出:[0,1]
* <p>
* 提示:
* <p>
* 2 <= nums.length <= 104
* -109 <= nums[i] <= 109
* -109 <= target <= 109
* 只会存在一个有效答案
*/
public class TwoSumDemo {
public static void main(String[] args) {
int[] nums = new int[]{2, 7, 11, 15};
int target = 9;
System.out.println(Arrays.toString(new TwoSumDemo().twoSum1(nums, target)));
System.out.println(Arrays.toString(new TwoSumDemo().twoSum2(nums, target)));

nums = new int[]{3, 2, 4};
target = 6;
System.out.println(Arrays.toString(new TwoSumDemo().twoSum1(nums, target)));
System.out.println(Arrays.toString(new TwoSumDemo().twoSum2(nums, target)));
}

public int[] twoSum1(int[] nums, int target) {
for (int i = 0; i < nums.length - 1; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] == target) {
return new int[]{i, j};
}
}
}
return new int[]{0};
}

public int[] twoSum2(int[] nums, int target) {
Map<Integer , Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int key = target - nums[i];
if (map.containsKey(key)) {
return new int[]{map.get(key), i};
}
map.put(nums[i], i);
}
return null;
}
}

3.字节跳动手写LRU算法

2.JUC

1.可重入锁

1.1 介绍

可重入锁(又名递归锁)

可重入锁这四个字分开来解释:
1、可: 可以
2、重: 再次
3、入: 进入
4、锁: 同步锁
5、进入什么: 进入同步域(即同步代码块/方法或显示锁锁定的代码)
6、一句话: 一个线程中的多个流程可以获取同一把锁, 持有这把同步锁可以再次进入
自己可以获取自己的内部锁

1.2 种类

隐式锁

隐式锁(即synchronized关键字使用的锁)默认是可重入锁 (同步块, 同步方法)

同步块
同一个对象的锁, 不用等着释放
代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.atguigu.juc;

public class ReEnterLockDemo {
static Object objectLockA = new Object();

public static void m1() {
new Thread(() -> {
synchronized (objectLockA) {
System.out.println(Thread.currentThread().getName() + "\t外层调用");
synchronized (objectLockA) {
System.out.println(Thread.currentThread().getName() + "\t中层调用");
synchronized (objectLockA) {
System.out.println(Thread.currentThread().getName() + "\t内层调用");
}
}
}
}, "t1").start();
}

public static void main(String[] args) {
m1();
}
}

同步方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 可重入锁-synchronized-同步方法
private synchronized static void m1() {
System.out.println("外层");
m2();
}

private synchronized static void m2() {
System.out.println("中层");
m3();
}

private synchronized static void m3() {
System.out.println("内层");
}

public static void main(String[] args) {
m1();
}
synchronized的重入的实现机理

synchronized的重入的实现机理:
monitorenter: 计数器+1
monitorexit: 计数器-1

显示锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static Lock lock = new ReentrantLock();

public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
System.out.println("外层");
lock.lock();
try {
System.out.println("内层");
} finally {
lock.unlock();
}
} finally {
// 故意注释掉这行, 这里不释放锁, t2线程没法执行
// 由于加锁和释放次数不一样, 第二个线程始终无法获取到锁, 导致一直在等待
lock.unlock(); // 正常情况, 加锁几次, 就要解锁几次
}
}, "t1").start();

new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "调用开始");
} finally {
lock.unlock();
}
}, "t2").start();
}

2.LockSupport

2.1 LockSupport是什么?

java.util.concurrent.locks包下的一个类

LockSupport中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程

2.2 线程等待唤醒机制(wait/notify)

3种让线程等待和唤醒的方法

1.使用Object中的wait()方法让线程等待, 使用Object中的notify()方法唤醒线程
2.使用JUC包中Condition的await()方法让线程等待, 使用signal()方法唤醒线程
3.LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

Object类中的wait和notify方法实现线程等待和唤醒
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.atguigu.juc;

public class LockSupportDemo {
static Object objectLock = new Object();

public static void main(String[] args) {
new Thread(() -> {
// try {
// Thread.sleep(3);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }

synchronized (objectLock) { // 必须在同步代码块中, 否则 java.lang.IllegalMonitorStateException
System.out.println(Thread.currentThread().getName() + "\t come in");
try {
objectLock.wait(); // 必须先wait了才能notify
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "\t 被唤醒");
}
}, "A").start();
new Thread(() -> {
synchronized (objectLock) { // 必须在同步代码块中, 否则 java.lang.IllegalMonitorStateException
objectLock.notify();
System.out.println(Thread.currentThread().getName() + "\t 通知");
}
}, "B").start();
}
}

wait方法和notify方法, 必须在同步代码块, 否抛出异常 java.lang.IllegalMonitorStateException

wait和notify方法必须要在同步块或者方法里面且成对出现使用
先wait后notify才OK

Condition接口中的await和signal方法实现线程的等待和唤醒
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.atguigu.juc;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockSupportDemo {
static Object objectLock = new Object();
static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();

public static void main(String[] args) {

new Thread(() -> {
// try {
// TimeUnit.SECONDS.sleep(3L);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }

lock.lock();
try {
// 必须在同步代码块中, 否则 java.lang.IllegalMonitorStateException
System.out.println(Thread.currentThread().getName() + "\t come in");
try {
condition.await(); // 必须先await了才能signal
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "\t 被唤醒");
} finally {
lock.unlock();
}
}, "A").start();

new Thread(() -> {
lock.lock();
try {
// 必须在同步代码块中, 否则 java.lang.IllegalMonitorStateException
condition.signal();
System.out.println(Thread.currentThread().getName() + "\t 通知");
} finally {
lock.unlock();
}
}, "B").start();

// synchronizedWaitNotify();
}
}

await()方法和signal()方法, 必须放在同步代码块, 否抛出异常 java.lang.IllegalMonitorStateException
await和signal方法必须要在同步块或者方法里面, 可以不成双成对, 多出来的await()交给signalAll()解决
先await后signal才OK

传统的synchronized和Lock实现等待唤醒通知的约束

线程先要获得并持有锁, 必须在锁块(synchronized或lock)中
必须要等待后唤醒, 线程才能够被唤醒

LockSupport类中的park等待和unpark唤醒

是什么?
通过park()unpark(thread)方法来实现阻塞和唤醒线程的操作
官网解释:

主要方法
阻塞 park() / park(Object blocker)
阻塞当前线程/阻塞传入的具体线程

唤醒 unpark(Thread)
唤醒处于阻塞状态的指定线程

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static void main(String[] args) {

Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "\t come in\t" + System.currentTimeMillis());
// sleep方法3秒后醒来, 执行park无效, 没有阻塞效果, 解释如下
// 先执行了unpark(t1)导致上面park方法形同虚设无效, 时间一样
LockSupport.park();
// LockSupport.park();
System.out.println(Thread.currentThread().getName() + "\t 被唤醒\t" + System.currentTimeMillis());
}, "t1");
t1.start();

// try {
// TimeUnit.SECONDS.sleep(3L);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }

Thread t2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 通知");
LockSupport.unpark(t1);
// LockSupport.unpark(t1); 最多解开一个park, 因为permit最大值就是1, 调用多次都是1
}, "t2");
t2.start();


// lockAwaitSignal();

// synchronizedWaitNotify();
}

LockSupport不需要同步代码块包裹
也没有part(), unpark()的先后顺序, 但是多个unpark()只能解开一个park()
unpark获得了凭证, permit变成1, 最大为1, 多次调用unpark无效, 最多解开一个park

重点说明(重要)

面试题

3.AbstractQueueSynchronized之AQS

是什么?

字面意思: 抽象的队列同步器
源代码:

技术解释: 是用来构建锁或者其他同步器组件的重量级基础框架级整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作, 并通过一个int类型变量表示持有锁的状态

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.atguigu.juc;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class AQSDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();

// 带入一个银行办理业务的案例来模拟我们的AQS如何进行线程的管理和通知唤醒机制

// 3个线程模拟3个银行网点, 受理窗口办理业务的顾客

// A 顾客就是第一个顾客, 此时手里窗口没有任何人, A可以直接去办理
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t come in");
try {
TimeUnit.MINUTES.sleep(3L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} finally {
lock.unlock();
}
}, "A").start();

// 第2个顾客, 第2个线程 由于受理业务的窗口只有一个(只能一个线程持有锁), 此时B只能等待
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t come in");
try {
TimeUnit.MINUTES.sleep(3L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} finally {
lock.unlock();
}
}, "B").start();

// 第3个顾客, 第3个线程 由于受理业务的窗口只有一个(只能一个线程持有锁), 此时C只能等待
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t come in");
try {
TimeUnit.MINUTES.sleep(3L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} finally {
lock.lock();
}
}, "C").start();
}
}

AQS为什么是JUC内容中最重要的基石

和AQS有关的

ReentrantLock
CountDownLatch
ReentrantReadWriteLock
Semaphore

进一步理解锁与同步器的关系
锁, 面向锁的使用者, 定义了程序员和锁交互的使用层API, 隐藏了实现细节, 你调用即可
同步器, 面向锁的实现者, 比如Java并发大神DougLee, 提出统一规范并简化了锁的实现, 屏蔽了同步状态管理, 阻塞线程排队和通知, 唤醒机制等

能干嘛

加锁会导致阻塞
有阻塞就需要排队, 实现排队必然需要有某种形式的队列来进行管理
解释说明

AQS初步

AQS初识

官网解释

有阻塞就需要排队, 实现排队必然需要队列
AQS使用一个volatile的int类型的成员变量来表示同步状态, 通过内置的FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node来实现锁的分配, 通过CAS完成对State值的修改

AQS内部体系架构

AQS内部体系架构

AQS自身:

1.AQS的int变量
AQS的同步状态State成员

1
2
3
4
/**
* The synchronization state.
*/
private volatile int state;

银行办理业务的受理窗口状态:
零就是没人, 自由状态可以办理
大于等于1, 有人占用窗口, 等着去

2.AQS的CLH队列

CLH(三个大牛的名字组成, 为一个双向队列)
银行候客区的等待顾客

3.小总结:
有阻塞就需要排队, 实现排队必然需要队列
state变量+CLH变种的双端队列

内部类Node(Node类在AQS类的内部)

1.Node的int变量
Node的等待waitState成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* Status field, taking on only the values:
* SIGNAL: The successor of this node is (or will soon be)
* blocked (via park), so the current node must
* unpark its successor when it releases or
* cancels. To avoid races, acquire methods must
* first indicate they need a signal,
* then retry the atomic acquire, and then,
* on failure, block.
* CANCELLED: This node is cancelled due to timeout or interrupt.
* Nodes never leave this state. In particular,
* a thread with cancelled node never again blocks.
* CONDITION: This node is currently on a condition queue.
* It will not be used as a sync queue node
* until transferred, at which time the status
* will be set to 0. (Use of this value here has
* nothing to do with the other uses of the
* field, but simplifies mechanics.)
* PROPAGATE: A releaseShared should be propagated to other
* nodes. This is set (for head node only) in
* doReleaseShared to ensure propagation
* continues, even if other operations have
* since intervened.
* 0: None of the above
*
* The values are arranged numerically to simplify use.
* Non-negative values mean that a node doesn't need to
* signal. So, most code doesn't need to check for particular
* values, just for sign.
*
* The field is initialized to 0 for normal sync nodes, and
* CONDITION for condition nodes. It is modified using CAS
* (or when possible, unconditional volatile writes).
*/
volatile int waitStatus;

说人话
等候区其他顾客(其他线程)的等待状态
队列中每个排队的个体就是一个Node

2.Node此类的讲解

内部结构

属性说明

AQS同步队列的基本结构

从我们的ReentrantLock开始解读AQS

Lock接口的实现类, 基本都是通过聚合了一个队列同步器的子类完成线程访问控制的

ReentrantLock的原理
ReentrantLock的原理

从最简单的lock方法开始看看公平和非公平

非公平锁走起, 方法lock()

lock()

addWaiter(Node mode)

enq(node);
双向链表中, 第一个节点为虚节点(也叫哨兵节点), 其实并不存储任何信息, 只是占位.
真正的第一个有数据的节点, 是从第二个节点开始的.

加入3号ThreadC线程进来 prev compareAndSetTail next

源码和3大流程走向

本次走非公平锁

nonfairTryAcquire(int acquires)

return false , 继续推进条件, 走下一个方法addWaiter
return true , 结束

acquireQueued

加入再抢抢失败就会进入
shouldParkAfterFailedAcquire 和 parkAndCheckInterrupt 方法中

方法unlock()
sync.release(1);
tryRelease(arg)
unparkSuccessor

3.Spring

Spring 中常见面试题: IOC AOP TX

1.Spring的AOP顺序

1.AOP常用注解

@Before 前置通知: 目标方法之前执行
@After 后置通知: 目标方法之后执行(始终执行)
@AfterReturning 返回后通知: 执行方法结束前执行(异常不执行)
@AfterThrowing 异常通知: 出现异常时执行
@Around 环绕通知: 环绕目标方法执行

2.面试题

你肯定知道spring , 那说说aop的全部通知顺序
SpringBoot或SpringBoot2对aop的执行顺序影响?
说说你使用AOP中碰到的坑

3.业务类

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.atguigu</groupId>
<artifactId>sgg3</artifactId>
<version>1.0-SNAPSHOT</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<!-- <version>1.5.9.RELEASE</version〉 ch/qos/Logback/core/joran/spi/JoranException解决方案-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.1.3</version>
</dependency>

<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<version>1.1.3</version>
</dependency>

<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.3</version>
</dependency>

<!-- web+actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- SpringBoot与Redis整合依赖 -->
<!--
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
-->

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>

<!-- jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>

<!-- Spring Boot AOP技术-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!-- redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>

<!-- 一般通用基础配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

接口

1
2
3
4
5
6
package com.atguigu.spring.service;

public interface CalcService {
int div(int x, int y);
}

实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.atguigu.spring.service.impl;

import com.atguigu.spring.service.CalcService;
import org.springframework.stereotype.Service;

@Service
public class CalcServiceImpl implements CalcService {
@Override
public int div(int x, int y) {
int result = x / y;
System.out.println("CalcServiceImpl被调用了, 我们的计算结果: " + result);
return result;
}
}

主类

1
2
3
4
5
6
7
8
9
10
11
package com.atguigu.spring;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}

4.新建一个切面类 MyAspect 并未切面类新增两个注解

MyAspect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.atguigu.spring;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyAspect {
@Before("execution(public int com.atguigu.spring.service.impl.CalcServiceImpl.*(..))")
public void beforeNotify() {
System.out.println("********@Before我是前置通知");
}

@After("execution(public int com.atguigu.spring.service.impl.CalcServiceImpl.*(..))")
public void afterNotify() {
System.out.println("********@After我是后置通知");
}

@AfterReturning("execution(public int com.atguigu.spring.service.impl.CalcServiceImpl.*(..))")
public void afterReturningNotify() {
System.out.println("********@AfterReturning我是返回后通知");
}

@AfterThrowing(" execution(public int com.atguigu.spring.service.impl.CalcServiceImpl.*(..))")
public void afterThrowingNotify() {
System.out.println("********@AfterThrowing我是异常通知");
}

@Around(" execution(public int com.atguigu.spring.service.impl.CalcServiceImpl.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object retvalue = null;
System.out.println("我是环绕通知之前AAA");
retvalue = proceedingJoinPoint.proceed();
System.out.println("我是环绕通知之后BBB");
return retvalue ;
}
}

5.Spring4 + SpringBoot1.5.9

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.atguigu.spring;

import com.atguigu.spring.service.CalcService;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.SpringBootVersion;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.SpringVersion;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;

@SpringBootTest
@RunWith(SpringRunner.class)
public class TestAop {
@Resource
CalcService calcService;

@Test
public void testAop4() {
System.out.println("Spring版本: " + SpringVersion.getVersion());
System.out.println("springboot版本: " + SpringBootVersion.getVersion());

// calcService.div(10, 2);
/*
运行结果:
Spring版本: 4.3.13.RELEASE
springboot版本: 1.5.9.RELEASE
我是环绕通知之前AAA
********@Before我是前置通知
CalcServiceImpl被调用了, 我们的计算结果: 5
我是环绕通知之后BBB
********@After我是后置通知
********@AfterReturning我是返回后通知
*/

calcService.div(10, 0);
/*
运行结果:
Spring版本: 4.3.13.RELEASE
springboot版本: 1.5.9.RELEASE
我是环绕通知之前AAA
********@Before我是前置通知
********@After我是后置通知
********@AfterThrowing我是异常通知

java.lang.ArithmeticException: / by zero
*/


}
}

6.Spring5 + SpringBoot2.3.3

将pom.xml版本号修改成2.3.3.RELEASE

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.atguigu.spring;

import com.atguigu.spring.service.CalcService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringBootVersion;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.SpringVersion;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;

@SpringBootTest
// @RunWith(SpringRunner.class)
public class TestAop {
@Resource
CalcService calcService;

@Test
public void testAop5() {
System.out.println("Spring版本: " + SpringVersion.getVersion());
System.out.println("springboot版本: " + SpringBootVersion.getVersion());

// calcService.div(10, 2);
/*
运行结果:
Spring版本: 5.2.8.RELEASE
springboot版本: 2.3.3.RELEASE
我是环绕通知之前AAA
********@Before我是前置通知
CalcServiceImpl被调用了, 我们的计算结果: 5
********@AfterReturning我是返回后通知
********@After我是后置通知
我是环绕通知之后BBB
*/

calcService.div(10, 0);
/*
运行结果:
Spring版本: 5.2.8.RELEASE
springboot版本: 2.3.3.RELEASE
我是环绕通知之前AAA
********@Before我是前置通知
********@AfterThrowing我是异常通知
********@After我是后置通知

java.lang.ArithmeticException: / by zero
*/
}
}

After相当于Finally的位置

执行顺序总结:

参考: 【Spring-AOP】@Around环绕通知详解

1
2
3
4
5
6
7
8
9
10
11
/*
* try{
* @Before
* Result = method.invoke(obj,args);
* @AfterReturing
* }catch(e){
* @AfterReturing
* }finally{
* @After
* }
* */

2.Spring循环依赖

恶心的大厂面试题

你解释下Spring中的三级缓存?
三级缓存分别是什么? 三个Map有什么异同?
什么是循环依赖? 请你谈谈? 看过Spring源码吗? 一般我们说的Spring容器是什么?
如何检测是否存在循环依赖? 实际开发中见过循环依赖的异常吗?
如何检测是否存在循环依赖? 实际开发中见过循环依赖的异常吗?
多例的情况下, 循环依赖问题为什么无法解决?

什么是循环依赖

多个bean之间相互依赖, 形成了一个闭环.
比如: A依赖于B、B依赖于C、C依赖于A

通常来说, 如果问Spring容器内部如何解决循环依赖, 一定是指默认的单例Bean中, 属性互相引用的场景

以JavaSE例子说明:

两种注入方式对循环依赖的影响

https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html#beans-dependency-resolution

我们AB循环依赖问题只要A的注入方式是setter且singleton, 就不会有循环依赖问题

spring容器循环依赖报错演示BeanCurrentlyCreationException

循环依赖现象在Spring容器中注入依赖的对象, 有两种情况

构造器方式注入依赖

1
2
3
4
5
6
7
8
9
10
package com.atguigu.spring.circulardepend.constructorinjection;

public class ServiceA {
private ServiceB serviceB;

public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
}

1
2
3
4
5
6
7
8
9
package com.atguigu.spring.circulardepend.constructorinjection;

public class ServiceB {
private ServiceA serviceA;
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
}

1
2
3
4
5
6
7
8
package com.atguigu.spring.circulardepend.constructorinjection;

public class ClientConstructor {
public static void main(String[] args) {
// new ServiceA(new ServiceB(new ServiceA(???)));
}
}

以set方式注入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.atguigu.spring.circulardepend.setinjection;

import org.springframework.stereotype.Component;

@Component
public class ServiceAA {
private ServiceBB serviceBB;

public void setServiceBB(ServiceBB serviceBB) {
this.serviceBB = serviceBB;
System.out.println("A里面设置了B");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.atguigu.spring.circulardepend.setinjection;

import org.springframework.stereotype.Component;

@Component
public class ServiceBB {
private ServiceAA serviceAA;

public void setServiceAA(ServiceAA serviceAA) {
this.serviceAA = serviceAA;
System.out.println("B里面设置了A");
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.atguigu.spring.circulardepend.setinjection;

public class ClientSet {
public static void main(String[] args) {
ServiceAA aa = new ServiceAA();
ServiceBB bb = new ServiceBB();

bb.setServiceAA(aa);
aa.setServiceBB(bb);

// 运行结果
/*
B里面设置了A
A里面设置了B
*/
}
}

结论:
构造器循环依赖是无法解决的
你想让构造器注入支持循环依赖, 是不存在的
俄罗斯套娃

重要Code案例演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.atguigu.spring.circulardepend;

public class A {
private B b;

public B getB() {
return b;
}

public void setB(B b) {
this.b = b;
}

public A() {
System.out.println("A create success");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.atguigu.spring.circulardepend;

public class B {
private A a;

public A getA() {
return a;
}

public void setA(A a) {
this.a = a;
}

public B() {
System.out.println("B create success");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.atguigu.spring.circulardepend;

public class ClientCode {
public static void main(String[] args) {
A a = new A();
B b = new B();

b.setA(a);
a.setB(b);
/*
// 运行结果
A create success
B create success
*/
}
}

之前基础代码案例


整合Spring案例

resource下创建applicationContext.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="a" class="com.atguigu.spring.circulardepend.A">
<property name="b" ref="b" />
</bean>

<bean id="b" class="com.atguigu.spring.circulardepend.B">
<property name="a" ref="a" />
</bean>

</beans>

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.atguigu.spring.circulardepend;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class ClientSpringContainer {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
A a = context.getBean("a", A.class);
B b = context.getBean("b", B.class);
/*
// 运行结果
21:59:31.029 [main] DEBUG o.s.c.s.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@6ad5c04e
21:59:31.311 [main] DEBUG o.s.b.f.xml.XmlBeanDefinitionReader - Loaded 2 bean definitions from class path resource [applicationContext.xml]
21:59:31.373 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating shared instance of singleton bean 'a'
A create success
21:59:31.395 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating shared instance of singleton bean 'b'
B create success
*/
}
}

默认的单例(singleton)的场景是支持循环依赖的, 不报错

指定bean的作用域

1
2
3
4
5
6
7
<bean id="a" class="com.atguigu.spring.circulardepend.A" scope="singleton">
<property name="b" ref="b" />
</bean>

<bean id="b" class="com.atguigu.spring.circulardepend.B" scope="singleton">
<property name="a" ref="a" />
</bean>

运行结果:

1
2
3
4
5
6
22:04:05.133 [main] DEBUG o.s.c.s.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@6ad5c04e
22:04:05.359 [main] DEBUG o.s.b.f.xml.XmlBeanDefinitionReader - Loaded 2 bean definitions from class path resource [applicationContext.xml]
22:04:05.399 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating shared instance of singleton bean 'a'
A create success
22:04:05.419 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating shared instance of singleton bean 'b'
B create success

原型(prototype)的场景是不支持循环依赖的, 会报错

指定bean的作用域

1
2
3
4
5
6
7
<bean id="a" class="com.atguigu.spring.circulardepend.A" scope="prototype">
<property name="b" ref="b" />
</bean>

<bean id="b" class="com.atguigu.spring.circulardepend.B" scope="prototype">
<property name="a" ref="a" />
</bean>

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
22:05:02.611 [main] DEBUG o.s.c.s.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@6ad5c04e
22:05:02.895 [main] DEBUG o.s.b.f.xml.XmlBeanDefinitionReader - Loaded 2 bean definitions from class path resource [applicationContext.xml]
A create success
B create success
Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'a' defined in class path resource [applicationContext.xml]: Cannot resolve reference to bean 'b' while setting bean property 'b'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'b' defined in class path resource [applicationContext.xml]: Cannot resolve reference to bean 'a' while setting bean property 'a'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:342)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:113)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1697)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1442)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:593)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:342)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:207)
at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1115)
at com.atguigu.spring.circulardepend.ClientSpringContainer.main(ClientSpringContainer.java:9)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'b' defined in class path resource [applicationContext.xml]: Cannot resolve reference to bean 'a' while setting bean property 'a'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:342)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:113)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1697)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1442)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:593)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:342)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:330)
... 9 more
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:268)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:330)
... 17 more

循环依赖出现如下异常:
BeanCreationException: Error creating bean with name ‘a’ … Is there an unresolvable circular reference?

重要结论(Spring内部通过3级缓存来解决循环依赖)

使用 DefaultSingletonBeanRegistry

循环依赖Debug-困难, 请坚持

实例化:
内存中申请一块内存空间
租赁好房子, 自己的家具东西还没有搬家进去

初始化属性填充:
完成属性的各种赋值
装修、家电家具进场

3个Map和四大方法, 总体相关对象

A/B两对象在三级缓存中的迁移说明

  1. A创建过程中需要B, 于是A将自己放到三级缓存里面, 去实例化B
  2. B实例化的时候发现需要A, 于是B先查一级缓存, 没有, 再查二级缓存, 还是没有, 再查三级缓存, 找到了A, 然后把三级缓存里面的这个A放到二级缓存里面, 并删除三级缓存里面的A
  3. B 顺利初始化完毕, 将自己放到一级缓存.里面(此时B里面的A依然是创建中状态), 然后回来接着创建A, 此时B已经创建结束, 直接从以及缓存里面拿到B, 然后完成创建, 并将A自己放到一级缓存里面

do开头的方法才是真正执行业务逻辑的代码

总结Spring是如何解决的循环依赖?

4.Redis

1.准备redis

查看Redis版本

1
2
3
4
5
6
7
8
9
10
11
启动redis
$ redis-server /etc/redis.config
$ ps -ef | grep redis
$ redis-cli
$ redis-server -v
Redis server v=6.2.1 sha=00000000:0 malloc=jemalloc-5.1.0 bits=64 build=6ac1a458dd461fe


127.0.0.1:6379> info
# Server
redis_version:6.2.1

2.redis传统五大数据类型的落地应用

基本数据类型: string list hash set zset(sorted set)

这些数据结构你如何使用? 用在哪些场景? 出了上述5大数据类型, 你还知道其他redis的类型吗?

备注 :
命令是不区分大小写, 而key是区分大小写的
help @类型名词

String

1.最常用:

set key value
get key

1
2
3
4
127.0.0.1:6379> set a b
OK
127.0.0.1:6379> get a
"b"

2.同时设置/获取多个键值

MSET key value [key value]
MGET key [key …]
M => Multi

1
2
3
4
5
6
7
8
9
127.0.0.1:6379> flushdb
127.0.0.1:6379> MSET a1 a1 b1 b1 c1 c1
OK
127.0.0.1:6379> get a1
"a1"
127.0.0.1:6379> mget a1 b1 c1
1) "a1"
2) "b1"
3) "c1"

3.数值增减

递增数字 INCR key
增加指定的整数 INCRBY key increment
递减数值 DECR key
减少指定的整数 DECRBY key decrement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
127.0.0.1:6379> incr bbb
(integer) 1
127.0.0.1:6379> incr bbb
(integer) 2
127.0.0.1:6379> incr bbb
(integer) 3
127.0.0.1:6379> get bbb
"3"
127.0.0.1:6379> incrby bbb 1000
(integer) 1003
127.0.0.1:6379> decr bbb
(integer) 1002
127.0.0.1:6379> decr bbb
(integer) 1001
127.0.0.1:6379> decr bbb
(integer) 1000
127.0.0.1:6379> decrby bbb 10
(integer) 990

4.获取字符串长度

STRLEN key

1
2
3
4
127.0.0.1:6379> set str abcdefghijklmnopqrstuvwxyz
OK
127.0.0.1:6379> strlen str
(integer) 26

5.分布式锁

setnx key value
set key value [EX seconds][PX milliseconds][NX][XX]
ex 表示是过期时间, nx 表示 lock是否存在, 不存在创建 , 存在则不创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
127.0.0.1:6379> set lock pay ex 10 nx
OK
127.0.0.1:6379> get lock
"pay"
127.0.0.1:6379> set lock order ex 10 nx
OK
127.0.0.1:6379> set lock pay ex 10 nx
OK
127.0.0.1:6379> get lock
"pay"
127.0.0.1:6379> ttl lock
(integer) 8
127.0.0.1:6379> get lock
(nil)
127.0.0.1:6379> ttl lock
(integer) -2

6.应用场景
商品编号、订单号采用INCR命令生成
是否喜欢的文章

1
2
3
4
5
6
7
8
127.0.0.1:6379> incr items:001
(integer) 1
127.0.0.1:6379> incr items:001
(integer) 2
127.0.0.1:6379> incr items:001
(integer) 3
127.0.0.1:6379> get items:001
"3"

hash

hash 对应java代码的 Map<String, Map<Object, Object>>
一次设置一个字段值 HSET key field value
一次获取一个字段值 HGET key field
一次设置多个字段值 HMSET key field value [field value …]
一次获取多个字段值 HMGET key field [field …]
获取所有字段值 HGETALL key
获取某个key内的全部数量 hlen key
删除一个key: hdel key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
127.0.0.1:6379> flushdb
127.0.0.1:6379> hset person id 1
(integer) 1
127.0.0.1:6379> hget person id
"1"
127.0.0.1:6379> hmset person name z3 sex 1
OK
127.0.0.1:6379> hmget person id name sex
1) "1"
2) "z3"
3) "1"

127.0.0.1:6379> hgetall person
1) "id"
2) "1"
3) "name"
4) "z3"
5) "sex"
6) "1"

127.0.0.1:6379> hlen person
(integer) 3

127.0.0.1:6379> hdel person sex
(integer) 1
127.0.0.1:6379> hgetall person
1) "id"
2) "1"
3) "name"
4) "z3"

应用场景: 购物车早期, 当前中小厂可用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
127.0.0.1:6379> hset shopcar:uid1024 334488 1
(integer) 1
127.0.0.1:6379> hset shopcar:uid1024 334477 1
(integer) 1
127.0.0.1:6379> hincrby shopcar:uid1024 334477 1
(integer) 2

127.0.0.1:6379> hlen shopcar:uid1024
(integer) 2
127.0.0.1:6379> hgetall shopcar:uid1024
1) "334488"
2) "1"
3) "334477"
4) "2"

list

向列表左边添加元素 LPUSH key value [value …]
向列表右边添加元素 RPUSH key value [value …]
查看列表 LRANGE key start stop
获取列表中元素的个数 LLEN key
弹出 lpop key / rpop key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
127.0.0.1:6379> lpush person z3 l4 w5 z6
(integer) 4
127.0.0.1:6379> lrange person 0 -1
1) "z6"
2) "w5"
3) "l4"
4) "z3"

127.0.0.1:6379> rpush person hl
(integer) 5
127.0.0.1:6379> lrange person 0 -1
1) "z6"
2) "w5"
3) "l4"
4) "z3"
5) "hl"

127.0.0.1:6379> llen person
(integer) 5
127.0.0.1:6379> rpop person
"hl"
127.0.0.1:6379> llen person
(integer) 4

应用场景: 微信文章订阅公众号

set

添加元素 SADD key member [member …]
删除元素(set remove) SREM key member [member …]
获取集合中的所有元素 SMEMBERS key
判断元素是否在集合中 SISMEMBER key member
获取集合中的元素个数(set cardinality) SCARD key
从集合中随机弹出一个元素, 元素不删除 SRANDMEMBER key [数字]
从集合中随机弹出一个元素, 出一个删一个 SPOP key [数字]
集合运算
集合的差集运算 A-B:
属于A但不属于B的元素构成的集合
SDIFF key [key …]

集合的交集运算A∩B
属于A同时也属于B的共同拥有的元素构成的集合
SINTER key [key …]

集合的并集运算A∪B
属于A或者属于B的元素合并后的集合
SUNION key [key …]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
127.0.0.1:6379> sadd set1 1 1 1 2 3 4 5
(integer) 5
127.0.0.1:6379> smembers set1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"

# 删掉了元素5
127.0.0.1:6379> srem set1 5
(integer) 1
127.0.0.1:6379> smembers set1
1) "1"
2) "2"
3) "3"
4) "4"

127.0.0.1:6379> sismember set1 5
(integer) 0
127.0.0.1:6379> sismember set1 4
(integer) 1

127.0.0.1:6379> scard set1
(integer) 4

# 弹出元素, 但不删除
127.0.0.1:6379> srandmember set1
"1"
127.0.0.1:6379> SMEMBERS set1
1) "1"
2) "2"
3) "3"
4) "4"****

# 弹出元素 , 删除
127.0.0.1:6379> spop set1
"1"
127.0.0.1:6379> smembers set1
1) "2"
2) "3"
3) "4"


# 集合运算
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> sadd set1 1 2 3 4 5 6
(integer) 6
127.0.0.1:6379> sadd set2 9 8 7 6 5 4 3 2 1
(integer) 9

127.0.0.1:6379> sdiff set2 set1
1) "7"
2) "8"
3) "9"
127.0.0.1:6379> sinter set1 set2
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
127.0.0.1:6379> sunion set1 set2
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"
9) "9"

应用场景

微信抽奖小程序

朋友圈点赞

zset

向有序集合中加入一个元素和该元素的分数
添加元素 ZADD key score member [score member …]
按照元素分数从小到大的顺序, 返回所有从start到stop之间的所有元素 ZRANGE key start stop [WITHSCORE]
获取元素的分数 ZSCORE key member
删除元素 ZREM key member [member …]
获取指定分数范围的元素 ZRANGEBYSCORE key min max [WITHSCORE]
增加某个元素的分数 ZINCRBY key increment member
获取集合中元素的数量 ZCARD key
获得指定分数范围内的元素个数 ZCOUNT key min max
按照排名范围删除元素
从小到大 ZRANK key member
从大到小 ZRERANK key member

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
127.0.0.1:6379> zadd score 10 cj 9 zsd
(integer) 2
127.0.0.1:6379> zrange score 0 -1
1) "zsd"
2) "cj"
127.0.0.1:6379> zscore score cj
"10"
127.0.0.1:6379> zrem score zsd
(integer) 1

127.0.0.1:6379> zrangebyscore score 11 12
(empty array)
127.0.0.1:6379> zrangebyscore score 10 12
1) "cj"

127.0.0.1:6379> zincrby score 1 cj
"11"
127.0.0.1:6379> zcard score
(integer) 1

127.0.0.1:6379> zcount score 10 11
(integer) 1
127.0.0.1:6379> zrank score cj
(integer) 0

应用场景:
根据商品销售对商品进行排序显示

抖音热搜

3.知道分布式锁吗? 有哪些实现方案? 你谈谈对redis分布式锁的理解, 删key的时候有什么问题?

1.常见面试题

1.Redis除了拿来做缓存, 你还见过基于Redis的什么用法?
2.Redis做分布式锁的时候有需要注意的问题?
3.如果是 Redis 是单点部署的, 会带来什么问题? 那你准备怎么解决单点问题呢?
4.集群模式下, 比如主从模式, 有没有什么问题呢?
5.那你简单的介绍一下 Redlock 吧?你简历上写redisson, 你谈谈
6.Redis分布式锁如何续期? 看门狗知道吗?

2.Base案例

使用场景: 多个服务间+保证同一时刻内+同一用户只能有一个请求(防止关键业务出现数据冲突和并发错误)

Redis数据库添加数据

1
2
127.0.0.1:6379> set goods:001 100
OK

创建两个Module , boot_redis01 boot_redis02
两个工程唯独端口不一样 , 1111 2222

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.atguigu</groupId>
<artifactId>boot_redis01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>boot_redis01</name>
<description>boot_redis01</description>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>

<!-- web+actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- SpringBoot与Redis整合依赖 -->

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>

<!-- jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>

<!-- Spring Boot AOP技术-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!-- redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>

<!-- 一般通用基础配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

application.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

server.port=1111
#2222

#=========================redis相关配香========================
#Redis数据库索引(默认方0)
spring.redis.database=0
#Redis服务器地址
spring.redis.host=192.168.1.100
#Redis服务器连接端口
spring.redis.port=6379
#Redis服务器连接密码(默认为空)
spring.redis.password=
#连接池最大连接数(使用负值表示没有限制)默认8
spring.redis.lettuce.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)默认-1
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接默认8
spring.redis.lettuce.pool.max-idle=8
#连接池中的最小空闲连接默犬认0
spring.redis.lettuce.pool.min-idle=0

主启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.atguigu.boot_redis01;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BootRedis01Application {

public static void main(String[] args) {
SpringApplication.run(BootRedis01Application.class, args);
}

}

配置
RedisConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.atguigu.boot_redis01.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.io.Serializable;

@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Serializable> restTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
}

业务类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.atguigu.boot_redis01.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GoodController {
@Autowired
StringRedisTemplate stringRedisTemplate;

@Value("${server.port}")
String serverPort;

@GetMapping("/buy_goods")
public String buy_goods() {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);

if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort);

return "成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort;
} else {
System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort);
}
return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort;
}
}

小测试
浏览器请求:
http://localhost:1111/buy_goods
http://localhost:2222/buy_goods

3.代码优化

Base案例的业务代码视为V1.0版本


问题一: 竞态条件

竞态条件参考: https://blog.csdn.net/zhm1563550235/article/details/84201583\
所谓竞态条件就是指的 线程A 需要判断一个变量的状态,然后根据这个变量的状态来执行某个操作。在执行这个操作之前,这个变量的状态可能会被其他线程修改。

解决竞态条件, 加单机锁synchronized, V2.0版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.atguigu.boot_redis01.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GoodController {
@Autowired
StringRedisTemplate stringRedisTemplate;

@Value("${server.port}")
String serverPort;

@GetMapping("/buy_goods")
public String buy_goods() {
synchronized (this) {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);

if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort);

return "成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort;
} else {
System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort);
}
return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort;
}
}
}


问题二: 卖出同一件商品

在nginx分布式微服务架构中, nginx轮询将请求转发给1111 2222, 会导致同一个商品被卖出两次的情况

采用添加分布式锁解决这个问题

配置nginx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ pwd
/usr/local/nginx/conf

# 主要配置如下: 将原本root html;注释掉, 然后改成proxy_pass, 配置上游服务器地址
$ vim nginx.conf

upstream goods {
server 192.168.15.85:1111 weight=1;
server 192.168.15.85:2222 weight=1;
}

server {
listen 80;
server_name www.xiamu.com;

location / {
# root html;
proxy_pass http://goods;
index index.html index.htm;
}
}

$ systemctl reload nginx
$ curl http://192.168.1.100/buy_goods
成功买到商品, 库存还剩下: 96 件 服务提供端口: 1111
$ curl http://192.168.1.100/buy_goods
成功买到商品, 库存还剩下: 95 件 服务提供端口: 2222

使用 jemeter压测, 使用100个线程 1秒请求 http://192.168.1.100/buy_goods

发现问题, 97这个商品被消费了两次

解决方案: 使用分布式锁, V3.0版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.atguigu.boot_redis01.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@RestController
public class GoodController {

public static final String REDIS_LOCK = "atguiguLock";

@Autowired
StringRedisTemplate stringRedisTemplate;

@Value("${server.port}")
String serverPort;

@GetMapping("/buy_goods")
public String buy_goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);// setnx

if (!flag) {
return "抢锁失败";
}

String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);

if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort);

stringRedisTemplate.delete(REDIS_LOCK);
return "成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort;
} else {
System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort);
}
return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort;
}
}


问题三: 假如代码出现异常的话, 可能无法释放锁 , 必须要在代码层面finally释放锁
使用finally释放锁, V4.0版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.atguigu.boot_redis01.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@RestController
public class GoodController {

public static final String REDIS_LOCK = "atguiguLock";

@Autowired
StringRedisTemplate stringRedisTemplate;

@Value("${server.port}")
String serverPort;

@GetMapping("/buy_goods")
public String buy_goods() {
try {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);// setnx

if (!flag) {
return "抢锁失败";
}

String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);

if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort);

return "成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort;
} else {
System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort);
}
return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort;
} finally {
stringRedisTemplate.delete(REDIS_LOCK);
}
}
}


问题四: 部署了微服务jar包的机器挂了, 代码层面根本没有走的finally这块, 没办法保证解锁, 这个key没有被删除, 需要加入一个过期时间限定key

设置key过期时间, V5.0版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.atguigu.boot_redis01.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

@RestController
public class GoodController {

public static final String REDIS_LOCK = "atguiguLock";

@Autowired
StringRedisTemplate stringRedisTemplate;

@Value("${server.port}")
String serverPort;

@GetMapping("/buy_goods")
public String buy_goods() {
try {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);// setnx
stringRedisTemplate.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS);

if (!flag) {
return "抢锁失败";
}

String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);

if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort);

return "成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort;
} else {
System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort);
}
return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort;
} finally {
stringRedisTemplate.delete(REDIS_LOCK);
}
}
}


问题五: 设置key+过期时间分开了, 必须要合并成一行具备原子性
V6.0版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.atguigu.boot_redis01.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

@RestController
public class GoodController {

public static final String REDIS_LOCK = "atguiguLock";

@Autowired
StringRedisTemplate stringRedisTemplate;

@Value("${server.port}")
String serverPort;

@GetMapping("/buy_goods")
public String buy_goods() {
try {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);// setnx

if (!flag) {
return "抢锁失败";
}

String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);

if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort);

return "成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort;
} else {
System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort);
}
return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort;
} finally {
stringRedisTemplate.delete(REDIS_LOCK);
}
}
}


问题六: 张冠李戴, 删除了别人的锁

线程A 和 线程B
当线程A执行操作墨迹了一下, 用了12秒, expire自动的释放了, 自己的锁已经被expire了, 此时线程B加了一把锁进来, 但是当线程A执行到了finally去删除锁的时候, 却误删除了线程B添加的锁

只能自己删除自己的锁, 不许动别人的锁
V7.0版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.atguigu.boot_redis01.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

@RestController
public class GoodController {

public static final String REDIS_LOCK = "atguiguLock";

@Autowired
StringRedisTemplate stringRedisTemplate;

@Value("${server.port}")
String serverPort;

@GetMapping("/buy_goods")
public String buy_goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);// setnx

if (!flag) {
return "抢锁失败";
}

String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);

if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort);

return "成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort;
} else {
System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort);
}
return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort;
} finally {
if (stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)) {
stringRedisTemplate.delete(REDIS_LOCK);
}
}
}
}


问题七: finally块的判断+del删除操作不是原子性的

redis事务复习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
### 正常情况
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> mu
(error) ERR unknown command `mu`, with args beginning with:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 v11
QUEUED
127.0.0.1:6379(TX)> set k2 v22
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
127.0.0.1:6379> get k1
"v11"
127.0.0.1:6379> get k2
"v22"


### 事务演示:
### watch 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
### client1
127.0.0.1:6379> set k1 1
OK
127.0.0.1:6379> set k2 2
OK
127.0.0.1:6379> watch k1
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 11
QUEUED
127.0.0.1:6379(TX)> set k2 22
QUEUED

### client2
###此时另一个客户端client2 进来修改了k1 ,打断了原本的事务
127.0.0.1:6379> get k1
"1"
127.0.0.1:6379> set k1 111111
OK
###

127.0.0.1:6379(TX)> exec
(nil)
127.0.0.1:6379> UNWATCH
OK
127.0.0.1:6379> get k1
"111111"

使用Redis事务解决 V8.1版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package com.atguigu.boot_redis01.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@RestController
public class GoodController {

public static final String REDIS_LOCK = "atguiguLock";

@Autowired
StringRedisTemplate stringRedisTemplate;

@Value("${server.port}")
String serverPort;

@GetMapping("/buy_goods")
public String buy_goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);// setnx

if (!flag) {
return "抢锁失败";
}

String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);

if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort);

return "成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort;
} else {
System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort);
}
return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort;
} finally {
while (true) {
stringRedisTemplate.watch(REDIS_LOCK);
if (stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)) {
stringRedisTemplate.setEnableTransactionSupport(true);
stringRedisTemplate.multi();
stringRedisTemplate.delete(REDIS_LOCK);
List<Object> list = stringRedisTemplate.exec();
if (list == null) {
continue;
}
}
stringRedisTemplate.unwatch();
break;
}
}
}
}

或者使用lua脚本来解决 V8.2版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package com.atguigu.boot_redis01.controller;

import com.atguigu.boot_redis01.utils.RedisUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;

import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@RestController
public class GoodController {

public static final String REDIS_LOCK = "atguiguLock";

@Autowired
StringRedisTemplate stringRedisTemplate;

@Value("${server.port}")
String serverPort;

@GetMapping("/buy_goods")
public String buy_goods() throws Exception {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);// setnx

if (!flag) {
return "抢锁失败";
}

String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);

if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort);

return "成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort;
} else {
System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort);
}
return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort;
} finally {
Jedis jedis = RedisUtils.getJedis();

String script = "if redis.call('get', KEYS[1]) == ARGV[1] "
+ "then "
+ " return redis.call('del', KEYS[1]) "
+ "else "
+ " return 0 "
+ "end";

try {
Object o = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value));
if ("1".equals(o.toString())) {
System.out.println("del redis lock ok");
} else {
System.out.println("del redis lock error");
}
} finally {
if (null != jedis) {
jedis.close();
}
}
}
}
}


问题八:
确保RedisLock过期时间大于业务执行时间的问题 (Redis分布式锁如何续期?)
集群+CAP对比zookeeper
redis异步复制造成的锁丢失, 比如: 主节点没来得及把刚刚set进来这条数据给从节点, 就挂了

Redisson实现分布式锁 V9.1版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.atguigu.boot_redis01.controller;

import com.atguigu.boot_redis01.utils.RedisUtils;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;

import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@RestController
public class GoodController {

public static final String REDIS_LOCK = "atguiguLock";

@Autowired
StringRedisTemplate stringRedisTemplate;

@Value("${server.port}")
String serverPort;

@Autowired
private Redisson redisson;

@GetMapping("/buy_goods")
public String buy_goods() throws Exception {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();

RLock redissonLock = redisson.getLock(REDIS_LOCK);
redissonLock.lock();

try {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);

if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort);

return "成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort;
} else {
System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort);
}
return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort;
} finally {
redissonLock.unlock();
}
}
}

在V9.1版本直接unlock的话, 可能会出现如下异常, 为了解决这个异常, 先进行判断在unlock

业务代码修改为如下 V9.2版本

优化 Redisson实现分布式锁 V9.2版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package com.atguigu.boot_redis02.controller;

import com.atguigu.boot_redis02.utils.RedisUtils;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;

import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@RestController
public class GoodController {

public static final String REDIS_LOCK = "atguiguLock";

@Autowired
StringRedisTemplate stringRedisTemplate;

@Value("${server.port}")
String serverPort;

@Autowired
Redisson redisson;

@GetMapping("/buy_goods")
public String buy_goods() throws Exception {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();

RLock redissonLock = redisson.getLock(REDIS_LOCK);
redissonLock.lock();

try {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);

if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort);

return "成功买到商品, 库存还剩下: " + realNumber + " 件\t 服务提供端口: " + serverPort;
} else {
System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort);
}
return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort;
} finally {
if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
redissonLock.unlock();
}
}
}
}

4.小总结

总结:
1.synchronized, 单机版OK, 上分布式
2.nginx分布式微服务, 单机锁不行
3.取消单机锁, 上redis分布式锁setnx
4.只加了锁, 没有释放锁, 出异常的话, 可能无法释放锁, 必须要在代码层面finally释放锁
5.宕机了, 部署了微服务代码层面根本没有走到finally这块, 没办法保证解锁, 这个key没有被删除, 需要有lockKey的过期时间设定
6.为redis的分布式锁key, 增加过期时间, 此外还必须要setnx+过期时间必须同一行
7.必须规定只能自己删除自己的锁, 你不能把别人的锁删除了, 防止张冠李戴, 1删2, 2删3
8.redis集群环境下, 我们自己写的也不OK, 直接上RedLock之Redisson落地实现

4.redis缓存过期淘汰策略

1.面试题

1.生产上你们你们的redis内存设置多少?
2.如何配置、修改redis的内存大小
3.如果内存满了你怎么办
4.redis清理内存的方式?定期删除和惰性删除了解过吗
5.redis缓存淘汰策略
6.redis的LRU了解过吗?可否手写一个LRU算法

2.Redis内存满了怎么办?

1.redis默认内存多少? 在哪里查看? 如何设置修改?

1.查看Redis最大占用内存

2.redis默认内存多少可以用?
如果不设置最大内存大小或者设置最大内存大小为0, 在64位操作系统下不限制内存大小, 在32位操作系统下最多使用3GB内存

3.一般生产上你如何配置?
一般推荐Redis设置为最大物理内存的四分之三

4.如何修改redis内存设置?
通过修改文件配置

1
2
3
4
5
6
7
8
9
10
11
$ whereis redis
redis: /etc/redis.config
$ vim /etc/redis.config

:set nu
/maxmemory <bytes>

975 # maxmemory <bytes>
976 maxmemory 104857600

:wq

通过命令修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ redis-cli     
127.0.0.1:6379> config get maxmemory
1) "maxmemory"
2) "0"
127.0.0.1:6379> shutdown
not connected> exit
$ redis-server /etc/redis.config
$ redis-cli
127.0.0.1:6379> config get maxmemory
1) "maxmemory"
2) "104857600"
127.0.0.1:6379> config get maxmemory
1) "maxmemory"
2) "104857600"
127.0.0.1:6379> config set maxmemory 1
OK
127.0.0.1:6379> config get maxmemory
1) "maxmemory"
2) "1"

5.什么命令查看redis内存使用情况?

info memory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
127.0.0.1:6379> info memory
# Memory
used_memory:955632
used_memory_human:933.23K
used_memory_rss:6811648
used_memory_rss_human:6.50M
used_memory_peak:1017432
used_memory_peak_human:993.59K
used_memory_peak_perc:93.93%
used_memory_overhead:912320
used_memory_startup:809848
used_memory_dataset:43312
used_memory_dataset_perc:29.71%
allocator_allocated:1121288
allocator_active:1466368
allocator_resident:3985408
total_system_memory:3953963008
total_system_memory_human:3.68G
used_memory_lua:37888
used_memory_lua_human:37.00K
used_memory_scripts:0
used_memory_scripts_human:0B
number_of_cached_scripts:0
maxmemory:1
maxmemory_human:1B
maxmemory_policy:noeviction
allocator_frag_ratio:1.31
allocator_frag_bytes:345080
allocator_rss_ratio:2.72
allocator_rss_bytes:2519040
rss_overhead_ratio:1.71
rss_overhead_bytes:2826240
mem_fragmentation_ratio:7.45
mem_fragmentation_bytes:5897024
mem_not_counted_for_evict:4
mem_replication_backlog:0
mem_clients_slaves:0
mem_clients_normal:102464
mem_aof_buffer:8
mem_allocator:jemalloc-5.1.0
active_defrag_running:0
lazyfree_pending_objects:0
lazyfreed_objects:0
127.0.0.1:6379>
2.真要打满了会怎么样? 如果Redis内存使用超出了设置的最大值会怎么样?

改改配置, 故意把最大值设为1个byte试试

1
2
3
4
5
6
7
127.0.0.1:6379> config set maxmemory 1
OK
127.0.0.1:6379> config get maxmemory
1) "maxmemory"
2) "1"
127.0.0.1:6379> set k1 v1
(error) OOM command not allowed when used memory > 'maxmemory'.

超过了设置的最大值会报异常, OOM

3.结论

设置了maxmemory的选项, 加入redis内存使用达到上限

没有加上过期时间你就会导致数据写满maxmemory, 为了避免类似情况, 引出下一章内存淘汰策略

3.Redis缓存淘汰策略

往Redis里写的数据是怎么没了的?

Redis过期键的删除策略

三种不同的删除策略:
1.定时删除

总结: 对CPU不友好,用处理器性能换取存储空间 (拿时间换空间)

2.惰性删除

总结: 对memory不友好,用存储空间换取处理器性能(拿空间换时间)

3.上面两种方案都走极端, 定期删除

总结: 定期抽样key, 判断是否过期, 可能有漏网之鱼

4.上诉步骤都过堂了, 还有漏洞吗?

5.由此引出内存淘汰策略

有哪些内存淘汰策略?

  1. noeviction:不会驱逐任何key
  2. allkeys-lru:对所有key使用LRU算法进行删除
  3. volatile-lru:对所有设置了过期时间的key使用LRU算法进行删除
  4. allkeys-random:对所有key随机删除
  5. volatile-random:对所有设置了过期时间的key随机删除
  6. volatile-ttl:删除马上要过期的key
  7. allkeys-lfu:对所有key使用LFU算法进行删除
  8. volatile-lfu:对所有设置了过期时间的key使用LFU算法进行删除

总结:

  1. 2 * 4得8
  2. 2个维度(过期键中筛选, 所有键中筛选)
  3. 4个方面(LRU, LFU, random, ttl)
  4. 8个选项
1
2
3
4
# 最近最少使用
# LRU means Least Recently Used
# 最不经常使用
# LFU means Least Frequently Used

你平时用哪一种?

默认的内存淘汰策略是: noeviction
一般生产环境不使用这个, 使用allkeys-lru

如何配置、修改?

配置文件

1
2
3
4
5
6
$ vim /etc/redis.config

# The default is:
#
# maxmemory-policy noeviction

命令

1
2
3
4
5
6
$ redis-cli
127.0.0.1:6379> config set maxmemory-policy allkeys-lru
OK
127.0.0.1:6379> config get maxmemory-policy
1) "maxmemory-policy"
2) "allkeys-lru"

5.redis的LRU算法简介

redis的LRU了解过吗? 可否手写一个LRU算法
是什么?

当前坑位占满了三个应用, 当第四个应用来了, 既可以从左边挤进去, 也可以从右边挤进去
当挤进去的时候, 谁多久没用谁出去, 谁利用率最低谁出去

算法来源: https://leetcode.cn/problems/lru-cache/

设置思想

LRU的算法核心是哈希链表: 本质就是HashMap+DoubleLinkedList 时间复杂度是O(1), 哈希表+双向链表的结合体
动画说明:

编码手写如何实现LRU

案例1: 直接继承LinkedHashMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package com.atguigu.lru;

import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCacheDemo<K, V> extends LinkedHashMap<K, V> {

private int capacity;

private static final float loadFactor = 0.75F;

private static final boolean accessOrder = false;

/**
* accessOrder – the ordering mode - true for access-order, false for insertion-order
* @param capacity
*/
public LRUCacheDemo(int capacity) {
super(capacity, loadFactor, accessOrder);
this.capacity = capacity;
}

@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return super.size() > capacity;
}

public static void main(String[] args) {
LRUCacheDemo lruCacheDemo = new LRUCacheDemo(3);

lruCacheDemo.put(1, "a");
lruCacheDemo.put(2, "b");
lruCacheDemo.put(3, "c");
System.out.println(lruCacheDemo.keySet());

lruCacheDemo.put(4, "d");
System.out.println(lruCacheDemo.keySet());

lruCacheDemo.put(3, "c");
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.put(3, "c");
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.put(3, "c");
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.put(5, "x");
System.out.println(lruCacheDemo.keySet());

/*
// accessOrder = true 基于访问顺序
// 运行结果:
[1, 2, 3]
[2, 3, 4]
[2, 4, 3]
[2, 4, 3]
[2, 4, 3]
[4, 3, 5]

// accessOrder = false 基于插入顺序
// 运行结果:
[1, 2, 3]
[2, 3, 4]
[2, 3, 4]
[2, 3, 4]
[2, 3, 4]
[3, 4, 5]
*/
}
}

案例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package com.atguigu.lru;

import java.util.HashMap;
import java.util.Map;

public class LRUCacheDemo2 {

// map负责查找, 构建一个虚拟的双向链表, 它里面安装的就是一个个Node节点, 作为数据载体.

// 1.构造一个Node节点, 作为数据载体
class Node<K, V> {
K key;
V value;
Node<K, V> prev;
Node<K, V> next;


public Node() {
this.prev = null;
this.next = null;
}

public Node(K key, V value) {
super();
this.key = key;
this.value = value;
}
}
// 2.构建一个虚拟的双向链表, 里面安放的就是我们的Node
class DoubleLinkedList<K, V> {
Node<K, V> head;
Node<K, V> tail;

// 2.1 构造方法
public DoubleLinkedList() {
head = new Node<>();
tail = new Node<>();
head.next = tail;
tail.prev = head;
}

// 2.2 添加到头
public void addHead(Node<K, V> node) {

node.prev = head;
node.next = head.next;

head.next.prev = node;
head.next = node;
}

// 2.3 删除节点
public void removeNode(Node<K, V> node) {
node.prev.next = node.next;
node.next.prev = node.prev;

node.next = null;
node.prev = null;
}

// 2.4 获得最后一个节点
public Node getLast() {
return tail.prev;
}
}

private int cacheSize;
Map<Integer, Node<Integer, Integer>> map;
DoubleLinkedList<Integer, Integer> doubleLinkedList;

public LRUCacheDemo2(int cacheSize) {
this.cacheSize = cacheSize; // 坑位
map = new HashMap<>(); // 查找
doubleLinkedList = new DoubleLinkedList<>();
}

public int get(int key) {
if (!map.containsKey(key)) {
return -1;
}

Node<Integer, Integer> node = map.get(key);
doubleLinkedList.removeNode(node);
doubleLinkedList.addHead(node);
return node.value;
}

public void put(int key, int value) {
if (map.containsKey(key)) {
Node<Integer, Integer> node = map.get(key);
node.value = value;
map.put(key, node);

doubleLinkedList.removeNode(node);
doubleLinkedList.addHead(node);
} else {
if (map.size() == cacheSize) {
Node<Integer, Integer> lastNode = doubleLinkedList.getLast();
map.remove(lastNode.key);
doubleLinkedList.removeNode(lastNode);
}

Node<Integer, Integer> newNode = new Node<>(key, value);
map.put(key, newNode);
doubleLinkedList.addHead(newNode);
}
}


public static void main(String[] args) {
LRUCacheDemo2 lruCacheDemo2 = new LRUCacheDemo2(3);

lruCacheDemo2.put(1, 1);
lruCacheDemo2.put(2, 2);
lruCacheDemo2.put(3, 3);
System.out.println(lruCacheDemo2.map.keySet());

lruCacheDemo2.put(4, 4);
System.out.println(lruCacheDemo2.map.keySet());

lruCacheDemo2.put(3, 3);
System.out.println(lruCacheDemo2.map.keySet());
lruCacheDemo2.put(3, 3);
System.out.println(lruCacheDemo2.map.keySet());
lruCacheDemo2.put(3, 3);
System.out.println(lruCacheDemo2.map.keySet());
lruCacheDemo2.put(5, 100);
System.out.println(lruCacheDemo2.map.keySet());

/*
// 运行结果:
[1, 2, 3]
[2, 3, 4]
[2, 3, 4]
[2, 3, 4]
[2, 3, 4]
[3, 4, 5]
*/

}
}

5.补充和总结

无~


面试题第三季
https://xiamu.icu/Java/面试题第三季/
作者
肉豆蔻吖
发布于
2024年1月15日
许可协议