面试题第三季
直面困难, 迎难而上, 要想着太阳底下没有新鲜事儿, 过了这个砍就好了, 一年后的今天回头看看都不是事儿, 做其他事儿说实话都是在逃避.
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 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 s5 = s4.intern(); 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;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 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 { 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 (() -> { synchronized (objectLock) { System.out.println(Thread.currentThread().getName() + "\t come in" ); try { objectLock.wait(); } catch (InterruptedException e) { throw new RuntimeException (e); } System.out.println(Thread.currentThread().getName() + "\t 被唤醒" ); } }, "A" ).start(); new Thread (() -> { synchronized (objectLock) { 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 (() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t come in" ); try { condition.await(); } catch (InterruptedException e) { throw new RuntimeException (e); } System.out.println(Thread.currentThread().getName() + "\t 被唤醒" ); } finally { lock.unlock(); } }, "A" ).start(); new Thread (() -> { lock.lock(); try { condition.signal(); System.out.println(Thread.currentThread().getName() + "\t 通知" ); } finally { lock.unlock(); } }, "B" ).start(); } }
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()); LockSupport.park(); System.out.println(Thread.currentThread().getName() + "\t 被唤醒\t" + System.currentTimeMillis()); }, "t1" ); t1.start(); Thread t2 = new Thread (() -> { System.out.println(Thread.currentThread().getName() + "\t 通知" ); LockSupport.unpark(t1); }, "t2" ); t2.start(); }
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 (); 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(); 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(); 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自身: 1.AQS的int变量 AQS的同步状态State成员
1 2 3 4 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 volatile int waitStatus;
说人话 等候区其他顾客(其他线程)的等待状态 队列中每个排队的个体就是一个Node
2.Node此类的讲解
内部结构
属性说明
AQS同步队列的基本结构
从我们的ReentrantLock开始解读AQS Lock接口的实现类, 基本都是通过聚合 了一个队列同步器 的子类完成线程访问控制的
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 /> </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 > <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 > <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 > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-pool2</artifactId > </dependency > <dependency > <groupId > redis.clients</groupId > <artifactId > jedis</artifactId > <version > 3.1.0</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency > <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 , 0 ); } }
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 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 , 0 ); } }
After相当于Finally的位置
执行顺序总结:
参考: 【Spring-AOP】@Around环绕通知详解
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) { } }
以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); } }
结论: 构造器循环依赖是无法解决的 你想让构造器注入支持循环依赖, 是不存在的 俄罗斯套娃
重要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); } }
之前基础代码案例
整合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); } }
默认的单例(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@6 ad5c04e22 :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 success22 :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@6 ad5c04e22 :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 .do CreateBean(AbstractAutowireCapableBeanFactory.java :593) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory . createBean(AbstractAutowireCapableBeanFactory.java :516) at org.springframework.beans.factory.support.AbstractBeanFactory .do GetBean(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 .do CreateBean(AbstractAutowireCapableBeanFactory.java :593) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory . createBean(AbstractAutowireCapableBeanFactory.java :516) at org.springframework.beans.factory.support.AbstractBeanFactory .do GetBean(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 .do GetBean(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两对象在三级缓存中的迁移说明
A创建过程中需要B, 于是A将自己放到三级缓存里面, 去实例化B
B实例化的时候发现需要A, 于是B先查一级缓存, 没有, 再查二级缓存, 还是没有, 再查三级缓存, 找到了A, 然后把三级缓存里面的这个A放到二级缓存里面, 并删除三级缓存里面的A
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 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" 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 /> </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 > <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 > <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 > <dependency > <groupId > redis.clients</groupId > <artifactId > jedis</artifactId > <version > 3.1.0</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency > <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 spring.redis.database =0 spring.redis.host =192.168.1.100 spring.redis.port =6379 spring.redis.password =spring.redis.lettuce.pool.max-active =8 spring.redis.lettuce.pool.max-wait =-1 spring.redis.lettuce.pool.max-idle =8 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 $ 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 / { 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); 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); 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); 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); 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); 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" 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 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); 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); 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 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 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.由此引出内存淘汰策略
有哪些内存淘汰策略?
noeviction:不会驱逐任何key
allkeys-lru:对所有key使用LRU算法进行删除
volatile-lru:对所有设置了过期时间的key使用LRU算法进行删除
allkeys-random:对所有key随机删除
volatile-random:对所有设置了过期时间的key随机删除
volatile-ttl:删除马上要过期的key
allkeys-lfu:对所有key使用LFU算法进行删除
volatile-lfu:对所有设置了过期时间的key使用LFU算法进行删除
总结:
2 * 4得8
2个维度(过期键中筛选, 所有键中筛选)
4个方面(LRU, LFU, random, ttl)
8个选项
你平时用哪一种?
默认的内存淘汰策略是: noeviction 一般生产环境不使用这个, 使用allkeys-lru
如何配置、修改?
配置文件
1 2 3 4 5 6 $ vim /etc/redis.config
命令
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 ; 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()); } }
案例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 { 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; } } class DoubleLinkedList <K, V> { Node<K, V> head; Node<K, V> tail; public DoubleLinkedList () { head = new Node <>(); tail = new Node <>(); head.next = tail; tail.prev = head; } public void addHead (Node<K, V> node) { node.prev = head; node.next = head.next; head.next.prev = node; head.next = node; } public void removeNode (Node<K, V> node) { node.prev.next = node.next; node.next.prev = node.prev; node.next = null ; node.prev = null ; } 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()); } }
5.补充和总结 无~