问题背景线上生产环境客户反馈,客户在我们的sass系统中点击"同步tiktokshop"时,同步的数据与tiktopshop数据不一致问题描述上午收到阿里云报警,线上MQ某台机器消息堆积严重,其余机器均为正常问题排查初步怀疑sql慢查询问题由于问题主要出现在Proxy同步系统(一个专用于同步的服务),在RDS MySQL的SQL洞察和会话管理模块进行针对性排查,确实有发现慢sql(处理时间0.9秒,高于标准0.5秒),但分析认为不至于产生消息堆积问题尝试在sql查询条件上增加更多索引条件,使sql查询速度降到(0.2s)修复业务逻辑查询过慢问题慢查询主要是因为同步是根据类别分批同步,每次同步都会进行一个总表的关联查询,当同步任务过多时,导致mysql的cpu过载,虽然在阿里云监控上未发现mysql的cpu有峰值,但为避免后续出现问题,这里也同步做修复对查询的总表业务新增一个缓存,键值为同步任务Id,且在开始分发同步任务之前,先查询总表,构建缓存,避免所有的同步任务同时间查询缓存没查到导致走mysql问题问题自测通过以上方法,在测试环境进行压力测试,发现mq依然出现消息堆积问题,进一步排查在同时提醒下,使用jstack分析java线程,发现某一线程一张处于wait状态,导致mq消息堆积 进一步跟踪发现,该进程尝试http请求外网接口,连接一直处于超时配置添加超时时间,解决该问题
Spec Kit 规范驱动工具Spec Kit 是 GitHub 提出的 规格驱动开发（Specification-driven Development） 工具包，用于通过“规格 → 计划 → 任务 → 实现”的流程，让软件开发更加结构化、可追踪、可协作。 git官网:https://github.com/github/spec-kit 第三方汉化:https://github.com/888888888881/spec-kit-chinese 安装使用uv命令进行安装 shell uv tool install specify-cli --from git+https://github.com/github/spec-kit.git安装成功后使用命令检查是否安装 shell specify使用在终端上使用命令初始化项目,构建规范文档,并按照构建过程选择自己的ide shell specify init . 在ai终端上依次使用 |命令|作用| |-|-| |specify constitution 原则 |给ai定义开发原则,如变量命名规范| |specify specify 用户场景 |为当前项目定义用户场景,避免ai过度设计| |specify plan 实施计划 |为ai定义实施计划| |specify task |生成任务| |specify implement |执行任务|评价 优点: 相较于市面上一些常规的规范驱动工具,如OpenSpec,SpecifyKit定义的规范更加详细,更加注重产品价值和用户场景 缺点: 由于SpecifyKit定义的规范驱动过于庞大,导致一次任务消耗过量的token,性价比不高,且由于目前ai模型上下文都有限制,这种工具很容易导致ai失忆,即使常规ide有上下文压缩,该工具也有保存记录,但避免ai发散的会好解决办法还是避免超出上下文 不适用与多人协作团队开发,每个人关注的业务和模块不同,不能共用同一specify,会导致每个人的specify定义的用户场景不同,从而导致产品混乱 摒弃了cursor等ide自带的优点,cursor最大的优点在于其可以随时回退到指定chat去修改需求而不影响整体代码,但是SpecifyKit问题在于需求在最开始的时候进行定义,即使后期可以修改需求,但是所有的任务计划都会因此改变,从而影响流程(比如当前已执行到阶段3,但是需求又修改了,那么任务计划就会因为需求改变而改变,从而导致流程中断) 使用如"specify plan 写一份总分总格式的日记",agent可能不会执行specify plan,而是直接生成日记
Vue基础v-if和v-for的执行顺序 vue2先执行v-if,再执行v-for vue3先执行v-for,再执行v-if v-show和v-if的区别 v-if再渲染的时候,根据条件判断是否渲染对应的dom,因此其渲染快,更新慢,适用于更新频次不高的场景 v-show在对应的dom上添加display:条件来判断是否渲染,因此其渲染慢,更新快,使用与更新频次较高的场景 vue3的ref和react的异同/vue如何实现数据绑定的vue通过ref/react来实现数据绑定,它们都是通过defineProps来实现的 + ref可以响应式更新任何对象或数值或字符串 + react只能绑定js对象vue的生命周期| Vue2 生命周期 | Vue3 对应生命周期（组合式 API） | 执行阶段说明 | | --------------- | ---------------------------- | ----------- | | — | setup | 所有生命周期函数执行前 | | beforeCreate | beforeCreate（建议使用 setup） | Vue 实例初始化之前 | | created | created（建议使用 setup） | Vue 实例初始化之后 | | beforeMount | beforeMount | 组件挂载到页面前 | | mounted | mounted | 组件挂载到页面后 | | beforeUpdate | beforeUpdate | 数据更新前 | | updated | updated | 数据更新后 | | beforeDestroy | beforeUnmount（Vue3 改名） | 组件卸载前 | | destroyed | unmounted（Vue3 改名） | 组件卸载后 |vue的父子组件通信方法 子组件通过props获取父组件参数 ```vue vue export default { props: { message: { type: String, required: true } } } + 父组件通过$emit获取子组件参数vue export default { methods: { sendData() { this.$emit('update-message', '子组件传来的数据') } } } vue import ChildComponent from './ChildComponent.vue' export default { components: { ChildComponent }, methods: { handleMessage(value) { console.log(value) } } } ```vue2和vue3的处理虚拟dom区别 vue2通过diff算法来计算哪些dom需要更新 由于两个虚拟dom只是更换了顺序,vue2会重新所有都便利更新一遍,浪费效率,因此vue3采用最长公共子序列+diff算法来避免重复计算问题 options API和Composition API的区别 options API：vue2的传统api使用方式,结构简单,易于理解,但是代码过长时不易阅读,需要组件化处理 Composition API：vue3的api使用方式，灵活使用,代码简单,但是学习成本高,不易记 vue如何做权限控制vue可以通过守护路由的方式实现权限控制,当页面在跳转前后可自定义函数,做权限判断跳转对应页面 ```js Vue.use(Router)const router = new Router({ routes: [ { path: '/login', component: Login } ] })// 全局前置守卫：判断是否登录 router.beforeEach((to, from, next) => { // to:来自哪个页面 // from:去哪个页面 // next():放行 next() })// 全局后置守卫：页面切换后执行 router.afterEach((to, from) => {})export default router ```vuex是什么vuex是vue的一个状态管理工具,可以实现vue的数据缓存 使用vuex需要配合localstore和computed来使用vuex由5个核心参数构成： + store:存储vue参数,本身只是临时存储,需要配合localstore来使用 + getter:获取store的参数 + mutations:设置store的参数 + actions:通常配合new Promise来异步执行mutations的方法,使用场景:通过ajax来更新store + modules:模块化,若vue项目过大,可以配置不同模块对应的不同sotre浏览器渲染原理 浏览器通过url访问服务器,获取静态资源 拿到静态html,开始解析html包含的纯html、css和js 纯html会解析成dom,如果时vue或者react,那么会先解析成虚拟dom,再转换为dom css会转换为cssom js会在转换dom和cssom中同步执行 最终将生成的cssom和dom构建成一个渲染树进行渲染 渲染优化手段/页面优化手段页面优化可以从渲染的角度进行分析和优化 1. 通过url获取的静态资源,可以放在具有cdn的第三方服务器,如果是图片的话可以使用url而非静态资源 2. 构建的html时,可以使用webpack+treeshaking,它可以在构建的时候删掉未使用的代码 3. css使用
类型推断ts的对象是强类型,js的对象是弱类型 + 强类型:不符合规范的赋值会报错 + 弱类型：赋值的时候可以随时修改对象类型ts会根据输入的数据自动判断类型 typescript let a = 'aaa' a = 10 //会报错类型注解可以在赋值的时候规定类型 typescript let a:string = 'aaa' let b:string类型断言ts在执行复制操作时,会判断该操作是否可以执行,如果可能不会执行成功,则不允许对结果对象操作 ```typescript let arr = [1,2,3] let result = arr.find(item => {item > 2}) result * 5 //会报错,认为result对象可能为空,不允许操作resultresult = arr.find(item => {item > 2}) as number result * 5 //断言result必然返回值 ```基本类型和联合类型typescript // 基本类型：string、number、boolean、null、undefined let str:string let num:number let bo:boolean let nu:null //不常用,通常用联合替代 let un:undefined //不常用,通常用联合替代 // 联合类型：可以指定对象有哪些类型或者范围 let str2:string|undefined = undefined //指定对象可能是string或者undefind let num2:1|2|3 = 5 //指定对象范围只能是1|2|3,赋值5会报错数组ts的数组有3种写法 typescript //不推荐,类型可以随意赋值 let arr1 = [1,'a',true] //推荐 let arr2:number[] = [1,2,3] //另类写法 let arr3:Array<number> = [1,2,3]元组ts的元组是一种特殊集合类型,限定了集合的存储个数和类型 typescript let t1:[number,string,boolean] = [1,'a',3] //会报错,第三个类型要求是boolean let t2:[number1,number2] = [1] //会报错,少了第二种类型 let t3:[number1,number2?] = [1] //不会报错,使用？代表该值可选枚举ts提供一种枚举类型 typescript enum myEnum{ A, B, C } console.log(myEnum.A) console.log(myEnum[0])函数typescript //第一个参数必填,第二个参数有默认值,第三个参数选填,第四个参数选填 function Fn(a:number,b = true,c?:string,...rest:number[]):void{ return 1 //会报错,指定了返回值是void } Fn('1') //会报错,指定了传参是number Fn(20,true,'aa',1,2,3,4) //正确用法接口接口定义了一个对象的使用规范 ```typescript interface Obj{ name:string, age,number }let obj:Obj = { name:'a', age:10 } ```类型```typescript let a:string|number //每次定义对象都要写一次很长的类型,过于麻烦type MyType = string|number let b:MyType //定义一次type,之后直接使用 ```泛型ts支持泛型,定义方法的时候先不定义其类型,使用的时候再定义 ```typescript function Fn(a:T,b:T):T[]{ return [a,b] } Fn(1,'a') //会报错,当指定泛型后,所有的T都会变为指定类型 Fn(1,2) //正确用法```函数重载js不支持函数重载,但是ts支持```typescript function hello(): string { return "hello" }function hello(value: string | number): string { if (typeof value === 'string') { return hello,我的名字是${value} } else if (typeof value === 'number') { return hello,我的年龄是${value} }else { return 'hello' } }hello('demo') ```接口的继承```typescript interface parther{ name:string age:number } interface child extends parent{ phone:string }let user:child = { name:'张三', age:18, phone:'10086' } ```类的使用ts的类使用和python/java的方法很像```typescript class myClass extends fatherClas implements fatherInterface{ public name: string // 类的属性 age: number // public可以省略,除了public还有private、protected和static,用法和java一样 phone?: string // 若ts的配置文件开启了强制验证,那么类的属性必须赋值,除非它是可选或有默认值 birthday = '2001.9' private static readonly address = 'china' //readonly类似java的final,表示不可修改// 构造函数,构造函数必须给必填项赋值 constructor(name:string,age:number) { super(); this.name = name; this.age = age; } } ```存取器ts提供了一个getter和setter的封装,可以取和设置类的私有属性 ```typescript class User{ private _phone:string = ''get phone(): string { if (!this._phone) return ''; if (this._phone.length === 11) { return this._phone.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2'); } else { return '******'; } } set phone(newPhone:string){ this._phone = newPhone } }let user = new User() user.phone = "12345678901" console.log(user.phone) // 输出：123****8901 ```抽象类ts的抽象类和java的抽象类概念和用法时一样的 ```typescript abstract class Animal { constructor(public name: string) {}abstract makeSound(): void; // 抽象方法：必须由子类实现move() { console.log(${this.name} is moving); } }class Dog extends Animal { makeSound() { console.log('Woof! Woof!'); } }const d = new Dog('Buddy'); d.makeSound(); // Woof! Woof! d.move(); // Buddy is moving ```泛型类用法和java的泛型类是一样的,在不确定传入的类型之前,可以使用T来代替 ```typescript class myClass{ public name: T// 构造函数,构造函数必须给必填项赋值 constructor(name:T) { this.name = name; } }let cls = new MyClass('张三') ```
Java 基础Java 面向对象有哪些特征封装、继承、多态、(抽象)什么是浅拷贝什么是深拷贝11 浅拷贝指对内存地址的拷贝,如= 深拷贝指对对象属性进行拷贝,如 beanUtilhashcode、==和 equals 的区别hashcode 求值的散列表值,相同值的哈希值一定相等,不同值的哈希值可能相等 ==是比较内存地址 equals 是比较对象,默认不重写的情况下,和==无区别,重写 equals 一般是通过==比较对象里的值是否相等,且重写也要重写 hashcode,否则在将对象存入 hashmap 这样的表时,hashcode 会取内存地址来求哈希值,进而导致不同对象可能存同一 hash 索引位置hashcode 是求键的位置之和,求哈希码,==是比值,equals 是比对象equals 相等/不等,hashcode 和==一定相等/不等 ==相等/不等,hashcode 和 equals 不一定相等/不等 hashcode 相等/不等,equals 和==不一定相等/不等java 代理模式有哪些 静态代理:直接在类里写函数 动态代理:基础接口,实现接口的函数 CGLIB 动态代理:导入 cglib 依赖,创建代理类和工厂类,通过反射来代理 jdk 动态代理和 CGlib 动态代理的取别 只有有接口的类才能使用 jdk 动态代理,它会使用代理类来操作 CGLIB 是没有接口的类会使用这种方法代理,spring 会将类作为父类来生成对于的子类,由子类重写父类方法从而增强补充父类方法.但是如果类被 final 修饰则无法使用 CGLIB 动态代理 理论上,CGLIB 生成代理类慢,调用快,jdk 动态代理生成快,调用慢 异常处理有那种方式 throw:抛出异常,catch(Exception e){throw new Exception("XXX");} throws:声明异常,public int xx(int x, int y) throws xxException {…}； try,cath,finally:捕获异常:try..cath..finally 重写和重载的区别 重载:同一类中,相同的方法不同的参数 重写,子类修改父类的方法 String、StringBuffter、StringBuilder 的区别和使用场景 String 是一个字符串类,其创建的值被放入字符串池,是不可变的,对象的值改变是因为改变了指针 StringBuffter 和 StringBuilder 其创建的值都是可变的,其原理是创建长度固定的数组,向数组里插入字符,数组满了再创建更大的数组,再插入 StringBuffter 使用了 synchronized 保证线程安全,但效率低 一个类如何避免被继承使用 final 修饰的类不能被继承一个类的构造方法被 private 修饰,如何获取这个类的对象将这个类用 static 修饰通过反射暴力破解讲讲你对反射的理解/为什么使用反射反射是获取类对象的一种方法,这种方法提出了一种叫反射机制的概念,其符合开闭原则,即不改变源代码的情况下对原有功能进行拓展,可以通过反射机制而不改变源码来读取操作任意类.反射通常使用在框架的使用上,在 bean 初始化时,其通过反射获取 bean 对象并存储在集合中.在 RPC 里,用反射调用远程传递过来的类对象并进行操作讲讲你对 static 的理解static 可以用来修饰类、函数或变量,让其具有类基本的属性,在类的生命周期中,如果一个变量被 static 修饰,那么其会在初始化阶段被赋予默认值.且如果一个变量是类变量,那么该类下的成员函数和成员变量都可共享使用该类变量自定义异常如何使用继承 RuntimeException,使用 throw new 自定义异常();抛出即可RuntimeException 和 Exception 的区别RuntimeException 是 Exception 的子类,代表运行时出现的异常Exception 的子类包括: RuntimeException:运行时的异常 IOException:读写时的异常 布尔类型可以和整数类型比较吗使用==比较不可以,只有 c++可以 使用 equals 比较指针的话,可以调用完的方法如何被回收方法存放在栈区,调用完成后出栈回收父类有构造函数,子类如何继承若父类构造函数无参,子类直接继承即可,默认会添加上 super(); 若父类构造函数有参,子类继承时需要加入 super(参数);讲讲你对泛型的理解/为什么要使用泛型由于集合存储时不知道对象的类型,会统一以 Object 类型存储,这会导致一些类型安全问题.因此,jdk1.5 后引用了一种泛型的设计,可以指定集合的存储类型,但是其只在编译时起作用,运行时没有泛型的概念.通过指定对象类型来避免类型安全和强制转换的问题.泛型一个重要的知识点是类型擦除和泛型擦除,当一个具有泛型的集合赋值给一个无泛型的集合,其泛型和内部存储的对象类型会向上转换,如 List1=List2,那么 List1 存储的对象都是 Object,原来的泛型和类型都被擦除了泛型可以通过使用<? super T><存储的未知类型必须是 T 的父类>指定上限或<? extend T>(存储的未知类型必须是 T 的子类)指定下限JDK1.8 有什么新特性 新增了 Lambda 为了兼容 Lambda,接口里面可以写方法,写的是默认方法 新增了函数式接口 stream 接口 多重注解 jvm 回收算法 抽象类和接口类有什么异同同: 都不能实例化 可以作为引用类型 基础抽象或接口的话,其方法必须全部实现,否则还需要继续抽象 接口: 不能定义构造方法 成员变量类型可以是 private、public、protected 一个类可以有多个接口 可以有静态方法 抽象: 可以定义构造方法 成员变量类型只能是 public 一个类只能由一个抽象 不能有静态方法 java 中有哪些新建对象的方法 new 反射 newinstance 序列化 Object.clone 静态方法 讲讲包装类的装箱拆箱 装箱:将基本类型变为对象,jdk1.5 之前是包装类(值),jdk1.5 后新增方法包装类.ValueOf(值) 拆箱:将对象变为基本类型,使用包装类.XXValue() 自动装箱:jdk1.5 后,可以直接将基本类型赋值给包装类,内部会通过调用 ValueOf(值)来实现,如果这个值在-128~127 之间,还会将其缓存,下一次创建不会新建新的对象,而是调用缓存的对象 自动拆箱:当基本类型和包装类进行比较时,包装类会进行拆箱 集合你认识的集合有哪些java 中大多数集合都是由 Colletions 和 Map 这个接口衍生而来的,而 Colletion 由衍生了 Queue、List 和 Set,因此主要为 Queue:队列,现先进先出 List:有序可重复列表 Set:无序不可重复列表 Map:具有映射关系的列表 常用的有 ArrayQueue、ArrayList、LinkeList、HashSet、TreeSet、HashMap、LinkedHashMap、TreeMap特殊的有:LinkedBlockingQueue、ArrayBlockingQueue、HashTable、ConcurrentHashMap、ThreadHashMap有序的就一定有Array,数组的有Linke,无序的一定有Tree和HashJava 中的队列有哪些,一般用在哪里 LinkList ArrayBlokingQueue:有界队列,有大小限制 LinkBlokingQueue:无界队列,无大小限制 一般用在线程池ArrayList 和 LinkList 有什么区别ArrayList 查的快,存的慢,原理是数组 LinkList 存的快,查的慢,原理是链表高并发的集合有哪些问题高并发集合会出现线程安全问题,一般采用锁或者线程安全的集合最初版本的 jdk 线程安全集合:Vector、Hashtable常用的非线程安全集合:ArrayList,HashMap 若想要将 ArrayList,HashMap 改为线程安全,可使用 Collections.synchronized(list)部分集合在内部实现了各种锁来实现线程安全,如: 当前常用的线程安全集合:java.util.concurrent、CopyOnwriteArrayList、ConcurrentHashMap 而线程池常用的安全线程队列集合是:LinkBlockingQueue 和 ArrayBolckingQueue讲讲 HashMap 原理jdk1.7 以前 HashMap 由哈希表(数组)+链表 jdk1.7 以后由哈希表(数组)+链表/红黑树其原理是通过键.hashcode()获取其哈希值,通过取模或高平低位算法获取对应下标存在数组里,默认数组长度是 8,不够会自动扩容,也可以自定义默认大小,将需要存的对象键值以链表形式存储,若链表长度超过 8,会将链表装为红黑树对于防止 hash 冲突,官方使用的是高平低位算法,即生成的哈希码是 int32 位,其中前 16 高位和后 16 低位进行异或,这样可以增加 hash 值的范围,从而避免 hash 冲突hashmap 的自动扩容根据其负载因子决定,其负载因子为 0.75,即若数组存了 3/4 的数据时就进行自动扩容 2 倍,扩容机制和 arraylist 类似,也是新建数组,然后复制数据.其复制数据前会重新计算每个链表或数的节点的 hash 值(这也是为什么节点会存 key,是因为方便重写计算 hash 值),然后 jdk1,7 以前其扩容复制使用的头插法将数据插入,但是头插法在多线程环境下可能会导致首尾相连,导致死循环,所以 jdk8 以后使用的是尾插法,一般情况下不建议触发 hashmap 的自动扩容机制,建议通过 new hashmap(int n)构造函数直接指定数组大小,默认数组大小是 8红黑树转换阈值是 8,其实 6 开始,红黑树的速度就已经快于链表,但为了防止频繁转换红黑树,所以设置为 8hashmap 为什么不用平衡树，用了红黑树讲讲 Arraylist、TreeMap、HashSet、LinkedHashMap 的原理Arraylist 其底层是一个数组,相比普通数组其封装了一个自动扩容的机制,即当插入新数据时,会先判断数组是否满了,若满了则新建一个比原来大 1.5 倍的数组,把原数据复制进来,再把新数据插入TreeMap 其底层是红黑树,每个节点存对于的键值对,当插入新数据的时候会与键进行比较,比键大的放左边,比键小的放右边,键必须重写 comparator 比较方法HashSet 其继承 HashMap,使用 HashMap 的哈希表(数组)来存值LinkedHashMap 其在 HashMap 基础上进行了修改,将链表改为双向链表,且每个数组索引存的链表都是连接起来的,保证了数据的有序性,可从链表直接找完所有数据,不需通过索引找HashMap 和 Hahtable/CurrentHashMap 区别 HashMap 是不安全的,Hashtable 是安全的 HashMap 键值允许 null,当键为 null 则把数据存在数组下标 0,不为 null 则取 key.hashCode()它的哈希值,而 HashTable/CurrentHashMap 直接使用 key.hashCode(),没有判断是否为 null,所以 HashTable/CurrentHashMap 键使用 null 会报空指针异常 Hashtable/CurrentHashMap 之所以不允许 null 为键,是因为在多线程中,无法判断键为 null 是没有这个键值对,还是有键值对只是值为 0.HashMap 中允许键为 null 是因为它不用考虑多线程环境,没有锁,可以直接使用 contains(key)来判断属于哪种情况,但加锁后的多线程不能使用 contains(key) 讲讲 ConcurrentHashMap7 的原理concurrentHashmap7 是将 hashtable 切分成了多个小分段,这些分段用 segment 数组来存储,不同的是 hashtable 用的是 synchnized 锁,concurrenthashmap7 用的是 reentranlock 锁segment 数组不能扩容,初始化后就固定大小,但是每个 hashmap 片段都可以扩容,扩容机制和 hashmap 相同,唯一有区别的是当扩容的时候,会尝试获取 2 次 hashmap 大小,两次不同,则加锁后再获取一次concurrenthashmap 在使用 get 的时候没有用上锁,只有在 Put 的时候才会加锁,避免多线程访问相同的 segment 索引,其获取索引的方式也和 hashmap 一样,sement 根据哈希值取模,hashmap 再根据哈希值取模.讲讲 ConcurrentHashMap8 的原理concurrenthashmap8 和 concurrenthhashmap7 完全不同,它没有使用 sgement 分段锁,没有使用 sgement 不代表完全删除了它,为了避免 jdk7 升级到 jdk8 发现没有 sgement 对象而报错,所以保留了 sgement,但是没有用上.concurrenthashmap8 在获取数据时给具体链表或红黑树加锁,不同 hashtable 的是 hashtable 是给整个 hashtable 加锁,这只是给哈希表存储的数据加锁,其他功能讲讲 ConcurrentHashMap 如何做缓存Current 的键存缓存的 id,值使软引用对象,软引用对象存需要缓存的数据和自身的 key,并使用引用队列存软引用对象当需缓存一个数据的时候,先判断引用队列是否可弹出软引用对象,可弹出则说明该软引用对象内部存的对象被回收,则根据软引用对象的 key 删除 Map 对应的键值讲讲 ThreadLocalMap 的原理ThreadLocalMap 通常是结合 ThreadLocal 使用的,每个线程的哈希值为键,值为线程希望存储的数据.当要通过 set 存储数据时,会获取线程对象,再获取线程对象的哈希值,根据哈希值将值存储进去若遇到哈希冲突问题,ThreadLocalMap 使用开放空间法,通过增加哈希值走下一个索引,判断下一个索引是否被占用,无占用就存储,get 的时候也是,判断存储的键和线程的键是否一致,不一致走下一个索引当 set 结束后会对集合进行扫描,若存储长度大于负载因子 2/3 时,则扩容 2 倍,这和 HashMap 类似hashmap 如何避免内存泄漏hashmap 存的对象一定要重写其 equals 和 hashcode 方法,这样不同对象但值相同覆盖存储而不是重复存储高级一点可以与面试官说,既然要考虑 hashmap 的内存泄漏问题,那么 hashmap 在这个程序中就由担当缓存的功能,可以讲上文的 hashmap 做缓存IO什么是 IO,NIO 又是什么,AIO 又是什么IO 是一种数据流,可以用来处理文件、网络,在 java 中,提供字符流和字节流来进行文件的操作,提供 Soket 方式来操作网络流但是 IO 在处理一个数据流的时候,线程是被占用的,不能去操作另一个新来的数据流,因此 IO 也就 BIO,是一种堵塞 IO,而 NIO 是一种非堵塞 IO,通过使用 Channel、Selector、Buffter 来搭建多路复用机制,去实现非堵塞的 IO.因此 NIO 常用于网络请求和读取大文件AIO 是 NIO 的优化,NIO 虽然是无堵塞 IO,但是其传输数据时需要确认是否有 IO 事件,而 IO 无需确认是否有 IO 事件什么是多路复用,NIO 如何实现多路复用单个线程监控多个客户端连接.Java 的 NIO 使用 Channel、Buffter、Selector 来实现多路复用.分为两步: 当连接建立时,连接会通过 selector 注册到 java 集合里(数组/链表/哈希表),selector 使用观察者模式监听连接是否有事件发生 客户端发送数据到达时,内核会发生 IO 中断,并通知 java 获取内核的数据,根据模型决定(select/poll/epoll)复制对应数据 模型的选择根据操作系统来决定,模型功能如下: select 模模型:连接存储在默认 1024 大小的数组中,有 IO 事件发生,轮询方式查找有事件的请求 poll 模型:连接存储在链表中,,有 IO 事件发生,同样使用轮询获取找有事件请求 epoll 模型:连接存储在 HashMap 中,,有 IO 事件发生,通过内核 epll_ctl 和 epoll_wait 函数回调的信息查找哈希表对应的数据 而数据传输使用的是 Buffter 来存储数据,它是一种 Byte 数组,加入了指针来动态更新数组大小,而数据就通过 channel 通道进行传输ByteBuffer 有什么特点 零拷贝:数据之间的转移本质上不会产生新内存 自动扩容:其数据结构是数组,但也可以像 ArrayList 一样进行扩容,但其扩容是按照 16 整数倍扩容,超过 512 则按 2^n 扩容 内存释放:ByteBuffter 可以指定其存储在堆还是直接内存,若直接需要指定特殊方法回收,netty 使用的就是计数法,每个 bytebuffter 计数为 1,使用 handler 会为其+1,结束 handler 时需要手动-1,当为 0 时则清除内存 池化管理 双指针读写分离:用双指针指定了读取和写入的开始索引 Netty 用的是什么线程模型Netty 主要用 Rector 多路复用线程模型,Rector 模型又可分为单线程 Rector、多线程 Rector 和一主多从 Rector 模型什么是序列化和反序列化序列化是将运行时使用的对象转为磁盘可读取的文件,反序列化反之.在 sringmvc 应用中,相应给客户端请求的对象不需要序列化,因为 srpingmvc 自带拦截器已实现了将 bean 对象转为 json 的序列化,但通过 mybatis 将对象映射给数据库时需要基础 Serializable 接口完成序列化多线程线程的生命周期/有几种状态 新建:new Thread 创建线程后,线程会进入新建状态 就绪:当调用 start()方法后,线程会先创建对应的线程内存,并获取 cpu 资源,这个过程就是就绪 运行:线程逻辑运行 堵塞:出于特殊原因 cpu 放弃执行该线程,堵塞分为同步堵塞(线程因锁而堵塞),等待堵塞(线程池的线程队列就是等待堵塞),其他堵塞(sleep 让线程睡眠) 死亡:线程执行结束 多线程的创建方式有几种 使用 Thread 类,重写 run 方法 使用 Runable 接口,重写 run 方法,可以基础其他类 使用 Callable 接口和 FutureTask 类,重写 call 方法,FutureTask 可以获取 call 方法返回值,但是无返回的时候会堵塞,若返回了线程则直接结束 使用线程池,如 Executor,但不推荐使用 Executor 及其子类创建线程池,而是用其提供的方法 ThreadPoolExecutor 创建 为什么不推荐使用 Executor 创建线程池Executor 其内部也是调用 ThreadPoolExecutor 创建线程池的,该类默认配置好了一些参数,方便快捷创建线程池,但是使用的线程队列是 LinkedBlockingQueue 无界堵塞队列,线程队列大小不限,会因为等待的线程可能过多而 OOM 内存溢出,且不能自定义线程名,不方便排查问题.因此推荐使用 ThreadPoolExecutor 自定义线程池,指定好对应的线程队列大小和拒绝策略线程池的核心参数 corePoolSize 核心线程池:表示允许的工作线程树 maximumPoolSize 最大线程池:表示允许的最大线程 workQueue/queueCapacity 线程队列:非核心线程被保存在此 keepAliveTime 线程存活时间:表示非工作线程可存活的时间 threadFactory 线程工厂:配置创建线程的工厂对象 handler 拒绝策略:当最大线程池满了后,定义新线程的处理方法 AbortPolicy(默认):直接报错 CallerRunsPolicy:用执行该线程池的线程执行 discardPolicy:直接丢掉 discardOldPolicy:丢掉队列里比较老的 线程池的原理/线程池中的工作流程是怎样这个问题要熟悉线程池的每个核心参数的作用,了解核心参数作用这题也就简单线程池是由队列和线程构成的.当任务提交到线程池时,先判断核心线程数够不够,还有没有可以运行的线程拿来用,不够则判断线程队列满没满,没满则按照线程存活时间在线程队列排队等待,满了则判断最大线程数满没满,没满则新建线程,满了则触发拒绝策略,线程池的核心线程数和最大线程数怎么配置合适一般核心线程数<=最大线程数最好,=是最好,而根据业务划分,核心线程数可分为 cpu 密集型:技术类任务,线程数=处理器数+1 IO 型:处理网络连接、数据操作的任务,线程数=2*处理器数,或者线程数=(1+线程等待时间/线程执行总时间)处理器数 这些都是理论值,tomcat 默认最大线程数就是 200,实际需要通过 jumer 或者 postman 进行压测来修改线程池有几种状态 Running:线程池正常工作,运行完后进入 TidYing ShutDown:调用 shutdown()方法,会使线程池停止接收新线程,但线程队列里的线程正常排队运行,运行完后 ShutDown 进入 TidYing 状态 Stop:调用 shutdownnow()方法,会使整个线程池停止 TidYing:线程池没有线程运行时进入此状态,并调用 terminated()方法,该方法是空方法,可重写 Terminated:若重写 terminated()方法,在执行 terminated()方法时进入该状态 为什么线程池创建新线程前需要先把线程放进队列等待创建线程会浪费资源和时间,而核心线程是可以复用的,所有会把新任务防止线程队列等待核心线程,等不下去了才会新建线程.这样设计可以提高相应速度和避免资源浪费线程池是怎么回收线程的核心线程默认不会回收,非核心线程会回收.当线程队列满而最大线程数没满的时候,会创建非核心线程,当线程队列为空的时候,会调用 pull 方法销毁非核心线程你认识哪些锁 synchronized Reentranlock(不是 lock,两者不一样) 什么是 CAS,它有什么优缺点,怎么避免这些问题CAS 是一种乐观的并发控制机制.它由内存值、预期值和新值构成.当 CAS 开始前,会先把内存值复制给预期值,然后 CAS 开始,内存值和预期值进行比较,相同 CPU 会内存值更新未新值,不相同则不进行任何操作优点: 原子性:通过其同个 CPU 指令来进行更新,不可中断 乐观锁:内存值与预期值进行比较,避免同时更新导致并发问题 缺点: 由于其是 CPU 操作,频繁使用 CAS 会带来性能开销.可以用分段或记时方式判断 会遇到 ABA 问题,CAS 通过乐观锁避免多线程同时修改,但是又可能多线程同时修改后又修改回来了,即 A->B->A.这种情况 CAS 无法通过预期值和内存值比较察觉.解决方法是在更新数据后添加更新版本,每次预期值内存值校验都校验以下版本信息 乐观锁悲观锁的区别 乐观锁:乐观的认为更新数据前后不会或少量被其他线程干扰.因此不会引起线程堵塞,更新数据前会判断数据是否被更新过,没有则更新,有则更新失败 悲观锁:悲观的认为更新数据前后一定会被其他线程干扰,通过线程堵塞来避免更新数据前后被其他线程干扰 什么是 AQS/AQS 的原理AQS 是各种锁和同步器的父类,它主要提供了volatile 的锁状态 state、双向同步队列、单向等待队列来实现线程的堵塞操作AQS 的工作流程主要分为 3 个阶段 获取锁:获取锁会尝试获取锁,满足条件则获取成功,不满足则让把线程添加进队列,若线程是头节点,还会再尝试获取锁,成功则取出头结点,然后用自旋方式再尝试获取,获取失败就退出自旋并让线程等待,获取成功则取出节点 执行线程:执行过程中若线程堵塞,会让线程进入等待队列,并释放锁,唤醒同步队列的其他线程,若被唤醒则判断同步队列尾部有无数据,有则放入尾部,没有则获取锁 释放锁:释放锁会尝试释放锁,同样模板模式不提供尝试释放锁的逻辑,需要子类实现,不满足则不释放,满足则将同步队列的队列取出节点 取出节点方式分为独占模式和共享模式,因此获取锁和释放锁又有获取独占锁、获取共享锁、释放独占锁、释放共享锁 独占模式:取出同步队列节点时只取头结点 共享模式:取出同步队列节点时会用传播机制取出所有节点 AQS 是允许同步等待的线程中断等待的,默认是不中断模式,可以通过中断获取锁的方式将线程放入同步队列,若同步队列的线程需要中断等待,不会直接移出队列,而是触发 cancel 方法(可重写),然后把前节点 next 引用到后节点,避免其被唤醒,当后节点发生唤醒、阻塞、中断等一系列改变节点操作时,才会把中断节点移出AQS 如何防止并发问题 3 次获取锁:第一次获取锁决定是否放入队列,第二次获取锁是在加入队列后判断若是头节点则尝试获取锁决定是否堵塞,第三次是完成堵塞和加入队列操作后再尝试获取锁 判断头节点状态:线程插入同步队列时,自旋判断上一个节点是否为空,为空则初始化头结点 CMS 更新节点和锁状态 节点中断不直接移出队列,由于移出节点会受多线程影响,所以只改变其前的 next,若后节点发生堵塞、中断和唤醒等一些列操作需要改变节点时,才移出中断节点 synchronized 和 Reetranlock 的区别 synchronized 是关键字,而 lock 是接口 synchronized 遇到异常会自动释放锁,而 lock 需要用 try...cath..finaly 捕捉异常才能释放 synchronized 只支持独占锁.lock 支持公平锁和非公平锁 synchronized 锁的是对象,而 lock 是根据父类 aqs 为代码块加入 int 类型 state 标识,被锁就+1,解锁就-1 synchronized 没有可重入性,而 lock 有可重入性,可嵌套重复使用 synchronized 会自动加锁释放锁升级锁,lock 需要手动 讲讲 volatile 原理volatile(用来实现共享变量):保证可见性和有序性.告知 jvm对象被保存在主内存而不是线程内存,并且对象创建前后被使用 lock 汇编指令锁住,lock 指令会根据 cpu 型号使用总线锁或缓存锁,总线锁是保证指导内存只能和一个线程通信,缓存锁是总线锁的优化,总线锁把整个 cpu 内存锁住,影响效率,缓存锁只锁 cpu3 级缓存中的第三级目标数据,且使用 MESI 协议和内存屏障完成.由于内存屏障所以 volatile不会触发指令重排,从而实现有序性cpu三级缓存:L1存核心频繁使用的指令和数据、L2存核心不频繁使用的指令和数据,L3存多核共享的指令和数据 MESI协议:想要操作总线锁或缓存锁数据必须是MESI协议,MESI是cpu内存的4个状态:修改、共享、独占、失效 内存屏障:为了优化MESI协议,cpu使用storeBuffter,但这会导致由管理的数据乱序,所有cpu使用内存屏障讲讲 synchronized 的原理synchronized 是一种 jvm 锁,它可以作用在代码块、方法和静态方法中,它会获取类对象的对象头判断是否加锁,用 Monitor 字节码指令将程序锁住,并将 Monitor 的计数器+1,synchronized 的锁是可升级的,过程分为 4 步 无锁:当 jvm 判断这段程序不会出现多线程竞争,那么其就不会加锁 偏向锁:当有线程竞争程序,会由无锁升级到偏向锁,这时会用 Monitor 锁住,如果新线程过来,会判断是否为偏向锁的线程,是则放其运行(可重入),不是则升级自旋锁 轻量级锁/自旋锁:新线程执行程序时发现当前程序被其他线程占用,其会原地循环 10 此获取程序执行权,若 10 次后无法获取,则升级为重量级锁.jdk7 以后自旋锁有了优化,第一次自旋 10 次失败则下次自旋次数-1,反之自旋成功下次自旋次数+1 重量级锁:新线程自旋失败后进入重量级锁,线程会直接等待直到程序不被占用为止 讲讲 Reentranlock 的原理Reentranlock 使用 CAS 方法和 AQS 类来实现加锁机制 CAS 方法:原子性的更新数据 AQS 类:一种队列同步器,内部有一个先进后出队列用来存储线程,被存储的线程会被堵塞,触发唤醒函数后会队首线程取出 而 Reentranlock 设计了两种锁,即非公平锁和公平锁,可在构造函数中使用 非公平锁:新线程遇到 lock()方法,优先尝试获取锁,发现锁的信息 state 为 0(未锁),则将当前线程独占,不允许其他线程使用,并调用 CAS 方法将锁信息 state+1,此时若有一个新线程遇到 lock(),优先尝试获取锁,发现锁的信息不是 0(锁住了),则再判断独占的线程是否是当前的新线程,是的话调用 CAS 方法将锁信息 state+1,不是会把线程放入 AQS 并线程等待,再调用 CAS 方法将锁信息 state+1 公平锁:新线程遇到 lock()方法,优先尝试判断 AQS 首节点是否未 null,若为 null 则说明没有线程等待,则获取锁信息,发现锁的信息不是 0(锁住了),则再判断独占的线程是否是当前的新线程,是的话调用 CAS 方法将锁信息 state+1,不是会把线程放入 AQS 并线程等待,再调用 CAS 方法将锁信息 state+1 ​ 在 AQS 将线程存入的过程中,可以配置等待的线程是否可打断,默认不可打断,若配置可打断,则不想等待的线程会在队列中将节点删掉,然后执行 cancel()方法,locktry()就是重写了这一步,设计了计数器,若线程超过时间还在等待则直接打断执行 cancel 方法()说说 AQS 的是共享模式和独占模式的区别 独占模式:一个锁只允许同时被一个线程获取,reentranlock 用判断 state 是否为 0 来实现,为 0 则获取锁,不为 0 则等待 共享模式:一个锁同一时间可以被多个线程获取,用判断 state 是否大于 0 来判断,大于 0 则获取锁,为 0 则等待 共享锁就是共享模式做的,假设共享锁只允许 3 个线程同时允许,那么前 3 个线程碰到锁都可以执行,而后 3 个只能等待,当它们需要被唤醒时,若用独占模式,只能唤醒一个,而共享可以把等待的 3 个同时唤醒,从而实现共享锁AQS 队列是怎么插入的使用尾插法将新的节点插入,但不同的是其插入前会先检查上一个节点是否为空,这时因为AQS 在插入节点前才会初始化队列,如果不检查会出现并发问题,即可能还没初始化就插入,所以需要插入前检查是否为空,AQS 检查的方式是不断自旋,若为空则初始化,不为就插入你认识哪些同步器/你认识哪些并发工具/并发工具类的应用场景 CountDownLauch:一等多,考试,10 人都交卷后 1 个老师批卷 CyclicBarrier:多等多,LOL10 个人等待彼此加载完后才打开各自的游戏 Samaphore 讲讲闭锁 CountDownLauch 原理CountDownLauch 闭锁是需要手动指定计数(解锁)次数,当调用 countdown.await 方法后,线程会堵塞,直到其他线程调用指定次数的 countDown 才会释放其原理类似 ReentranLock,内部是共享锁机制,区别与在创建 CoutDownLauch 后会把锁信息的 state 初始化为指定值,计数(解锁)1 次-1,直到为 0 会直接唤醒 AQS 所有排队线程,不像 Reentranlock 只唤醒前面 1 个讲讲闭锁 CyclicBarrier 原理在构造函数中指定计数,线程都执行到 await 后等待并开始计数+1,直到所有线程都执行到 await 后在释放所有线程,重新计数原理是通过使用ReentranLock来实现的,当第一个线程执行到 await 后开始计数,并用锁执行一个计数的任务,其他线程到了以后碰到锁都会计数并等待,直到计数等于构造函数指定的计数后,释放所有线程,并重新计数讲讲信号量 Samaphore 原理Samaphore 是和 CountDownLauch 反着来,前者是计数(解锁)一定次数释放锁,信号量是计数(加锁),一定次数后堵塞线程countdownlunch 和 cyclicbarrier 的区别 底层机制:countdownlauch 使用的是 AQS 的共享机制实现,cyclicbarrier 使用的是 ReentranLock 独占锁实现的 工作原理:countdownlauch 是一批线程等待另一批线程为其释放,cyclicbarrier 是线程彼此等待一起释放 计数量:countdownlauch 计数随意,cyclicbarrier 计数量等于线程量 重用性:countdownlauch 是一次性的,释放了线程就结束了,cyclicbarrier 可重用,释放了线程重新计数 synchronized 和 volatile 的区别讲这个问题需要先了解并发编程的 3 个特性 原子性:操作时要么一次执行完成,要么不执行,不允许中断暂停 可见性:一个变量可以被所有线程同时访问,不允许其他线程自己缓存起来偷偷用 有序性:字节码指令串行化执行,谁先执行谁就运行,按顺序执行,jvm 为了优化编译会进行指令重排,导致无序 了解了并发编程 3 大特性再去看这两个的区别 原子性:volatile 不能保证原子性,执行++操作由于在局部变量表中执行,可保证原子性,但其他多线程操作时就不能保证,而 sychronized 是排他锁,代码不能被打断,因此具有原子性 可见性:synchronized 通过 jvm 指令把操作的数据放在主内存,从而实现可见性.volatile 是告知 jvm 对象在主内存而非线程内存,并加 lock 指令实现总线锁和缓存锁来实现可见性 有序性:synchronized 代码块内的代码是串行化,也可以在 synchronized 里面执行重排序来打破有序性,但默认是有序性的.volatile 直接禁止指令重排而实现有序性 为什么 notify 和 wait 方法必须在 synchronized 里使用wait 和 notify 是用来做多线程通信协调的,调用 wait 方法会将线程的锁释放再让线程在等待区堵塞,调用 notify 方法线程会尝试重写获取锁wait 和 sleep 的区别 wait 执行释放线程的锁,并让线程在等待区堵塞直到重新被唤醒 sleep 单纯的堵塞线程 为什么不推荐使用 stop 停止线程/如何优雅的停止线程java 官方不推荐使用 stop()停止线程,因为其本质是暴力停止线程,会把 jvm 的锁也就是 synchronized 的锁释放掉,造成安全问题,但 Reentranlock 的锁不会释放,因为其是 api 层面的锁,且若直接暴力暂停,使用了 ThreadLocal 的对象可能不会被释放,造成内存泄漏问题推荐使用 interrupt()方法中断线程,使用 Interupt()方法后,然后判断 if(Thread.currentThread().isInterrupted())结果执行自己的逻辑手动结束线程,这样可以自定义锁的释放问题,也可以解决 ThreadLocal 导致的内存泄漏问题什么是 ThreadLocal?它的原理是什么ThreadLocal 是用来存储每个线程的变量,创建好 ThreadLocal 后,每个线程都可以在里面设置属于自己专属变量,其他线程无法干扰.其底层是通过使用 ThreadLocalMap 实现原理是把变量放在具体的线程空间上ThreadLocal 什么情况下会产生内存泄漏ThreadLocal 的变量属于弱引用,当数据为 null 时或线程销毁时会被回收,但是如果碰到线程池,数据不为空且不会被销毁,则不会回收,产生内存泄漏.因此在使用完 ThreadLocal 后要用 xx.remove 手动情况里面的数据为 null多线程如何进行线程通信/ThreadLocal 如何传递数据给子线程子线程传递数据给父线程,只需修改父线程的 ThreadLocal 即可 父线程不能直接传递值给子线程,需要使用 InheritableThreadLocal,它会把 ThreadLocalMap 里的数据复制给子线程什么是死锁/如何避免死锁两个以上的线程互相抢夺资源而产生的循环等待问题被称为死锁,死锁产生的条件有: 一个资源只能被一个线程使用 线程等待时不释放资源 线程在使用某资源时,其他线程不能申请该资源 若干线程首位相接 打破死锁的方式就是打破上述的 4 个条件其中一个就可以 设置共享锁,比如 ReadLock 就是共享锁,而独占锁如 ReetranLock 会出现一个资源一个线程使用的问题,但是共享锁要考虑线程安全,只是读取数据没有线程安全问题,但是如果有写数据就不能使用共享锁 设置线程等待超时处理方案,比如使用 locktry() 若不能申请资源,则释放线程而不让其等待 设置锁的时候注意加锁顺序 串行、并行、并发的区别 串行:程序按顺序一个个执行 并行:程序同时执行 并发:程序看似同时执行,实际底层是任务被拆分成多个,cpu 交替执行 什么是守护线程线程分为守护线程和用户线程,用户线程就是普通的线程,守护线程会在所有普通线程关闭时才自动关闭,GC 垃圾回收器就是在守护先里,也可以手动配置一个线程变为守护线程说说你对线程安全的理解线程安全是指多线程在执行相同代码时,得到正确结果,那么这个线程就是安全的.如果一个线程在执行操作的时候因其他线程影响得到的最终结果不正确,那么就是线程不安全使用了 ConcurrentHashMap 一定线程安全吗不一定,ConcurrentHashMap 虽然使用了锁,但是其 Put 和 get 方法是原子性的操作,如果在多线程情况下,每个线程都复合使用 put 和 get 这样的原子性操作还是会出现数据不一致的问题举例:A 线程尝试 get 数据,hashmap 上锁保护,get 完成后锁就解开了,然后 A 线程开始尝试根据 get 的值 put 数据,但是 B 线程也开始 get 数据,这两个原子性操作就会出现竞争这把锁,如果 A 线程竞争到了还好,竞争不到 B 线程可能会拿到未修改的数据,这时 A 再修改或者 B 再修改都是错误的解决方法要具体业务具体分析,上一个例子可以用 volatile 用共享变量解决,或者把整个原子性的复合操作加锁线程通信的方式有哪些 wait 和 notify 设计线程通信 使用共享变量 volater 使用 Juc 包工具 AQS 的子类或 exchanger 使用 rxjava JVMJVM 的主要功能是什么 解释和运行:在虚拟机中解释和编译字节码文件,实现跨平台运行 内存管理:为对象自动分配内存,自动回收不再使用的对象 即时编译:java 中对常用的热点关键字代码进行缓存,下次使用的时候不再通过编译而是直接读取缓存 JVM 是由那几部分构成 类加载器:用来加载字节码文件 内存管理区:用来存储加载的类和接口 执行引擎:包括即使编译、解释器和垃圾回收器 本地接口:由于 jvm 底层也是 c++制作,所以内部含有 c++接口 1 个 class 字节码文件由哪几部分构成 基本信息:字节码对应的 java 版本号、访问标识符等 字段:对象的名字、类型 常量池:包括常量池(这个常量池只是避免字节码文件出现重复的字节,不是字符串常量池)、类或接口名等 方法:类的方法信息,包括字节码指令 属性:类的内部类列表 i=0;i=i++;i 应该是多少 简单回答:0 高级回答:i++会先把 i 存放再临时操作数栈中,然后对局部变量数组中的进行+1 操作,这时 i 会变成 1,但是临时操作数栈中的 i 会覆盖局部变量数组 解释: =产生的字节码指令: 会把局部变量数组中的对象数据放入临时操作数栈,执行完=右边的操作后会放回局部变量数组 ++产生的字节码指令:会直接修改局部变量数组里的值,而不是像其他运算符修改临时操作数栈的值 简述类的生命周期以及流程类的生命周期大致可分为 5 个阶段,也可以说是 7 个阶段 加载 连接(验证、准备、解析) 初始化 使用 卸载 在类的加载阶段,会把字节码文件信息的 5 个信息(基本信息、常量池、字段、方法、属性)加上一个虚方法表(记录父类的父方法)加载到方法区.其中方法区在 jdk8 以前是永久代,jdk8 以后,字段和方法这两个属性被加载到堆区,而方法区在 jdk8 以后其他 4 个数据从永久代改到元空间在连接阶段,会把存储在内存中的字节码文件进行校验,查看字节码是否符合虚拟机规范(如前前几个字节含"caffee"这个字段).然后会将 static 的变量进行初始化默认值(只是默认值,不执行赋值操作,除非 static 有 final 修饰),最后解析将字节码常量池信息由编号改为内存地址(字节码文件中出现的常量由编号指向,类似一对多,解析后由内存地址指向)初始化阶段,会执行静态代码块 static{}中的语句 只有用上类级别的对象或方法才会进行初始化 以下情况会触发初始化阶段: 直接获取静态对象:类.静态对象会触发 static{} new 类会触发 static{} 执行当前类的 main 方法会触发 static{} 调用 Class.forName(类名)会触发 static{} 以下情况不会触发初始化阶段: 获取的静态对象被 final 修饰,final 在连接阶段就已完成初始化 new 的是一个数组,new 类[10]不会触发 static{} 掉用的是父类的静态对象,不会触发子类的 static{},如子类.父类静态对象 以及被初始化过的类,不能再被初始化 因此,初始化阶段的执行顺序是静态代码块->代码块->构造函数->main类的加载器有哪些 bootstrap 启动类加载器:用来加载 java 核心类(加载目录 jre/lib) extension 拓展类加载器:用来加载 java 官方提供的通用但非核心的类(加载目录 jre/lib/ext) application 应用类加载器:用来加载自定义的类(项目目录/target/classes) 什么是双亲委派机制为防止一个类重复出现被多个类加载器加载,jvm 引入双亲委派机制,其通过自下而上首先从 application 应用类加载器->extension 拓展类加载器->bootsrap 启动器类加载器去查询该类是否被加载,若被加载则返回类的对象,若没被加载,就自上而下从 bootsrap 启动器类加载器->extension 拓展类加载器->application 应用类加载器去查找该类是否在该启动器的加载目录,找到就加载如何打破双亲委派机制 自定义类加载器:通过集成 ClassLoader 这个抽象类,实现 loaderClss 方法来实 线程上下文获取类加载器:通过线程上下文获取类加载器 getContextClassLoader()类加载器,从而避开由父加载器委派,属于打破双亲委派. osgi 机制,此方法已被淘汰,但提供了热部署思路:本质上还是自定义类加载器,一个类加载器委派给另一个类加载器,从而实现线上热部署 JDBC 如何加载驱动,是否打破双亲委派JDCB 是否打破双亲委派是有争议的说法,JDBC 使用的是 jdk 的一种 SPI 服务发现机制(将接口实现类的名字配置在文件中，并由服务加载器读取配置文件，加载实现类)即由启动类加载器加载 Driver 服务加载类,通过此类来查找项目下的驱动类,找到后使用线程上下文获取类加载器 application 应用加载器来加载.TomCat 如何打破双亲委派机制每个 webapps 应用都有一个自定义类加载器Spring 如何打破双亲委派机制讲讲 java 的内存结构 堆(线程共享):jdk8 以前存方法区,jdk8 以后存方法区里的字段、对象和字符串常量池,堆内存又可分为 used(已使用的内存)、total(总共内存)、max(最大内存),当 total 不够用时会自动扩张,直到 max 最大内存 方法区(线程共享):jdk8 以前方法区在堆的永久代里,jdk8 以后在本地内存里,且把方法区存的类基本数据、常量池、字段、方法、属性中的字段和方法存进堆里,把常量池分为运行时常量池和字符串常量池,字符串常量池也放进堆里,方法区存字符串常量池的引用,把方法区存的命名空间由永久代改为元空间 栈(线程不共享):分为虚拟机栈和本地方法栈,如果由 navitide 修饰就用本地方法栈,它是用 c++实现的,没有就用虚拟机栈,栈存栈帧,每个方法都是以栈帧的形式存入,栈帧又分为局部变量表(存基本数据)、临时操作数栈(存需要计算的数)、帧数据(存动态链接,类解析后的内存引用链接、存方法出口,方法返回的位置和类型、存异常表引用,trycatch 的引用位置) 程序计数器(线程不共享):存每个线程执行到的字节码指令位置,以便切换线程时指导上一次执行到了哪 本地内存(线程共享):jdk8 以后存元空间,但除此之外,如果有用上 NIO 或文件操作技术如(BuffterWrite),那么这些对象会被存在本地内存.因为如果不存本地内存,那么执行顺序是读取数据->本地内存->堆->读取堆数据,这样效率太低,不如读取数据->本地内存->读取本地内存数据,另外本地内存的对象不属于 jvm,所以垃圾回收不会回收到,需要手动销毁 讲讲各个内存是怎么回收的 线程不共享的内存会随着线程销毁而内存销毁 线程共享使用垃圾回收器回收:主要通过判断是否被引用和引用类型,方法区的类对象还要额外判断类的对象或子类是否被回收才能回收 GC 如何判断对象被引用 引用计数法:jvm1.3 之前,一个对象被创建的时候同时会创建其标数,被使用则+1,当标数为 0 时则回收 可达性分析法(目前使用的方法):栈区数据不和普通对象成引用关系,而是栈区数据和 gcroot 对象成引用关系,普通对象再和 gcroot 对象成引用关系,若普通对象无法链路到 gcroot,则可回收,gcroot 是不可回收对象. 一下对象可称为 gcroot 对象: 每个线程 Thread 对象 通过类加载器加载出来的类对象,静态变量的 gcroot 是它 synchronized(这里的对象),锁的 gcroot 是它 本地方法调用的全局对象(jvm 本地接口调用 c++的对象 ) 什么是强、软、弱、虚引用 强引用:如 new Object();这样创建出来的对象是强引用,无法被 gc 回收,当内存不足时会抛出 OOM 异常 软引用:由 new SoftReference<类型>(对象)实现,空间不足时回收 弱引用:由 new WeekReference<类型>(对象)实现,被 gc 发现则回收 虚引用:不能单独存在,必须和软/弱引用对象联合使用,当对象被垃圾回收器回收时,执行特点方法 终结器引用(面试可用来吹牛,实际不会用):Object 有一个方法是 finalize(),对象使用 finalize()时,GC 第一次回收不会回收它,第二次才会回收它 拓展:假设软/弱的对象已被回收,如何回收软/弱引用对象?这里以软引用为例,使用引用队列 ReferenceQueue,在 new SoftReference<类型>(对象,引用队列),当软引用内部对象被回收时,就可以通过 queue.poll()获取到这个软引用,再使用软引用对象 = null;回收它介绍一下你了解的垃圾回收算法垃圾回收算法主要分为: 标记清除算法:根据可达性分析算法获取可回收的对象和不可回收的对象,将它们进行标记,GC 执行时清除标记可回收的对象 复制算法:将内存空间分为一半,一半存当前堆的对象,当 GC 执行时,将不可回收的对象复制到另一半,清除掉可回收的那一半 标记整理算法:对标记整理算法的优化,清除对象后重新整理内存空间,避免出现内存碎片 GC 分代算法(此算法是最常用的算法,它严格来说不是一种算法,而是一种思想,将标记整理和复制算法进行了结合):该算法把堆区 used 区域分为新生代和老年代,新生代使用复制算法,其通过再细分将新生代分为伊甸园区和两个幸存者区 S1、S2.新创建的对象会被出在伊甸园区,当伊甸园区满了后会触发新生代的垃圾回收 YoungGC(或者叫 MinnorGC),YoungGC 会把伊甸园区数据放在 S0,然后使用复制算法进行回收,不同的是当复制不可回收对象时,会对不可对象进行+1 标记,当标记到达 15 时,会把不可回收对象放在老年代.当老年代也满了以后,会先触发 YoungGC 再触发 FullGC,FullGC 是对整个堆区进行回收,新生代用复制算法,老年代用标记整理算法清除. JVM 性能如何调优JVM 性能一般不需要调优,如果条件艰苦或项目需求不同,可以考虑 根据业务需求调整:若一个服务属于频繁处理请求对象,没有或很少保存长期对象,那么就可以考虑减少老年代空间,反之,若一个服务很少有新对象产生,而保存了很多 Spring 的 Bean 对象,可以减少新生代,扩充老年代. 通过查看 FullGC 次数、YoungGC 次数调整,若 YongGC 频繁触发,说明线上服务可能工作频繁,可以考虑增加新生代或者查看 QPS 请求数判断是否被攻击.若 FullGC 频繁触发,可以考虑增加老年代空间,或者跟踪代码查看是否死循环 JVM 垃圾回收器有哪些 Serial 和 SerialOld:前者是单线程的新生代的复制算法回收器,后者是单线程的老年代的标记整理算法回收器 ParNew 和 CMS:前者是多线程的新生代复制算法回收器,后者是使用 4 步:初始标记(标记 GCrood 对象)->并发标记->重新标记->并发清理,使用的是老年代的标记清除算法,它的重新标记是防止并发标记时产生新的对象没有标记到. ParallerScavenge 和 ParallerOld(简称 PS 和 PO):前者是多线程的复制算法回收器,不同 ParNew 的是它可以自动调整堆大小,并允许设置最大暂停时间,是 jdk8 默认回收器.后者是多线程的老年代标记整理算法回收器 G1:jdk9 默认的垃圾回收器.jdk9 以前的垃圾回收器老年代和新生代是连续的,它的特点有以下: 分块处理:G1 将老年代和新生代的每个区拆分为独立且无个数限制的块(只要内存足够大),要求逻辑连续物理不连续.这样垃圾回收的时候不必全堆扫描,只用找到对应的块. 设置最大处理:G1 允许设置最大暂停时间,若有设置最大暂停时间,则同一时间只能处理特定的某个块 新生代新处理方法:在处理新生代时,当伊甸园区块占 60%的时候,会触发 YoungGC,把伊甸园区数据放进幸存者区或 Humougous 区,使用复制算法,当伊甸园区要放进幸存者区的对象超过幸存者区的一半,则直接放进 Humougous 区,Humougous 区属于老年代,但也被独立为一个块 老年代新处理方法:在处理老年代时,若老年代区块占 45%,则触发混合回收,回收所有新生代和部分老年代,它使用的是复制算法,具体步骤类似 CMS,分为初始标记(标记 GCrood 对象)->并发标记->最终标记->并发复制清理,它的最终标记不同 CMS 重新标记,它是标记那些并发标记中因引用改变而漏标的对象,不会处理新产生的对象. 内存溢出问题有哪些?如何解决 OOM 异常:程序内存不足,启动时配置内存 java xx.jre -xmx 内存大小 OOMGC 异常:GC 回收时内存不够 .... 如何解决频繁 GC 的问题 使用 top 命令查询 cpu 状态 区分是 fullGC(老年代 GC)问题回收 YGC(新生代 GC)问题 使用 jstack 追踪线程,查看对应代码.是否死循环等问题 什么是持久代?/为什么删除持久代?/为什么增加元空间jdk1.8 以前,持久代主要用来存放类的信息、字段、名字等元数据但是持久代存储的数据量是有上限的,默认是 64m,超出会报异常,因此使用元空间代替jdk1.8 后,jvm 把方法区存放在元空间里.每个类都会创建一个元空间,用来存储类的常量池、方法区、类信息元数据.元空间属于本地内存,不归 jvm 管String 类型的变量会被 gc 回收吗看清楚,直接"xx"的变量属于常量池,不会被回收,new String("xx")属于堆,会被回收堆区分为哪几个结构?有何异同?gc 回收会根据不同的对象进行划分不同区域 新生代:区域相对老年代较小，对象生命周期短、存活率低，回收频繁。 老年代:区域较大，对象生命周期长、存活率高，回收不及年轻代频繁。 数据库什么是索引/索引有哪些索引类似数组的下标,存储数据表中某一特定数据位置的数据.相对于字典的目录,可以优化数据库的查询速度 按数据结构划分:B+树索引、Hash 索引、全文索引 按底层物理存储划分:聚簇索引、非聚簇索引 按存储内容划分:主键索引(只能有 1 个,不允许空)、唯一索引(只能有 1 个,允许空、普通索引(允许重复,允许空) 按字段个数划分:单列索引(1 个索引对应 1 个字段)、组合索引(1 个索引对应多个字段,用最左前缀原则结合)、联合索引(特殊的组合索引,每个字段顺序是连续的) 索引的优缺点又哪些优点: 提高查询速度 通过唯一索引能保证数据唯一 加速两个表的连接 缺点: 创建索引需要申请额外存储空间 含有索引的字段,发生增删改时由于需要更新索引,所以效率较慢 了解哈希索引吗哈希索引是基于哈希表来实现的,每一行数据都会生成对应的哈希码,哈希索引将所有哈希码存储在索引中,通过哈希码可以找到对应数据的指针.因此哈希索引的查询速度会更快一些,但是哈希索引由于无序特征,不能用于排序,哈希索引只支持等值查询,不支持其他范围或模糊查询,因为哈希码是整列的内容来计算的只有 memory 引擎可以显示设置哈希索引,且其默认就是哈希索引.innodb 不支持直接设置哈希索引,但在特定情况下可以转为哈希索引:若某个辅助索引被频繁使用,会自动转到哈希索引里聚簇索引和非聚簇索引的区别 聚簇索引:索引和值都存储在叶子节点上,能提高查询速度,但涉及增删改操作时由于需要更新索引索引效率慢,聚簇索引就是主键索引.聚簇索引每个非叶子节点存的都是键 非聚簇索引:用一个额外的平衡树存储索引,先查询索引,再根据索引查询值,查询速度慢,但涉及增删改操作不需要回表更新,索引增删改效率高.辅助索引就是非聚簇索引,在 innodb 中,在聚簇索引之上创建的都是辅助索引.非聚簇索引每个非叶子节点存的都是值 主键索引、唯一索引、普通索引的区别 主键索引:不允许为空和重复,每个表只能有 1 个,可作为外键,主键索引就是聚簇索引 唯一索引:不允许重复,可以有多个,不可作为外键 普通索引:可重复,可为空 普通索引尽量用普通索引,除非要保证唯一性才需要使用唯一索引,唯一索引和普通索引在搜索和增删改上效率是不同的搜索:B+树搜索是根据叶子节点的双链表使用二分查找搜索数据的,唯一索引找到数据就停止,普通索引会继续往下查找 更新:普通在更新数据时,如果数据页在当前内存 change buffter 中,就直接修改内存的 change buffterz,就不会重新从磁盘中修改,如果不再内存中,就在磁盘中查找,但唯一索引在内存和非内存的修改数据前会进行一次判断是否重复单列索引和组合索引、联合索引的区别 单列索引:一个索引对应一个列,如果有多个单列索引,查询优化器会选择其中一个 组合索引:一个索引对应多个列,使用它的好处是减少空间开销,但是其必须符合最左前缀原则 联合索引:特殊的组合索引,其字段是连续的 什么是最左前缀原则/最左前缀原则何时失效在使用联合索引时,查询的字段必须符合最左前缀原则时才索引才会生效,具体指的是查询的字段经过排序后从左往右和联合索引的顺序相同.若查询的字段跳过联合索引的字段顺序,那么就会失效innodb 和 myisam 的区别 innodb 默认使用聚簇索引创建 b+树,若没有聚簇索引(主键索引),那么其会使用唯一索引作为键值创建 b+树,若叶没有唯一索引,那么其会创建一个隐式的主键索引.而 myisam 则聚簇索引 innodb 支持视为,myisam 不支持事物 innodb 可以使用外键,myisam 不可用使用外键 innodb 没有保存行数,查询行数需要全文扫描,myisam 保存了行数 innodb 是行锁,多线程操作同行会上锁.myisam 是表锁,多线程操作同表会上锁. B 树和 B+树的区别 数据结构不同:B 树是平衡二叉树,而 B+树是平衡二叉树下面加了双向链表,无论 B 树还是 B+树,其每个节点存储的个数不能超过 2m-1,m 是阶层 存储内容不同:B 树存数据页和页地址,B+树非叶子节点存页地址,叶子节点存数据页,包括页目录和数据 B+树查询只查询子节点,子节点之间有引用,而 B 树要查询全部 B+树根节点保存在内存,子节点在磁盘 sql 语句的执行过程/mysql 的结构是什么,分别有什么用理解什么是索引下推需要直到 mysql 的结构分为: 连接层:负责与客户端建立 TCP 连接 服务层:负责解析 SQL 语法,对引擎层返回的数据进行筛选 计算引擎层:负责存储和提取数据,innodb 使用 B+树实现,每个叶子节点都是一个数据页(16kb),数据页包含数据和页目录(将数据页数据切分,便于查询),每个非叶子节点都存储这其下一层数据页的目录.当要执行查询的时候,会先根据索引从头节点一层层找下去,最后找到其对应的数据页,再根据数据页的页目录找对其对应的目录,再从目录里遍历查询对应数据,若有范围查询,再从当前数据页直接跳到下一个数据页查询 文件系统层:所有数据、日志保存与此 什么是索引下推索引下推是一种查询优化方案,当需要查询一个有条件的数据的时候,通常情况下是计算引擎层返回查询的数据,服务层再根据条件筛选.开启索引下推后,会先在服务层筛选数据,把筛选到的索引再交给计算引擎层去提取,这样可以提高查询效率什么是回表、什么是索引覆盖innodb 根据非聚簇索引查询数据的时候,会先在一个平衡二叉树里根据值查询索引,查询到索引后若需要返回的是不止当前值的数据或需要返回整行数据,则需要根据查询到的索引再去聚簇索引里查询整行数据,这一步操作就是回表(查完非聚簇再去查聚簇),而索引覆盖指的是需要返回的数据就是非聚簇索引的值,那么就直接返回,不再去查非聚簇索引(查完非聚簇索引就结束)索引什么时候会失效七字口诀:模型数空运最快 模:使用模糊查询,且查询条件以%开头(索引覆盖除外).一种特殊情况就是索引覆盖,查询的值刚好就是辅助索引的键,就不会回表查询,这样就属于使用索引了 型:查询时使用的类型错误,即使有隐式转换也不会用索引 数:查询的字段内部又用了函数,如 select sum(字段) 空:索引字段查询条件是 is null、is not null、!=或 or,但是如果实际数据中 null 值过少,查询优化器会判断走索引 运:查询的字段使用了四则运算 最:联合索引,查询的字段不符合最左前缀原则.但是使用 order by 即使符合最左前缀也会失效,因为 order by 排序会判断 where 有没有用索引,如果没有 where 则 orderby 自己使用排序算法排序,返回的数据自然没有使用索引.因此 orderby 可能会导致索引失效,除非索引覆盖 快:Mysql 查询优化器判断使用全文搜索比索引搜索更快就不会用索引搜索 索引失效的最根本原因就是索引查询速度大于全表查询速度,查询优化器使用全表查询索引排序的流程?为什么索引排序可能导致索引失效mysql 使用 order by 来执行排序,具体流程如下:当计算引擎层将数据返回到服务层时,服务层开始判断 sql 语句有无使用索引查询,如果又使用索引就会判断是索引快还是全表扫描快,若索引快直接使用索引进行查询,返回的数据本身就是排好序的,若全表快,使用全表扫描出数据后再用自带的排序算法排序其排序算法是先判断数据量大小是否超过 sort buffter,默认 256kb,可配置,若未超过使用快排,超过了将文件拆分为 n 个 sorf buffter,局部快排,外部归并varchar 和 char 有什么区别/varchar 和 char 的应用场景 varchar 是可变长度字符,char 是不可变长度字符,若存储'abc',使用 char(10)会占用 10 个长度,剩下 7 个会用空格填补,而使用 varchar(10)就只占用 3 个.但是 varchar 效率较低,当需要更新 varchar 的数据时,每次更新 varchar 都会改变长度,而 char 不需要改变长度. char 只能存 255 个字符,与编码无关.而 varchar 能存 2^16-1 个字节,而具体一个字符多少字节与其编码有关 事物的基本特性有哪些/什么是 ACID事物的基本特性指的是 ACID,即: A 原子性:一个事物操作要么全部成功,要么全部失败.在 mysql 中靠 undo log 来保证,它会记录需要回滚的日志信息 C 一致性:数据库从一个一致性状态变为另一个一致性状态.比如 A 有 100,B 有 0,加起来共 100,A 转账 50 给 B,但是转账失败了,两人的钱加起来应该还是 100,B 不能突然多出 50,前后应该数据应该一致.靠其他三个特性来保证 I 隔离性:一个事物在操作时,另一个事物是不可见的.靠 MVCC 来保证 D 持久性:事物一旦提交,就永远保存在数据库中.靠 redo log 来保证,它会记录每次修改的信息 binlog 和 redolog 的区别redolog 是事物每次尝试修改前都会写入修改记录 binlog 是事物最终提交前才会写入的修改记录,通常恢复数据使用 binlog/redolog,主从同步使用 binlog事物隔离级别分为哪些 读未提交:允许读取还未修改完的数据读取的数据可能是正在修改还未修改完未提交的数据 读已提交:读取到的数据必然是刚修改完提交完的数据 重复读:连续读一个正在修改的数据时,不管数据修改成功提交或回滚,拿到的数据结果都一样 序列化:使用类似锁结构,事物开启时不允许其他事物开启 事物问题有哪些 脏读:隔离级别为读已提交时产生,事物读取了另一个事物未提交的数据,设置读已提交可解决 幻读:隔离级别为幻读时产生,两个事物执行相同查询,结果不同,设置一致性非锁定锁可解决 不可重复读:同一事物同一查询结果不同,设置重复读可解决 MVCC 是什么MVCC 是数据库并发控制的一种操作,它使用乐观锁来实现,可以将多个事物进行隔离,类似多线程隔离.由于 mysql 自带的共享锁或排他锁都是悲观锁的一种,是主动加锁,这有可能导致线程堵塞,因此 MVCC 使用乐观锁来实现.其实现原理是每当一个事物创建时,会创建一个集合 readview 用来存储未提交的事物版本号,当事物对数据表进行操作时,会通过快照读(无锁 select)查询每个表的隐藏字段(当前事物版本号,上一个事物版本号)获取事物版本号,若查询的版本号小于 readview 最小的版本号,则代表当前数据表数据已经被其他事物提交了,新事物可以正常提交,那么 readview 最小的事物版本号就可以提交.若查询的版本号大于或等于 readview 集合里的事物版本号,则代表数据表的数据正在被一个事物挂着,则使用该表的上一个历史事物版本号(innodb 默认保存 3 个历史版本,不然会带来存储开销问题)读未提交:不使用 mvcc,因此读的数据可能就是正在修改还没修改完没提交的数据 读已提交:正常使用 mvcc,每次事物开启都创建一个 redview,读到的数据必然是已修改完已提交的数据 重复读:不正常使用 mvcc,每次事物开启都会判断是否已经创建了 readview 了,有就沿用上一个 readview,因此可能出现第一次读的数据是未提交正在修改的数据,而返回其历史版本,但是当这个数据修为完提交了,由来 1 个读事物,还会判断其为未提交正在修改数据,返回其历史版本,导致读到重复数据MVCC的乐观锁不是CAS实现的,而是快照读(无锁select查隐藏字段)什么是三星索引三星索引是数据库索引查询的一种评分,三星是最理想的,但通常情况下到不了三星 一星:查询多个值的时候,下一个值刚好就在旁边的叶子节点(数据页) 二星:排序的时候查询的值是主,主键由于本身就是排好序的,因此不用额外排序 三星:查询的数据刚好是查询的列,就是索引覆盖 一颗 B+树能存多少行数据/Innodb 能存多少行数据,怎么算的假设 1 行数据 1kb,那么一个叶子节点(数据页)能放 16 行这样的数据假设主键使用 bigint,占 8 个字节,innodb 指针占 6 字节,一个数据就占:6+8=14 个字节一个非子节点能存 16kb 数据,那么一个数据页能存 16kb/14b = 1170 个,因此一个数据页能存大概 1170 个数据因此,若 b+树是 2 层,那么其存储的数据量为 1170*16=约 2 万,若为 3 层,那么其存储的数据量是 1170*1170*16 = 约 2 千万,若是 4 层,其存储的就是 1170*1170*1170*16=200 亿mysql 的锁有哪些 根据锁属性划分:全局锁、共享锁、排他锁 根据锁的力度:表锁、行锁、记录锁、间隙锁、临建锁 什么是全局锁、共享锁、排他锁 全局锁:对整个数据库实例加锁,使整个数据库变为只读状态 共享锁:又名读锁,允许多线程读取数据库内容,但是不允许其对数据内容进行修改,直到共享锁释放 排他锁:又名写锁,当对某数据进行更新的时候,不允许其他线程对该数据进行更新,但可以读取 什么是表锁、行锁、记录锁、间隙锁、临建锁 表锁:当事物对数据库进行操作时,事物对整个表进行上锁,在该事物未结束前其他事物不能访问,myisam 默认使用 行锁:当事物对数据库进行操作时,事物对操作的行进行上锁,在该事物未结束前其他事物不能访问,innodb 默认使用 记录锁:特殊的行锁,只锁住一行,一般出现在唯一索引 间隙锁:特殊的行锁,锁住一个区间,此区间是左开右闭(n,m] 临建锁:特殊的行锁,锁住一个区间,此区间是左闭右闭[n,m] Mysql 有死锁吗?怎么避免死锁死锁是指两个以上的线程抢夺资源而互相等待的问题两个线程 AB 同时对两行数据进行修改的时候会出现死锁,比如 A 线程修改第一行和第二行,B 线程修改第二行和第一行,就会出现互相等待解决方法就是开启超时回滚就行,默认超时回滚时间就是 60s,或者在业务层方向将多个修改整合为 1 个修改,进行批量修改mysql 为什么需要主从同步 数据备份,防止宕机 任务分配,Master 负责读,Slave 负责存 mysql 复制原理是什么使用 binlog 二级制日志文件记录数据的改变,在复制的时候就会读取 Binlog 文件数据库 ID 生成有哪些策略 自增 uuid redis 自增:使用内存,性能好,但是其也是自增的一种,而且容易数据泄露,重启系统后可能会生成同样的 id 雪花算法:由 1 位占位符、41 位时间戳、10 位机器 id、12 位随机序列号生成 id 分布式锁如何实现 使用数据库,当线程被锁上在数据库记录,其他线程想要执行先访问数据库查询是否被锁 使用 zookerper 的临时节点作为锁 使用 redis,查询某 key 是否存在,存在则等待,执行时添加上 key 基于 redis 方案生成的 redssion 框架 redis 分布式锁需要使用什么命令setnx key value ex 时间,使用 setnx 代表若 key 不存在则无法创建,添加过期时间是为了防止死锁zookerper 和 redis 做分布式锁有何区别 zookerper 做分布式锁,每次上锁都要创建几个临时节点,使用完锁自动删除 redis 只是简单的对数据进行操作 限流算法有哪些 计数器算法:每个一段时间设置请求上限 滑动时间窗口算法:计数器算法的滑动版 漏桶限流算法:规定最大请求上限 令牌桶限流算法:按一定速率生成令牌存入桶内直到上限,请求过来时从桶里取令牌才能通过,无令牌则等待 微服务开发的原则是什么 每个功能就是一个独立的服务 什么是 CAP 定理 C:数据一致性 A:服务可用性 P:分区容错性(指服务可独立对外,A 服务宕机不影响 B 服务) 什么是 BASE 理论 B:基本可用性(允许宕机时服务损失部分功能,保留关键功能) S:软状态(允许数据在同步时存在延迟,同步的数据处于中间状态) E:数据最终一致性(无论同步是否延迟,最终数据都必须一致) 什么是 2pc 协议2pc 协议是分布式事物的一种处理方案,当多个服务需要提交事物时,需要一种协议来处理,2pc 协议是其中一种,其步骤分为 第一阶段:各参与者提交操作,协调者向参与者发送请求是否同意,成功提交操作代表同意 第二阶段:若参与者都同意,则协调者将事物全部提交 第二阶段:若有一个参与者不同意,则回滚所有事物 2pc 协议有什么缺点 堵塞问题:第一阶段只提交 sql,不执行事物,占用资源 单点问题:协调者宕机会导致参与者堵塞 什么是 3pc 协议3pc 在 2pc 基础上加了一个阶段,在各参与者提交操作之前先向协调者提交状态,协调者确认状态后才能提交操作什么是 TCC 协议TCC 协议是当前分布式事物最常用的协议,其优点在与无堵塞,随时回滚 T:协调者确认参与者事物状态 C:若参与者正常,协调者同意参与者提交事物 C:若参与者宕机,协调者回滚所有事物 对比 2pc,好处在于 2pc 在其他参与者未同意之前都不会提交事物,而 tcc 会直接提交事物,出错了再回滚,这样避免了数据库连接池堵塞为什么 TCC 会出现空回滚空回滚是因为协调者再 T 阶段确认参与者状态时,由于网络原因等问题无法获取其状态,就会空回滚如何解决 TCC 幂等/悬挂问题幂等问题:sql 修改数据库时,事物提交但是提交失败,重新提交时数据不一致.tcc 协议在 T 阶段确认完毕后会执行其中一个 C 阶段,但是可能由于网络问题导致已经提交事物了,但是没有反馈提交成功,tcc 以为没有成功所有再提交异常悬挂问题:由于网络延迟,协调者再向参与者发送 t 确认时超时,而参与者再收到 t 以后仍然执行确认事物解决方法:使用数据库记录协调者是否提交有哪些消息服务方案看具体业务需求: 同步:微服务使用 feign 的 http 协议调用,dubbo 使用 rmi 协议远程调用方法 异步:使用 MQ 异步调用 最大努力通知方案是什么若想要将某消息发送给某服务,需要最大努力保证接收方会收到,有一下方案可以处理: 发送方重复发送,直到接收方返回接收成功 接收方接收到数据后对数据进行确认 什么是幂等问题?有什么常见解决方法幂等问题就是重复执行同意任务 数据库记录:当执行某操作时,向数据库查询是否有执行过该操作 token 令牌:执行某操作时携带令牌,执行完后删除令牌 什么是双写一致性问题如何保证 redis 和 mysql 数据一致,就是更新数据,是先更新/删除缓存好,还是先更新/删除数据库好,有两种方案: 保证强一致性:更新数据前加上锁,保证无第二线程影响,更新数据库,再删除 redis(有性能影响) 保证最终一致性:使用延时双删策略,更新数据前先删除缓存,再更新数据库,更新完成后再借助 MQ 发送异步延时删除 redis(可以使用阿里的 cannel 监听 binlog,有更新则触发 MQ 发布延时删除),若删除失败则再发布 MQ 再删除 通常使用:先更新数据库,再删除缓存我的方案:更新数据库前加上 redis 锁,更新完后解锁,在删除数据库前判断是否有 redis 锁认证和授权的区别是什么 认证:根据用户密码或其他信息确认用户是否登录 授权:根据用户提供的令牌或权限判断用户是否可以使用某服务 cookie 和 session 的区别是什么 cookie:存在客户端浏览器的数据 session:存在服务端的数据 什么是 token?什么是 jwt? token:一种用户登录的加密令牌 jwt:生成和解密 token 的算法 spring 的核心是什么spring 是一个 IOC 和 AOP 容器开源框架 IOC:控制反转,由容器帮我们控制对象 AOP:面向切面,把代码切入到目标功能 容器:用一系列 map 结构来存储数据 Spring 事物传播机制是什么?假设容器里由 A 类和 B 类,A 类事物要调用 B 类的话,会设计事物传播 REQUIRED:默认传播机制.A、B 类都有事物,可以互相调用.其中一个出现异常则全部回滚 NESTED:A 嵌套 B 的事物调用.B 异常只有 B 回滚 还有很多事物传播机制,这里只讲最常用的 2 个Spring、SpringMVC、Springboot 的区别 Spring 是一个 IOC 和 AOP 的开源框架 SpringMVC 是一个基于 Spring 的网站开发框架 Springboot 是 Spring 生态圈的整合和简化框架 Spring 自动装配原理是什么自动装配简单来说就是自动将第三方组件的 bean 装在到 ioc 容器里,不需要开发人员再去写 bean 相关配置在 springboot 应用里面,只需要加上@SpringBootApplication 就可以实现自动装配@SpringBootApplication 它是一个复合注解,真正实现自动装配的注解是它里面的@EnableAutoConfigruation 这个注解自动装配主要依靠三个核心技术 找:由于此配置类是放在第三方 jar 包,通过 springboot 中约定大于配置的理念,去把这个配置类的全路径放在 classpath 里的一个 srping.factories 这个文件里面,这样 springboot 就可以通过 springfactoriesLoder 找到第三方组件的配置类在哪 装:引入第三方依赖的 stater ,启动组件的时候这个组件里必须含有@Configuration 配置类.通过其配置类的@Bean 注解找到需要装入 ioc 容器 用:通过 spring 提供的 ImportSelector 这样的接口动态加载对应的 bean spring 设计模式有哪些 工厂模式 策略模式 代理模式 观察者模式 适配器模式 单例模式 模板模式 装饰者模式 spring 怎么实现事物? 编程式事物:自己通过代码实现 声明式事物:使用@Transcation 实现 事物实现的原理是执行的这个类有@Transcation 时,会把这个类作为 bean 放入 aop 容器中,如果有事物则关闭自动代理,当类的方法执行结束后无异常则把代理对象的事物提交,反之回滚spring 事物什么时候失效 bean 没有被 spring 容器管理 有异常 数据库不支持事物 修饰方法不是 public 讲讲 SpringMVC 的工作流程这个要有图,心中画好一张图 请求在进入 mvc 前,先值需 HttpMessageConvert,将数据转换,一般是转换为 Json,可以自定义转换数据格式 请求进入 mvc,先到 DispatcherServlet 前端控制器,这个 Servlet 是 javaweb 的一个前端控制器,控制请求是否拦截 请求数据从 DispatcherServlet 到 HandlerMapping,根据 url 找对应的 Controller 类在哪,找到后请求数据返回到 DispatcherServlet 前端控制器 请求数据从 DispatcherServlet 到 HandlerAdapter,根据 url 用来找 Controller 对应的方法 请求从 HandlerAdapter 到 Handler,执行对应 Controller 里的方法,返回 ModleView 或响应数据到 DispatcherServlet 前端控制器 响应从 DispatcherServlet 到 ViewResolver,主要是将响应的数据如果是 ModelView 则找对应的 http 文件渲染,若是 Model 对象则转为 json 对象,最后返回 DispatcherServlet 响应从 DispatcherServlet 到 HttpMessageConvert,将响应数据格式修改,并返回给用户 其中,若有配置拦截器则 请求从 DispatcherServlet 到 HandlerMapping 会触发拦截器的 preHandler 返回 ModleView 或响应数据到 DispatcherServlet 前端控制器会触发拦截器的 postHandler 响应从 DispatcherServlet 到 HttpMessageConvert 会触发拦截器的 afterHandler springmvc 多个拦截器,它们的执行顺序是如何 先执行拦截器 A 的 preHandler,再执行拦截器 B 的 preHandler 先执行拦截器 B 的 postHandler,再执行拦截器 A 的 postHandler 先执行拦截器 B 的 afterHandler,再执行拦截器 A 的 postHandler 简述 IOC 的工作流程 准备上下文环境:获取扫描 xml 和注解的对象,如 ApplicationContext 的实现类,XmlWebApplicationContex 和 AnnotationConfigApplicationContext,这两个一个是扫描 xml,一个是扫描注解的,而 ApplicationContext 主要是由 BeanFactory 接口实现的 扫描获取 bean 定义,通过扫描获取 BeanDefination,其包含 Bean 的名称、依赖关系等定义信息 实例化 bean 对象:由 ApplicationContext 创建 DefaultListableBeanFactory,由它加载 BeanDefination 从而创建一个单列的 Bean 对象 存储 bean 对象,将 Bean 放到 HashMap 容器里面,使用三级缓存进行缓存 简述 spring bean 的生命周期springbean 的声明周期分为 5 个阶段 创建前准备:bean 在加载之前从上下文和配置中去解析和查找 bean 有关的拓展实现,比如 init-method(容器初始化 bean 之前执行函数)、destory-method(容器销毁 bean 后执行的函数)、BeanFactoryPostProcessor(bean 前和后实现的函数),这是 spring 提供给开发者使用 bean 之前可自定义拓展的实现,很多第三方组件也经常用到,比如 dubbo 实例化 bean: 通过反射,去创建 bean 的对象 依赖注入:若 bean 含有@Autowrite 注入其他 bean,会去扫描加载对应的 bean 容器缓存:把创建好的 bean 存入容器和缓存 容器销毁:应用上下文关闭时,会销毁 bean,并执行 destory-method 函数 spring 的 bean 是如何获取的/什么是三级缓存/spring 如何解决循环依赖循环依赖问题是 AB 两个 Bean 相互依赖产生的,一般在设计中我们不允许这种情况出现,而 Srping 提供了三级缓存的方案来解决循环依赖问题 字段注入/set 注入,遇到循环依赖不会报错,但是使用循环依赖的 bean 会报错 1.获取 A 对象,在 1-3 级缓存中查找有无 A 对象2.无 A 对象,通过反射实例化对象,并将 A 对象的对象工厂存入 3 级缓存,并将 A 的 id 保存3.反射尝试初始化 A 对象,发现 A 对象依赖 B 对象,在 1-3 级缓存中查找有无 B 对象4.无 B 对象,通过反射实例化对象,并将 B 对象的对象工厂存入 3 级缓存,并将 B 的 id 保存5.反射尝试初始化 B 对象,发现 B 对象依赖 A 对象,在 1-3 级缓存中查找有无 A 对象6.有 A 对象的对象工厂,通过对象工厂获取实例化的对象,并将 A 的实例化对象放入 2 级缓存,删除 3 级缓存的 A 对象工厂7.B 对象获取到 A 的实例化对象,B 对象初始化完成,将 B 的初始化对象放入 1 级缓存,删除 3 级缓存的 B 对象工厂8.A 对象获取 B 对象的初始化对象,A 对象初始化完成,将 A 的初始化对象放入 1 级缓存,删除 2 级缓存的 A 对象工厂9.由于 A 对象引用从实例化变为初始化,所以 B 对象也拿到了 A 对象的初始化对象 构造器注入,遇到循环依赖会直接报错 1.获取 A 对象,在 1-3 级缓存中查找有无 A 对象2.反射尝试初始化 A 对象,发现 A 对象依赖 B 对象,在 1-3 级缓存中查找有无 B 对象,无 A 对象将 A 的 id 保存 3.反射尝试初始化 B 对象,发现 B 对象依赖 A 对象,在 1-3 级缓存中查找有无 A 对象,无 B 对象并将 B 的 id 保存 4.反射尝试初始化 A 对象,发现 A 对象依赖 B 对象,在 1-3 级缓存中查找有无 B 对象,无 A 对象将 A 的 id 保存,发现已保存,则说明有循环依赖,直接报错如何理解 springboot 的 stater 依赖传统的 spring 如果想要加载第三方组件,需要写冗余的 xml 文件,springboot 不需要写,因为它的 stater 依赖里面含有对应的配置类BIO、NIO、AIO 分别是什么 BIO:同步堵塞 IO NIO:异步堵塞 IO AIO:NIO 的 2.0,也是一种异步堵塞 IO,区别是 AIO 有数据需要读的时候再通知给线程 网络通信中一般如何管理线程一般使用 selector 管理线程,即一个线程有多个 selector,1 个 selector 管理多个 channel而在 netty 中,eventLoop 就相对于 selector,一个 eventLoopGroup 管理多个 eventLoop,一个 eventLoop 管理多个 channel当 channel 有读写任务,selector 就会分配线程给他,没有则不分配.三握手四挥手三次握手是建立连接时使用 第一次握手:客户端发送 syn 给服务端,服务端把连接信息存入半连接队列,询问是否能连接 第二次握手:服务端确认可以连接,给客户端发送 syn 和 ack 第三次握手:客户端收到确认后,给服务端发送 ack,服务端把连接信息从半连接队列移到全连接队列,连接建立(这样可以防止一下子很多连接,全连接内存爆了) 四次挥手是关闭连接时使用 第一次挥手:客户端发送 fin 给服务端,表示要关闭连接 第二次挥手:服务端发送 ack 给客户端,表示自己准备关闭 第三次挥手:服务端可能还有数据没发完,彻底发完后,发送 fin 给客户端表示可以关闭连接 第四次挥手:客户端收到后发送 ack 给服务端,双方关闭连接 什么是 MQTT 协议MQTT 协议是一种发布订阅协议,主要由发送者、代理者、订阅者构成A 客户端想要给 B 客户端发送消息,可以先发送给服务端(发布),服务端再把信息发送个客户端 B(订阅)它的格式由: 2 字节固定报头:第一个字节消息类型、DUP、可靠等级、RETAIN,第二个字节包含剩余长度信息 可变报头:根据固定报头消息类型,可变包头携带信息不同 消息 try 和 finaly 都有 return,finaly 的 return 还会执行吗会,finally 中有 return 会覆盖其他块的 return同包下的类怎么调用同包下的类可以直接调用,不使用 importif(1){}可以执行吗不能,c++中可以,但 java 中不行什么是索引索引是一种数据结构,通过在每行数据中添加索引来说一下数据库查询优化方法 使用 limit 使用索引 开慢查询日志 规划化数据库设计:使用 NF3 模式 页合并:B+树每个节点只有 16kb,也就是 6048 字节存储空间,能存储的数据也就只有 6048/(数据类型+6)的内容(6 是索引的字节),若数据满了就会往下开一个节点,导致查询变慢,页合并就是匀一定数据给其他节点多余空间的节点 页分裂:将一个节点变成 2 个节点,而不是往下开节点,当然,开的节点不能超过 m 阶 进程、线程、协程的区别 进程:计算机的一个基本单元,一个应用 线程:进程的一个执行单元 协程:线程的一个执行单元,一种轻便线程,具有挂载方法和切换线程的功能 Rxjava/协程+线程池+Mapeduce 结合思想这是我自己发明的进程调度算法有哪些 先来先去 时间片轮 最短进程有效 最短剩余时间优先 最高响应优先 最高响应比优先 多级反馈发:设置优先级,按顺序进队列,有空余的补上,有优先级高的补上 什么是字节序的大端和小段 大端:高位字节在低地址 小段:低位字节在低地址 判断大端小端用 c++的联合与指针linux 中常用命令 cat/tail 查看日志 ps -ef|grep 进程名 根据进程名获取 PID netstat -tuln|grep 端口号 根据端口号获取 PID kill PID 杀死进程 docker/docker-compose 的常用命令 docker logs/run/start 查看日志/创建并启动/启动 docker-compose up -d --build 安装并后台启动 docker exect 进入容器内部 docker pull/tag/push 拉取/标记/上传,一般结合 dockerHub 用 数据库的存储过程和触发器是什么 存储过程:可以将特定的 sql 语法存储起来,它由类型、约束条件和 create procedue 构成 触发器:一种特殊的存储过程,执行特定的语法如 insert、update 等可以触发我们的逻辑,它会自动触发 redis支持事务吗？redis通过mutil来控制事务,但是不同于传统事务ACID,不支持回滚redis数据结构有哪些? String:一般用于复杂的计数功能的缓存：微博数，粉丝数等,底层实现方式 hash:Hash适合用于存储对象,当数据较少时使用ziplist,数据多时用dict list：比如微博的关注列表，粉丝列表，消息列表,使用双向链表Linkedlist实现,redis3后改为quiklist set： set是一个存放不重复值的无序集合,如果数据是整数且少于512个,用intset,否则用dict来存集合 zset： zset 相比 set 多了一个权重参数score,即有序的set，当数据少于128个用ziplist,否则使用skiplist+dict redis数据底层结构分别是什么 sds:动态字符串sds 或者 long,存的是字符串用sds,存的是数字用Long .sds是一种动态字符串,由已占用空间的长度、剩余可用空间的长度、数据空间构成 dict:一种key-value映射关系的数据结构 ziplist:一个经过特殊`编码的双向链表,普通双向链表,存的数据再内存中不是连续的,会有大量内存碎片,而ziplist将双向链表的数据进行特殊编码使它们连续 quiklist:一种特殊的ziplist,ziplist由于数据连续,所以查询效率高,但是更新效率就很慢,因为每次更新都要重新编码,而quicklist的每个节点都是一个ziplist,优化了此问题 intset:一个由整数组成的有序集合 skiplist：一种特殊收到set集合,每两个相邻节点之间有一个索引,从而构成一颗树,通过二分法课快熟查到数据 redis的持久化有哪些,都是怎么实现的 RDB:默认方式,隔一段时间保存一次数据 AOF:保存操作的记录 redis 是如何删除过期的键 主动:当用户访问对应的 key 时,若超时则删除 被动:redis 每隔 10s 随机抽 20 个 key,有超时则删除,若有 25%以上的 key 超时则再随机抽,重复步骤 redis 集群有哪些 主从复制:一个主 Master,多个从 Slave 哨兵模式 sentinel:在主从复制模式上加了个 sentinel 监控 Master 状态,宕机则重选 redis cluster:不同于主从复制,其没有主节点,各个节点之间可互相访问,但可能会出现宕机数据缺失,最好的模式是各个节点配置一个子节点只关联该节点 什么是 redis 脑裂问题,如何解决脑裂问题主要是 sentinel 哨兵模式下出现的问题,由于网络问题,主节点无法访问子节点,sentinel 重选主节点,网络恢复后两个主节点数据不一致解决方法:限制主节点数量什么是缓存雪崩、穿透、击穿,如何解决 雪崩:key 同时失效,大量数据访问数据库,导致数据库宕机.解决方法:使用多级缓存+使用锁 穿透:空数据没有缓存,大量空数据访问数据库,导致数据库宕机解决方法:空数据缓存 null 就行 击穿:热点 key 失效时,大量请求访问数据库,导致数据库宕机解决方法:热点 key 过期时间长一点或者不让其过期 mybatis 的#和$有什么区别它们都是 sql 参数的替换符,但是#可以防止注入mysql 引擎有哪些?/聚簇和非聚簇有什么区别索引和数据一起存放的引擎叫聚簇 innoDb:索引和数据一起存放,使用 b+树 myisam:只存非聚簇索引,使用哈希数 mysql 中的索引有什么用?索引结构有哪些?索引就类似查字典时可以通过查询偏旁来查找字,它可以提供我们查询效率 B+树:数据满了则开一个子叶存数据 哈希树:数据无序,查询单个数据快,范围查询慢,不能排序 Redis 为什么是单线程 redis 基于内存的,cpu 不影响其效率 单线程没有数据同步问题,不需要锁 MQ什么是 MQmq 是一种先进先出的消息队列,是一种常用的异步通信框架,它可以实现消息的异步处理、流量消峰、日志处理等RabbitMQ 和 kafka 的区别RabbimtMQ 和 Kafka 的区别总的可以概括为 7 种: 结构区别:RabbitMQ 是一种消息代理模式,主要结构是生产者、交换机、队列、消息、消费者.而 Kafka 是一种分布式流模式,主要结构是生产者、分区、消费者构成. 消息顺序:RabbitMQ 消息顺序发送时可能会出现消费失败消息返回队列而导致原本有序变乱序.而 kafka 的 1 消费者就是一个分区,不会出现收到其他分区消息,也就不会出现乱序 消息路由:RabbitMQ 可以使用交换机对消息进行过滤分发(广播、路由、话题模式),而 kafka 没有这种功能 消息时序:RabbitMQ 可以指定消息的存活时间和消息多久进入队列,而 kafka 只支持指定分区内所有消息的存活时间 消息留存:RabbitMQ 消息被消费了就直接删除,而 kafka 是没有到达分区的存活时间则不会删除 容错处理:RabbitMQ 提供了消息重试和死信交换机的机制,而 Kafka 没有这种功能,需要手动实现 处理速度:RabbimtMQ 在 IO 处理上慢于 Kafka 因此,如果有大数据处理业务,如日志监听、数据 ETL 等,可以使用 Kafka.如果有消息发送、定时任务等需求可以使用 RabbitMQRabbitMQ 的工作模式有哪些MQ 共有 5 种工作模式 simple 模式:1 个生产者、1 个队列、1 个消费者 work 模式:1 个生产者、1 个队列、多个消费者 广播模式:1 个生产者、1 个交换机、多个队列、多个消费者.交换机可将消息分发到不同的队列 路由模式:广播模式的升级版,交换机分发消息时会根据携带的 key 进行分发 话题模式:路由模式的升级版,交换机分发消息时携带的 key 支持一定规则 RabbitMQ 常见的问题有哪些 消息顺序问题:生产者发送有序的消息给 1 个队列,多个消费者获取消息,但是消费者处理消息时间不同,可能导致消费者消费完成的顺序不是有序的 消息丢失问题:生产者发送消息,交换机收发,队列收发,消费者受消息可能出现网络波动、系统中断等问题导致消息丢失.生产者丢失,开启消息事物或 confirm 确认模式,Mq 队列丢失开启持久化,消费者丢失关闭自动 ack 消息重复问题:消费者消费完数据准备提交确认但网络波动导致提交失败会导致队列重复发送消息.可使用 redis 消息堆积问题: Nacos 如何热更新配置的 配置变更传播机制：在 nacos 更新配置后,nacos 会通过长轮询机制心跳检测、本地缓存和配置本地监听器的方式更新服务的本地配置 Spring 层的配置刷新机制：Nacos 更新完成后触发@RefreshScope,@RefreshScope 会更新对应的 Bean 的@Value
示例文章这是一个示例文章，展示了如何使用 Typecho Markdown Sync Tool。功能特性 自动同步 Markdown 文件到 Typecho 支持图片自动上传到七牛云 智能检测文件变化 代码示例```python这是一个 Python 代码示例def hello_world(): print("Hello, World!") return True ```图片示例列表 项目 1 项目 2 项目 3 表格| 列1 | 列2 | 列3 | |-----|-----|-----| | 数据1 | 数据2 | 数据3 | | 数据4 | 数据5 | 数据6 |引用这是一个引用示例可以包含多行内容链接访问项目主页本文由 Typecho Markdown Sync Tool 自动生成
微服务事务理论分布式系统无法完成三个指标,即CAP定理 Consistency(一致性):各服务之间数据一致 Availability(可用性):访问服务必须得到响应 Partition Tolerance(分区容错性):各服务之间异步执行,互不影响 事务问题在微服务中,一个服务出现问题,其关联的服务仍然会继续运行,这就容易出现事务问题出现这种原因是分布式不可避免的CAP定理,即分布式服务中(P),一旦A服务宕机,B服务为保持可用性(A),不会也跟着宕机,那么数据(C)就不一致,产生事务问题解决方法 AP模式:各服务在运行过程中,一旦出错则其关联服务执行弥补措施,即同时保证可用性(A)和分布式(P) 例:创建订单和扣减余额正常运行,但是扣减库存出现问题,那么就删除订单并添加余额 CP模式:各服务在运行过程中,等待其关联服务运行完成后再一起提交,即同时保证一致性(C)和分布式(P) 例:创建订单和扣减余额运行完成后等待扣减库存,扣减库存出现问题,则不提交事务无论是AP还是CP,都需要一个中间商检测各服务的健康状况,从而判断执行什么操作,因此我们需要一个事务协调者,即Seata认识Seata介绍Seata是一个微服务/分布式的事务管理框架,其主要有3各部分组成: TC-事务协调者:维护全局事务状态,协调全局事务提交或回滚 TM-事务管理器:定义全局事务范围、定义全局事务、提交或回滚全局事务 RM-资源管理器:管理分支事务资源,与TC交谈以注册事务和报告事务状态,并驱动分支事务提交或回滚 部署TC服务1.下载TC服务首先我们要下载seata-server包，地址在http://seata.io/zh-cn/blog/download.html2.修改本地配置```properties registry { # tc服务的注册中心类，这里选择nacos，也可以是eureka、zookeeper等 type = "nacos"nacos { # seata tc 服务注册到 nacos的服务名称，可以自定义 application = "seata-tc-server" serverAddr = "127.0.0.1:8848" group = "DEFAULT_GROUP" namespace = "" cluster = "SH" username = "nacos" password = "nacos" } }config { # 读取tc服务端的配置文件的方式，这里是从nacos配置中心读取，这样如果tc是集群，可以共享配置 type = "nacos" # 配置nacos地址等信息 nacos { serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" username = "nacos" password = "nacos" dataId = "seataServer.properties" } } ```3.nacos添加配置```properties数据存储方式，db代表数据库store.mode=db store.db.datasource=druid store.db.dbType=mysql store.db.driverClassName=com.mysql.jdbc.Driver store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true store.db.user=root store.db.password=123 store.db.minConn=5 store.db.maxConn=30 store.db.globalTable=global_table store.db.branchTable=branch_table store.db.queryLimit=100 store.db.lockTable=lock_table store.db.maxWait=5000事务、日志等配置server.recovery.committingRetryPeriod=1000 server.recovery.asynCommittingRetryPeriod=1000 server.recovery.rollbackingRetryPeriod=1000 server.recovery.timeoutRetryPeriod=1000 server.maxCommitRetryTimeout=-1 server.maxRollbackRetryTimeout=-1 server.rollbackRetryTimeoutUnlockEnable=false server.undo.logSaveDays=7 server.undo.logDeletePeriod=86400000客户端与服务端传输方式transport.serialization=seata transport.compressor=none关闭metrics功能，提高性能metrics.enabled=false metrics.registryType=compact metrics.exporterList=prometheus metrics.exporterPrometheusPort=9898 ```4.创建数据库tc服务在管理分布式事务时，需要记录事务相关数据到数据库中，需要提前创建好这些表。这些表主要记录全局事务、分支事务、全局锁信息：```mysqlSET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0;-- 分支事务表DROP TABLE IF EXISTS branch_table; CREATE TABLE branch_table ( branch_id bigint(20) NOT NULL, xid varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, transaction_id bigint(20) NULL DEFAULT NULL, resource_group_id varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, resource_id varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, branch_type varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, status tinyint(4) NULL DEFAULT NULL, client_id varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, application_data varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, gmt_create datetime(6) NULL DEFAULT NULL, gmt_modified datetime(6) NULL DEFAULT NULL, PRIMARY KEY (branch_id) USING BTREE, INDEX idx_xid(xid) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;-- 全局事务表DROP TABLE IF EXISTS global_table; CREATE TABLE global_table ( xid varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, transaction_id bigint(20) NULL DEFAULT NULL, status tinyint(4) NOT NULL, application_id varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, transaction_service_group varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, transaction_name varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, timeout int(11) NULL DEFAULT NULL, begin_time bigint(20) NULL DEFAULT NULL, application_data varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, gmt_create datetime NULL DEFAULT NULL, gmt_modified datetime NULL DEFAULT NULL, PRIMARY KEY (xid) USING BTREE, INDEX idx_gmt_modified_status(gmt_modified, status) USING BTREE, INDEX idx_transaction_id(transaction_id) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;-- 全局锁DROP TABLE IF EXISTS lock_table; CREATE TABLE lock_table ( row_key VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, xid VARCHAR(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, transaction_id BIGINT(20) NULL DEFAULT NULL, branch_id BIGINT(20) NOT NULL, resource_id VARCHAR(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, table_name VARCHAR(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, pk VARCHAR(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, gmt_create DATETIME NULL DEFAULT NULL, gmt_modified DATETIME NULL DEFAULT NULL, PRIMARY KEY (row_key) USING BTREE, INDEX idx_branch_id(branch_id) USING BTREE ) ENGINE = INNODB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;SET FOREIGN_KEY_CHECKS = 1; ```5.启动TC服务进入bin目录，运行其中的seata-server.bat即可：微服务集成Seata(部署TM,RM)1.引入依赖```xml com.alibaba.cloud spring-cloud-starter-alibaba-seata seata-spring-boot-starter io.seata io.seata seata-spring-boot-starter ${seata.version} ```2.修改配置文件yaml seata: registry: # TC服务注册中心的配置，微服务根据这些信息去注册中心获取tc服务地址 # 参考tc服务自己的registry.conf中的配置 type: nacos nacos: # tc server-addr: 127.0.0.1:8848 namespace: "" group: DEFAULT_GROUP application: seata-tc-server # tc服务在nacos中的服务名称 cluster: SH tx-service-group: seata-demo # 事务组，根据这个获取tc服务的cluster名称 service: vgroup-mapping: # 事务组与TC服务cluster的映射关系 seata-demo: SH解决方法XA模式XA模式是一种CP模式(强一致性),等待所有服务执行完成后再一并回滚seata对XA进行了一点小调整,但大体相同yaml srata: data-source-proxy-mode: XAjade @service public class xxServiceImpl implements xxService{ @override @GlobalTransactional public void xxMethod(){ ... } }AT模式(默认)AT模式是XA模式的改进方案,属于AP模式(强可用性),不等待其他服务,先记录原始数据,再直接提交,若失败则修改数据AT使用一个全局锁,保证在同一时间修改数据时只能由一个事务处理(下图假设事务1第2此提交失败)由一个极端情况,就是同一时间两个修改数据时,另一个事务不受seata管理,这时seata会报错,业务上我们捕捉这个异常,然后人工处理(途中事务1第2次提交失败)在使用前需要先在微服务数据库里创建快照undolog(不是seata数据库)``sql DROP TABLE IF EXISTSundo_log; CREATE TABLEundo_log(branch_idbigint(20) NOT NULL COMMENT 'branch transaction id',xidvarchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',contextvarchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'undo_log context,such as serialization',rollback_infolongblob NOT NULL COMMENT 'rollback info',log_statusint(11) NOT NULL COMMENT '0:normal status,1:defense status',log_createddatetime(6) NOT NULL COMMENT 'create datetime',log_modifieddatetime(6) NOT NULL COMMENT 'modify datetime', UNIQUE INDEXux_undo_log(xid,branch_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;```yaml srata: data-source-proxy-mode: ATjava @service public class xxServiceImpl implements xxService{ @override @GlobalTransactional public void xxMethod(){ ... } }TCC模式TCC模式是AT模式的手动挡,AT模式会提前保存原始数据,错误后恢复原始数据.而TCC模式则是分为三个模式,Try,Confirm,Cancel Try:事务提交 Confirm:提交成功后执行 Cancel:提交失败后执行 且TCC没有用锁,性能是最好的实现空回滚从而防止业务悬挂,就是各模式没有走正常流程(Try->Confirm/Cancel)而直接执行,我们需要自定义一张表来记录数据状态.在执行Confirm/Cancel前先判断状态状态表需要自己定义,整体结构可参考此表sql DROP TABLE IF EXISTS `account_freeze_tbl`; CREATE TABLE `account_freeze_tbl` ( `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `freeze_money` int(11) UNSIGNED NULL DEFAULT 0, `state` int(1) NULL DEFAULT NULL COMMENT '事务状态，0:try，1:confirm，2:cancel', PRIMARY KEY (`xid`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;整体设计流程如图:```java @LocalTCC public interface xxxService{ //Try @TwoPhaseBusinessAction(name="try方法名称",commitMethod ="confirm方法名称" ,rollbackMethod ="cancel方法名称" ) void Try方法(@BusinessActionContextParameter 参数); //在需要上下文获取的数据使用@BusinessActionContextParameter //confirm和cancel想要拿到参数,就需要给参数@B...Contex//context可以获取事务信息和参数 Bollean confirm方法(BusinessActionContext context); Bollean cancel方法(BusinessActionContext context); } ```java @service public class xxServiceImpl implements xxService{ @override @Transactional public void Try方法(){ // 1. 获取事务信息 int xid = RootContext.getXID(); // 2. 查看状态表中是否已经有事务,有则返回空(防止事务悬挂) ... // 3. 执行事务 ... // 4. 向状态表中添加信息 ... } @override public void confirm方法(BusinessActionContext context){ // 1. 获取事务信息 int xid = context.getXid; // 2. 判断状态表,查看是否存在且状态为Try ... // 3. 删除状态 ... } @override public void cancel方法(BusinessActionContext context){ // 1. 获取事务信息 int xid = context.getXid; // 2. 判断状态表,查看是否存在且状态为Try ... //2.2若不存在,则空回滚,向状态表中添加空信息 //context.getActionContext("参数")获取参数 // 3. 恢复数据,状态改为Cancel ... } }SAGA模式该模式由于没有事务隔离,非常危险,不建议使用
无论是密码登录还是第三方微信登录,我们都需要向前端返回一个token,这个过程就叫认证当用户向服务器发起请求时,会对token是否有效进行验证
多级缓存概述多级缓存是一种设计思路,充分利用请求的每个环境进行缓存,减少Tomcat的压力 一级缓存:浏览器客户端缓存,主要由cookie或LocalStroage 二级缓存:Nginx本地缓存 三级缓存:Redis缓存,主要由Nginx用Lua发起向Redis的询问 四级缓存:JVM缓存,进程缓存 五级缓存:本地数据库 JVM缓存JVM是一种基于本地进程的缓存,相较于Redis其使用本地缓存,没有网络开销(Redis可能使用其他服务器)速度更快,但是不能数据共享.常见的JVM缓存就是以静态变量的形式进行缓存,线程未结束,变量就不会被释放.Spring官方推荐使用Caffeine,其内部就是使用静态Map创建Caffeinejava //创建构造器 Cache<String,String> cache = Caffeine.newBuilder().build();创建构造器时可添加其他功能 initialCapacity(long):存储初始化大小 maximumSize(long):存储最大大小 expireAfterWrite(Duration.ofSeconds(long)):存储存long秒后释放 一般在Config里创建Caffeinejava @Configuration public class CaffeineConfig { @Bean public Cache<String,String> XXCache(){ Cache<String,String> cache = Caffeine.newBuilder() .initialCapacity(1) .maximumSize(1) .expireAfterWrite(Duration.ofSeconds(10)) .build(); return cache; } }使用Caffeinejava //存数据 cache.put("键","值");java //取数据,若无数据返回Null cache.getIfPresent("键"); //取数据,若无数据执行方法 cache.get("键",key->{ return ""; });Nginx使用RedisNginx配置修改使用前需要导入OpenResty```nginxuser nobody;worker_processes 1; error_log logs/error.log;events { worker_connections 1024; }http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; #导入lua模块 lua_package_path "user/local/openrestry/lualib/?.lua;;"; #导入c模块 lua_package_cpath "user/local/openrestry/lualib/?.so;;"; #设置集群 upstream tomcat-cluster{ server tomcatIp1:端口; server tomcatIp2:端口; } server { #监听端口和域名 listen 8081; server_name localhost; #映射位置 location / { root html; index index.html index.htm; } location /item{ proxy_pass http://tomcat-cluster; } #设置某接口监听由lua脚本控制 location ~ /api/item/(\d+){ # 默认响应类型 default_type application/json; # 响应结果由lua/item.lua文件决定 content_by_lua_file lua/item.lua } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } } ```导入common.lua```lua -- 导入redis local redis = require('resty.redis') -- 初始化redis local red = redis:new() red:set_timeouts(1000,1000,1000)-- 关闭redis连接的工具方法，其实是放入连接池 local function close_redis(red) local pool_max_idle_time = 10000 -- 连接的空闲时间，单位是毫秒 local pool_size = 100 --连接池大小 local ok, err = red:set_keepalive(pool_max_idle_time, pool_size) if not ok then ngx.log(ngx.ERR, "放入redis连接池失败: ", err) end end -- 查询redis的方法 ip和port是redis地址，key是查询的key local function read_redis(ip, port, key) -- 获取一个连接 local ok, err = red:connect(ip, port) if not ok then ngx.log(ngx.ERR, "连接redis失败 : ", err) return nil end -- 查询redis local resp, err = red:get(key) -- 查询失败处理 if not resp then ngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key) end --得到的数据为空处理 if resp == ngx.null then resp = nil ngx.log(ngx.ERR, "查询Redis数据为空, key = ", key) end close_redis(red) return resp end -- 封装函数，发送http请求，并解析响应 local function read_http(path, params) local resp = ngx.location.capture(path,{ method = ngx.HTTP_GET, args = params, }) if not resp then -- 记录错误信息，返回404 ngx.log(ngx.ERR, "http not found, path: ", path , ", args: ", args) ngx.exit(404) end return resp.body end -- 将方法导出 local _M = {read_http = read_http, read_redis = read_redis }return _M ```导入item.lua```lua -- 导入common库,获取发送http请求方法 local common = require('common') local read_http = common.read_http local read_redis = common.read_redis -- 导入json库 local cjson = require('cjson')-- 获取路径参数 local id = ngx.var[1]-- 封装查询函数,先查redis,后发http function read_data(key,path,params) -- 先查redis local resp = read_redis("127.0.0.1",6379,key) -- 判断查询结果,未空则发http if not resp then ngx.log(ngx.ERR,"redis查询失败,尝试查询http,key:",key) resp = read_http(path,params) end return resp end -- 查询 local itemJson = read_data("item.id"..id,"/item/"..id,nil) local itemStockJson = read_data("item.stock.id"..id,"/item/stock/"..id,nil) -- 组合json local item = cjson.decode(itemJson) local stock = cjson.decode(itemStockJson) item.stock = stock.stock item.sold = stock.sold-- 返回结果 ngx.say(cjson.encode(item)) ```Nginx本地缓存OpenResty为Nginx提供了shard dict的功能,在nginx的多个worker之间共享数据,实现缓存功能.Nginx配置修改在nginx.conf中添加配置```nginx开启本地缓存,名字为item_cache,大小为150mlua_shard_dict item_cache 150m ``````nginxuser nobody;worker_processes 1; error_log logs/error.log;events { worker_connections 1024; }http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; #导入lua模块 lua_package_path "user/local/openrestry/lualib/?.lua;;"; #导入c模块 lua_package_cpath "user/local/openrestry/lualib/?.so;;"; #开启本地缓存,名字为item_cache,大小为150m lua_shard_dict item_cache 150m; #设置集群 upstream tomcat-cluster{ server tomcatIp1:端口; server tomcatIp2:端口; } server { #监听端口和域名 listen 8081; server_name localhost; #映射位置 location / { root html; index index.html index.htm; } location /item{ proxy_pass http://tomcat-cluster; } #设置某接口监听由lua脚本控制 location ~ /api/item/(\d+){ # 默认响应类型 default_type application/json; # 响应结果由lua/item.lua文件决定 content_by_lua_file lua/item.lua } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } } ```本地缓存操作在lua中操作lua -- 获取本地缓存对象 local item_cache = ngx.shared.item_cache -- 存储,指定key,value,过期时间,单位s,默认0为永不过期 item_cache:set('key','value',1000) -- 读取 local val = item_cache:get('key')修改item.lua如在上文的item.lua中,可先查询本性再查询redis,最后发送http```lua -- 导入common库,获取发送http请求方法 local common = require('common') local read_http = common.read_http local read_redis = common.read_redis -- 导入json库 local cjson = require('cjson') -- 获取本地缓存对象 local item_cache = ngx.shared.item_cache-- 获取路径参数 local id = ngx.var[1]-- 封装查询函数,先查redis,后发http function read_data(key,expire,path,params) -- 查询本地缓存 local val = item_cache:get(key) if not val then -- 查redis ngx.log(ngx.ERR,"本性缓存查询失败,尝试查询redis,key:",key) val = read_redis("127.0.0.1",6379,key) -- 判断查询结果,未空则发http if not val then ngx.log(ngx.ERR,"redis查询失败,尝试查询http,key:",key) val = read_http(path,params) end end item_cache:set(key,val,expire) return val end-- 查询 local itemJson = read_data("item.id"..id,1800,"/item/"..id,nil) local itemStockJson = read_data("item.stock.id"..id,60,"/item/stock/"..id,nil) -- 组合json local item = cjson.decode(itemJson) local stock = cjson.decode(itemStockJson) item.stock = stock.stock item.sold = stock.sold-- 返回结果 ngx.say(cjson.encode(item)) ```缓存同步MQ同步通过MQ,当item服务更新时,发布更新消息,cache服务更新redis优点:快缺点:依赖MQ可靠性Cannel同步通过监听mysql的binlog,一旦触发增删改事件,则同步更新redis优点:快,低耦合(不用再item服务写代码)缺点:依赖数据库可靠性安装Cannel1.修改msql配置sh vi /tmp/mysql/conf/my.cnfini log-bin=/var/lib/mysql/mysql-bin binlog-do-db=heima配置解读： log-bin=/var/lib/mysql/mysql-bin：设置binary log文件的存放地址和文件名，叫做mysql-bin binlog-do-db=heima：指定对哪个database记录binary log events，这里记录heima这个库 ### 2.设置用户权限接下来添加一个仅用于数据同步的账户，出于安全考虑，这里仅提供对heima这个库的操作权限。mysql create user canal@'%' IDENTIFIED by 'canal'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%' identified by 'canal'; FLUSH PRIVILEGES;重启mysql容器即可docker restart mysql测试设置是否成功：在mysql控制台，或者Navicat中，输入命令：show master status;3.创建网络我们需要创建一个网络，将MySQL、Canal、MQ放到同一个Docker网络中：sh docker network create heima让mysql加入这个网络：sh docker network connect heima mysql4.安装Cannel网络上下载cannel,可以上传到虚拟机，然后通过命令导入：docker load -i canal.tar然后运行命令创建Canal容器：sh docker run -p 11111:11111 --name canal \ -e canal.destinations=heima \ -e canal.instance.master.address=mysql:3306 \ -e canal.instance.dbUsername=canal \ -e canal.instance.dbPassword=canal \ -e canal.instance.connectionCharset=UTF-8 \ -e canal.instance.tsdb.enable=true \ -e canal.instance.gtidon=false \ -e canal.instance.filter.regex=heima\\..* \ --network heima \ -d canal/canal-server:v1.1.5说明: -p 11111:11111：这是canal的默认监听端口 -e canal.instance.master.address=mysql:3306：数据库地址和端口，如果不知道mysql容器地址，可以通过docker inspect 容器id来查看 -e canal.instance.dbUsername=canal：数据库用户名 -e canal.instance.dbPassword=canal ：数据库密码 -e canal.instance.filter.regex=：要监听的表名称 表名称监听支持的语法：mysql 数据解析关注的表，Perl正则表达式. 多个正则之间以逗号(,)分隔，转义符需要双斜杠(\\) 常见例子： 1. 所有表：.* or .*\\..* 2. canal schema下所有表： canal\\..* 3. canal下的以canal打头的表：canal\\.canal.* 4. canal schema下的一张表：canal.test1 5. 多个规则组合使用然后以逗号隔开：canal\\..*,mysql.test1,mysql.test2使用Cannel导入依赖xml <dependency> <groupId>top.javatool</groupId> <artifactId>canal-spring-boot-starter</artifactId> <version>1.2.1-RELEASE</version> </dependency>编写配置yaml canal: destination: heima # canal的集群名字，要与安装canal时设置的名称一致 server: 192.168.150.101:11111 # canal服务地址修改实体类通过@Id、@Column、等注解完成Item与数据库表字段的映射：```java package com.heima.item.pojo;import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Transient;import javax.persistence.Column; import java.util.Date;@Data @TableName("tb_item") public class Item { @TableId(type = IdType.AUTO) @Id private Long id;//商品id @Column(name = "name") private String name;//商品名称 private String title;//商品标题 private Long price;//价格（分） private String image;//商品图片 private String category;//分类名称 private String brand;//品牌名称 private String spec;//规格 private Integer status;//商品状态 1-正常，2-下架 private Date createTime;//创建时间 private Date updateTime;//更新时间 @TableField(exist = false) @Transient private Integer stock; @TableField(exist = false) @Transient private Integer sold; } ```编写监听类通过实现EntryHandler<T>接口编写监听器，监听Canal消息。注意两点： 实现类通过@CanalTable("tb_item")指定监听的表信息 EntryHandler的泛型是与表对应的实体类 ```java @CanalTable("tb_item") @Component public class ItemHandler implements EntryHandler {@Autowired private RedisHandler redisHandler; @Autowired private Cache<Long, Item> itemCache; //发生新增时触发事件 @Override public void insert(Item item) { // 写数据到JVM进程缓存 .... // 写数据到redis ... } //发生更新时触发事件 @Override public void update(Item before, Item after) { // 写数据到JVM进程缓存 ... // 写数据到redis ... } //发生删除时触发事件 @Override public void delete(Item item) { // 删除数据到JVM进程缓存 ... // 删除数据到redis ... } } ```
任务调度技术任务调度是特定业务场景下的定时任务处理，在分布式架构下，分布式调度框架的设计显得尤为重要。这里简要介绍了两种常用的分布式调度框架Quartz和xxl-job的特性、基本架构和参数配置，以加深了解。Spring Schedulespring自带一种基于注解的任务调度在application中添加@EnableScheduling注解```java @SpringBootApplication @EnableScheduling //开启定时任务功能 public class SpringbootTaskApplication {public static void main(String[] args) { SpringApplication.run(SpringbootTaskApplication.class, args); } }```在定时任务类上添加规则和方法,使用cron表达式```java @Slf4j @Component public class MyJob {//@Scheduled(cron = "0/${mytask.abc:15} * * * * ?") 在application.yml配置中添加参数可以通过配置方式指定规则 mytask.abc = 10 ; @Scheduled(cron = "0,15,30,45 * * * * ?") public void hello(){ log.info("say hello"); } } ```Quartz介绍这是一个作业调度的开源项目,一般可用和spring结合使用,在实际开发中可用用 Quartz做一个定时任务,比如每小时执行一次,或每个月上午10点执行一次,每个月下午5点执行一次安装```xml org.springframework.boot spring-boot-starter-quartz ```使用 编写任务信息 编写任务处理规则 配置调度中心(启动时开启任务调度) 首先,先编写job类```java public class Job1 implements Job { private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");private void before() { System.out.println("任务1：开始执行-" + dateFormat.format(new Date())); } @Override public void execute(JobExecutionContext arg0) throws JobExecutionException { before(); System.out.println("任务1：业务逻辑。。。"); after(); } private void after() { System.out.println("任务1：执行结束"); System.out.println(); } ```再通过工具类配置规则```java @Component public class QuartzSchedulerManager { @Autowired private Scheduler scheduler;// 开始执行定时器 public void startJob() throws SchedulerException { startJob1(scheduler); startJob2(scheduler); scheduler.start(); } // 获取Job信息 public String getJobInfo(String name, String group) throws SchedulerException { TriggerKey triggerKey = new TriggerKey(name, group); CronTrigger cronTrigger = (CronTrigger) scheduler.getTrigger(triggerKey); return String.format("time:%s,state:%s", cronTrigger.getCronExpression(), scheduler.getTriggerState(triggerKey).name()); } // 修改某个任务的执行时间 public boolean modifyJob(String name, String group, String time) throws SchedulerException { Date date = null; TriggerKey triggerKey = new TriggerKey(name, group); CronTrigger cronTrigger = (CronTrigger) scheduler.getTrigger(triggerKey); String oldTime = cronTrigger.getCronExpression(); if (!oldTime.equalsIgnoreCase(time)) { CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(time); CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(name, group) .withSchedule(cronScheduleBuilder).build(); date = scheduler.rescheduleJob(triggerKey, trigger); } return date != null; } // 暂停所有任务 public void pauseAllJob() throws SchedulerException { scheduler.pauseAll(); } // 暂停某个任务 public void pauseJob(String name, String group) throws SchedulerException { JobKey jobKey = new JobKey(name, group); JobDetail jobDetail = scheduler.getJobDetail(jobKey); if (jobDetail == null) return; scheduler.pauseJob(jobKey); } // 恢复所有任务 public void resumeAllJob() throws SchedulerException { scheduler.resumeAll(); } // 恢复某个任务 public void resumeJob(String name, String group) throws SchedulerException { JobKey jobKey = new JobKey(name, group); JobDetail jobDetail = scheduler.getJobDetail(jobKey); if (jobDetail == null) return; scheduler.resumeJob(jobKey); } // 删除某个任务 public void deleteJob(String name, String group) throws SchedulerException { JobKey jobKey = new JobKey(name, group); JobDetail jobDetail = scheduler.getJobDetail(jobKey); if (jobDetail == null) return; scheduler.deleteJob(jobKey); } // 启动任务1 private void startJob1(Scheduler scheduler) throws SchedulerException { // 通过JobBuilder构建JobDetail实例，JobDetail规定其job只能是实现Job接口的实例 JobDetail jobDetail = JobBuilder.newJob(Job1.class).withIdentity("job1", "group1").build(); // 基于表达式构建触发器 CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?"); // CronTrigger表达式触发器 继承于Trigger。TriggerBuilder 用于构建触发器实例 CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("job1", "group1") .withSchedule(cronScheduleBuilder).build(); scheduler.scheduleJob(jobDetail, cronTrigger); } // 启动任务2 private void startJob2(Scheduler scheduler) throws SchedulerException { // 通过JobBuilder构建JobDetail实例，JobDetail规定其job只能是实现Job接口的实例 JobDetail jobDetail = JobBuilder.newJob(Job2.class).withIdentity("job2", "group2").build(); // 基于表达式构建触发器 CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?"); // CronTrigger表达式触发器 继承于Trigger。TriggerBuilder 用于构建触发器实例 CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("job2", "group2") .withSchedule(cronScheduleBuilder).build(); scheduler.scheduleJob(jobDetail, cronTrigger); } } ```若希望服务启动时运行,写以下配置类```java @Configuration public class QuartzListener implements ApplicationListener { @Autowired private QuartzSchedulerManager quartzSchedulerManager;// 初始启动quartz @Override public void onApplicationEvent(ContextRefreshedEvent event) { try { quartzSchedulerManager.startJob(); System.out.println("任务已经启动..."); } catch (SchedulerException e) { e.printStackTrace(); } } // 初始注入scheduler @Bean public Scheduler scheduler() throws SchedulerException{ SchedulerFactory schedulerFactoryBean = new StdSchedulerFactory(); return schedulerFactoryBean.getScheduler(); } } ```若希望通过api启动任务类,在controler里执行以下api```java @RestController @RequestMapping("") public class QuartzController { @Autowired private QuartzSchedulerManager quartzSchedulerManager;// @Description: 获取定时器信息 @GetMapping("/info") public String getQuartzJob(String name, String group) { String info = null; try { info = quartzSchedulerManager.getJobInfo(name, group); } catch (SchedulerException e) { e.printStackTrace(); } return info; } // @Description: 修改定时器的 执行时间 @PostMapping("/modify") public boolean modifyQuartzJob(String name, String group, String time) { boolean flag = true; if (!CronExpression.isValidExpression(time)) { throw new RuntimeException("非法的cron表达式"); } try { flag = quartzSchedulerManager.modifyJob(name, group, time); } catch (SchedulerException e) { e.printStackTrace(); } return flag; } // @Description: 启动所有定时器 @PostMapping("/start") public void startQuartzJob() { try { quartzSchedulerManager.startJob(); } catch (SchedulerException e) { e.printStackTrace(); } } // @Description: 暂停指定 定时器 @PostMapping(value = "/pause") public void pauseQuartzJob(String name, String group) { try { quartzSchedulerManager.pauseJob(name, group); } catch (SchedulerException e) { e.printStackTrace(); } } // 暂停所有定时器 @PostMapping(value = "/pauseAll") public void pauseAllQuartzJob() { try { quartzSchedulerManager.pauseAllJob(); } catch (SchedulerException e) { e.printStackTrace(); } } // 删除指定定时器 @PostMapping(value = "/delete") public void deleteJob(String name, String group) { try { quartzSchedulerManager.deleteJob(name, group); } catch (SchedulerException e) { e.printStackTrace(); } } } ```XXL-JOB介绍xxl-job是基于分布式集群的任务调度模块 单机情况下,一个任务调度中心可以发起一种任务调度 但在集群情况下,多个任务调度中心就会重复发一种任务调度 如我们希望在每个月1号清空积分,但用了集群分布式,有了多个调度中心,那么每个调度中心都会清空积分,这样任务就重复了,我们希望只有一个任务中心发起任务调度即可,而xxl-job就是专门解决分布式集群任务调度的开源框架 执行服务:编写任务信息 调度服务:编写任务规则 执行服务:配置调度中心(启动时开启任务调度) cron表达式对于cron表达式,如果自己写的话会比较麻烦,可以使用网络上的cron表达式在线 生成工具https://cron.qqe2.com/