电商型企业网站建设,nodejs可以做网站么,鹰潭网站建设yt1983,android基础入门教程文章目录 -2 flink-1 linux of viewlinux查看占用cup最高的10个进程的命令#xff1b; 〇、分布式锁 分布式事务0-1分布式锁--包含CAP理论模型概述分布式锁#xff1a;分布式锁应该具备哪些条件#xff1a;分布式锁的业务场景#xff1a; 分布式锁的实现方式有#… 文章目录 -2 flink-1 linux of viewlinux查看占用cup最高的10个进程的命令 〇、分布式锁 分布式事务0-1分布式锁--包含CAP理论模型概述分布式锁分布式锁应该具备哪些条件分布式锁的业务场景 分布式锁的实现方式有基于Zookeeper - 分布式锁实现思想优缺点基于Redis - 分布式锁实现思想实现思想的具体步骤优缺点Redis分布式锁实现-例子方案一改进 方案一 再改进 方案一方案二改进方案二再改进方案二再再次改进 方案二分段锁 基于数据库 - 分布式锁实现思想A. 悲观锁排他锁优点缺点高并发情况下也会造成占用过多的应用线程导致业务无法正常响应。B. 乐观锁 0-2 分布式事务锁的实现原理synchronized 锁的实现原理redis中有哪些锁分布式锁的特征 一、 Java集合1.2 流程图关系1.3 底层实现1.4 集合与数组的区别1.4.1 元素类型1.4.2 元素个数 1.5 集合的好处1.6 List集合我们以ArrayList集合为例1.7 迭代器的常用方法1.8 ArrayList、LinkedList和Vector的区别1.8.1 说出ArrayList,Vector, LinkedList的存储性能和特性1.8.2 多线程场景下如何使用 ArrayList1.8.3 为什么 ArrayList 的 elementData 加上 transient 修饰 1.9 Set集合的特点1.9.1 说一下 HashSet 的实现原理1.9.2 HashSet如何检查重复HashSet是如何保证数据不可重复的 1.10 TreeSet对元素进行排序的方式1.11 ListSetMap集合的特点 区别1.12 HashMap和Hashtable的区别1.3 HashSet 与 HashMap的区别1.4 说一下HashMap的实现原理(非常重要)①HashMap的工作原理HashMap存储结构常用的变量HashMap 构造函数tableSizeFor() put()方法详解hash()计算原理resize() 扩容机制get()方法为什么HashMap链表会形成死循环1.4.1 HashMap 基于 Hash 算法实现的 1.5 HashMap的扩容操作是怎么实现的1.6 HashMap是怎么解决哈希冲突的1.7 什么是哈希1.8 所有散列函数都有如下一个基本特性1.9 什么是哈希冲突 二、 Java基础-IO流反射堆与栈面向对象三大特性String、StringBuffer、StringBuilder自动装箱与拆箱基本数据类型与包装类的区别int 和 Integer 有什么区别应用场景的区别 堆和栈的区别重点来说一下堆和栈那么堆和栈是怎么联系起来的呢? 堆与栈的区别 很明显延伸关于Integer和int的比较Java 为每个原始类型提供了包装类型 Double和double的区别面向对象和面向过程的区别2.1 面向对象三大特性2.1.1 封装 继承 多态2.1.2 其中Java 面向对象编程三大特性封装 继承 多态2.1.3 关于继承如下 3 点请记住2.1.4 什么是多态机制Java语言是如何实现多态的2.1.5 Java实现多态有三个必要条件继承、重写、向上转型。 2.2 类与接口2.2.1 抽象类和接口的对比2.2.3 普通类和抽象类有哪些区别2.2.4 抽象类能使用 final 修饰吗2.2.5 重写与重载2.2.6 重载Overload和重写Override的区别。2.2.7 对象相等判断(1) 和 equals 的区别是什么 2.2.8 hashCode 与 equals (重要) 2.3 反射-反射-反射-反射2.3.1 什么是反射机制2.3.2 反射机制优缺点2.3.3 请简介你对Java反射的理解以及使用场景 3. io流 - IO、NIO、BIO、AIOjava 中 IO 流分为几种?按照流的流向分可以分为输入流和输出流按照流的角色划分为节点流和处理流。IO流主要的分类方式有以下3种IO和NIO的区别BIO,NIO,AIO 有什么区别?Files的常用方法都有哪些 4. String、StringBuffer、StringBuilder字符型常量和字符串常量的区别什么是字符串常量池String 是最基本的数据类型吗String有哪些特性String为什么是不可变的吗如何将字符串反转 数组有没有 length()方法String 有没有 length()方法String 类的常用方法都有那些在使用 HashMap 的时候用 String 做 key 有什么好处String和StringBuffer、StringBuilder的区别可变性线程安全性性能 5. maven常用打包命令总结值传递和引用传递有什么区别Java包 JDK 中常用的包有哪些import java和javax有什么区别 三、 多线程-1 高并发〇、使用多线程的场景1. 为什么使用多线程 1. 线程概述1.1 线程和进程1.2 并发和并行1.3 多线程的优势1.4 程序运行原理1.5 主线程 1.6 线程的 6 种状态2. 线程的创建和启动2.1 Thread类2.2创建线程有哪几种方法2.2.1 继承**Thread**类重写**Run**方法其中**Thread**类本身也是实现了**Runnable**接口2.2.2 实现**Runnable**接口重写**run**方法2.2.3 实现 **Callable** 接口重写 **call**方法有返回值2.2.4 通过线程池创建线程 4 线程池的核心参数有哪些4个参数的设计: 四、Springspring常用的注解什么是基于Java的Spring注解配置? 给一些注解的例子怎样开启注解装配 Spring的优缺点是什么Spring 框架中都用到了哪些设计模式Spring 应用程序有哪些不同组件使用 Spring 有以下方式Spring面向切面编程(AOP)什么是AOPSpring AOP and AspectJ AOP 的区别Spring AOP中的动态代理如何理解 Spring 中的代理解释一下Spring AOP里面的几个名词Spring在运行时通知对象Spring切面可以应用5种类型的通知什么是切面 Aspect Spring控制反转(IOC)什么是Spring IOC 容器控制反转(IoC)有什么作用IOC的优点是什么Spring 的 IoC支持哪些功能Spring 的 IoC 设计支持以下功能BeanFactory 和 ApplicationContext有什么区别 什么是Spring的依赖注入依赖注入的基本原则依赖注入有什么优势查找定位操作与应用代码完全无关。有哪些不同类型的依赖注入实现方式构造器依赖注入和 Setter方法注入的区别 Spring事务的实现方式和实现原理Spring支持的事务管理类型 spring 事务实现方式有哪些Spring中事务的实现方式1、编程式---实现事务 2、声明式---实现事务 说一下Spring的事务传播行为spring事务的实现原理Spring 事务失效的7种场景1.1、未启用[spring事务管理](https://so.csdn.net/so/search?qspring事务管理spm1001.2101.3001.7020)功能1.2、方法不是public类型的1.3、数据源未配置事务管理器1.4、自身调用问题1.5、异常类型错误1.6、异常被吞了1.7、业务和spring事务代码必须在一个线程中2、如何快速定位事务相关bug 说一下 spring 的事务隔离Spring框架的事务管理有哪些优点你更倾向用哪种事务管理类型Spring Beans请解释Spring Bean的生命周期解释Spring支持的几种bean的作用域Spring容器中的bean可以分为5个范围 Spring如何处理线程并发问题Spring基于xml注入bean的几种方式Spring的自动装配在Spring框架xml配置中共有5种自动装配基于注解的方式 Spring 框架中都用到了哪些设计模式spring 是如何开启事务的核心原理1 基于注解开启事务2 基于代码来开启事务 spring的事务在什么场景下会失效spring 对事务如何进行管理Spring 支持两种方式事务管理Spring的事务机制包括声明式事务和编程式事务。 Spring JDBC-Spring对事务管理的支持spring如何对bean进行自动的事务管理1、Spring事务管理概述2、TransactionDefinition3、TransactionStatus4、声明式事务管理 Spring事务的实现方式和实现原理1Spring事务的种类2spring的事务传播行为3Spring中的隔离级别4spring中事务的配置方式(5) Spring框架中有哪些不同类型的事件 五、springBootspringBoot的实现原理什么是 Spring BootSpringBoot是什么Spring Boot的核心功能Spring Boot 主要有如下优点 SpringBoot启动过程-流程Spring Boot 的核心注解是哪个什么是 JavaConfig Spring Boot 自动配置原理是什么你如何理解 Spring Boot 配置加载顺序spring boot 核心的两个配置文件什么是 Spring Profiles如何实现 Spring Boot 应用程序的安全性比较一下 Spring Security 和 Shiro 各自的优缺点 ? Spring Boot 中如何解决跨域问题 ?Spring Boot 中的监视器是什么如何在 Spring Boot 中禁用 Actuator 端点安全性我们如何监视所有 Spring Boot 微服务什么是 WebSockets如何重新加载 Spring Boot 上的更改而无需重新启动服务器Spring Boot项目如何热部署Spring Boot 打成的 jar 和普通的 jar 有什么区别 ?运行 Spring Boot 有哪几种方式Spring Boot 需要独立的容器运行吗开启 Spring Boot 特性有哪几种方式如何使用 Spring Boot 实现异常处理如何使用 Spring Boot 实现分页和排序微服务中如何实现 session 共享 ?Spring Boot 中如何实现定时任务 ? 六、 springMVC什么是Spring MVCSpring MVC的主要组件springMVC工作原理/流程MVC框架注解原理是什么Spring MVC常用的注解有哪些 Spring Cloud注册中心配置中心原理详解谈谈你对 spring Cloud 的理解 注册中心Eureka服务搭建小结 Ribbo - 负载均衡1. 负载均衡流程2. 负载均衡策略 nacos注册中心1. 配置集群1. 创建 namespace2. 配置命名空间1. nacos添加配置文件2. 从nacos拉取配置 配置中心Nacos1.搭建服务端2.搭建客户端3.动态刷新4.nacos config高可用5.nacos config高级配置 ConfigApollo 一、业务场景介绍二、Spring Cloud核心组件Eureka三、Spring Cloud核心组件Feign四、Spring Cloud核心组件Ribbon五、Spring Cloud核心组件Hystrix六、Spring Cloud核心组件Zuul七、总结使用及配置EurekaFeignRibbonHystrixZuul 七、REDIS什么是RedisRedis有5中数据类型 SSHLZredis 持久化 —— RDBRedis DataBase和 AOFAppend Only File一、redis持久化----两种方式二、redis持久化----RDB三、redis持久化----AOF四、redis持久化----AOF重写五、redis持久化----如何选择RDB和AOF六、Redis的两种持久化方式也有明显的缺点 redids事务 ACIDRedis 的事务需要先划分出三个阶段从严格意义上来说Redis 是没有事务的。因为事务必须具备四个特点原子性一致性隔离性持久性 redis部署方案standaloan(单机模式)ssentinel哨兵模式Redis Cluster 集群模式 Redis有哪些优缺点优点缺点高性能高并发: 为什么要用 Redis 而不用 map/guava 做缓存?Redis为什么这么快Redis有哪些数据类型Redis的应用场景总结一计数器缓存会话缓存全页缓存FPC查找表消息队列(发布/订阅功能)分布式锁实现 总结二什么是Redis持久化Redis 的持久化机制是什么各自的优缺点优点缺点 Redis 主从架构 八、MyBatis简介结构图Mybatis缓存一级缓存、二级缓存MyBatis是什么mybatis的实现原理JDBC编程有哪些不足之处MyBatis是如何解决这些问题的Mybatis优缺点优点缺点映射关系 MyBatis的解析和运行原理MyBatis的工作原理 MyBatis的功能架构是怎样的把Mybatis的功能架构分为三层 MyBatis的框架架构设计是怎么样的架构图如下Mybatis都有哪些Executor执行器它们之间的区别是什么Mybatis中如何指定使用哪一种Executor执行器#{}和${}的区别 在mapper中如何传递多个参数Mybatis如何执行批量操作使用foreach标签 使用ExecutorType.BATCH如何获取生成的主键 Mapper 编写有哪几种方式接口绑定有两种实现方式使用MyBatis的mapper接口调用时有哪些要求Mybatis的Xml映射文件中不同的Xml映射文件id是否可以重复简述Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系Mybatis是如何将sql执行结果封装为目标对象并返回的都有哪些映射形式Xml映射文件中除了常见的select|insert|updae|delete标签之外还有哪些标签Mybatis映射文件中如果A标签通过include引用了B标签的内容请问B标签能否定义在A标签的后面还是说必须定义在A标签的前面 高级查询MyBatis实现一对一一对多有几种方式怎么操作的Mybatis是否可以映射Enum枚举类 动态SQLMybatis动态sql是做什么的都有哪些动态sql能简述一下动态sql的执行原理不 插件模块Mybatis是如何进行分页的分页插件的原理是什么简述Mybatis的插件运行原理以及如何编写一个插件。 九、数据库-MySQLMySQL主从、集群模式简单介绍1、主从模式 Replication2、集群模式3、主从模式部署注意事项 UNION 和 UNION ALL 区别分库分表1.垂直拆分2、水平拆分 MySQL有哪些数据类型1、整数类型**2、实数类型**3、字符串类型**4、枚举类型ENUM**5、日期和时间类型** MySQL中varchar与char有哪些区别1、固定长度 可变长度2、存储方式3、存储容量 Mysql的索引和主键的区别数据库基础知识数据库的事务的基本特性事务的四大特性(ACID)4.1、原子性Atomicity4.2、一致性Consistency4.3、隔离性Isolation4.4、持久性Durability 事务的隔离级别5.1、事务不考虑隔离性可能会引发的问题1、脏读2、不可重复读3、虚读(幻读) 5.2、事务隔离性的设置语句事务隔离级别如何在java代码中使用Java代码演示及隔离级别的设置5.3、使用MySQL数据库演示不同隔离级别下的并发问题1、当把事务的隔离级别设置为read uncommitted时会引发脏读、不可重复读和虚读2、当把事务的隔离级别设置为read committed时会引发不可重复读和虚读但避免了脏读3、当把事务的隔离级别设置为repeatable read(mysql默认级别)时会引发虚读但避免了脏读、不可重复读4、当把事务的隔离级别设置为Serializable时会避免所有问题 数据库三大范式是什么mysql有关权限的表都有哪几个MySQL的binlog有有几种录入格式分别有什么区别 MySQL存储引擎MyISAM与InnoDB区别MYsql如何查询到一条数据的简述其查询原理BufferPool缓存机制事务Mysql的InnoDB是如何使用索引的其索引的原理是什么B树索引的优点 MySQL-索引innoDB,B树索引MySQL 索引失效如何解决为什么要建立索引哪些情况适合建立索引哪些情况不适合建立索引为什么索引使用的是 B 树——重点 索引分为哪几类聚簇索引和非聚簇索引的区别什么是[聚簇索引](https://so.csdn.net/so/search?q聚簇索引spm1001.2101.3001.7020)重点非聚簇索引 聚簇索引和非聚簇索引的区别主要有以下几个什么叫回表重点 MySQL索引失效的几种情况重点MySQL索引优化手段有哪些什么叫回表重点什么叫索引覆盖重点 索引-索引-索引什么是索引索引有哪些优缺点 索引有哪几种类型 LEARNING_CONTENT 一个分布式锁的解决方案另一个是分布式事务的解决方案
-2 flink
链接flink参考文章
-1 linux of view
参考链接 linux常见面试题
linux查看占用cup最高的10个进程的命令
参考文章linux查看cpu占用最高的进程
〇、分布式锁 分布式事务
0-1分布式锁–包含CAP理论模型
参考文章-分布式锁
概述
随着互联网技术的不断发展用户量的不断增加越来越多的业务场景需要用到分布式系统。
分布式系统有一个著名的理论CAP指在一个分布式系统中最多只能同时满足下面三项中的两项
一致性Consistency在分布式系统中的所有数据备份在同一时刻是否同样的值等同于所有节点访问同一份最新的数据副本 可用性Availability保证每个请求不管成功或者失败都有响应 分区容错性Partition tolerance系统中任意信息的丢失或失败不会影响系统的继续运作
所以在设计系统时往往需要权衡在CAP中作选择要么AP要么CP、要么AC。
当然这个理论也并不一定完美不同系统对CAP的要求级别不一样选择需要考虑方方面面。
而在分布式系统中访问共享资源就需要一种互斥机制来防止彼此之间的互相干扰以保证一致性这个时候就需要使用分布式锁。
分布式锁
当在分布式模型下数据只有一份或有限的此时需要利用锁技术来控制某一时刻修改数据的进程数。这种锁即为分布式锁。
为了保证一个方法或属性在高并发的情况下, 同一时间只能被同一个线程执行在传统单体应用单机部署的情况下可以使用并发处理相关的功能进行互斥控制。但是随着业务发展的需要原单体单机部署的系统被演化成分布式集群系统后由于分布式系统多线程、多进程并且分布在不同机器上这将使原单机部署情况下的并发控制锁策略失效单纯的应用并不能提供分布式锁的能力。为了解决这个问题就需要一种跨机器的互斥机制来控制共享资源的访问这就是分布式锁要解决的问题
分布式锁应该具备哪些条件
互斥性在分布式系统环境下一个方法在同一时间只能被一个机器的一个线程执行 高可用的获取锁与释放锁 高性能的获取锁与释放锁 可重入性具备可重入特性具备锁失效机制防止死锁即就算一个客户端持有锁的期间崩溃而没有主动释放锁也需要保证后续其他客户端能够加锁成功 非阻塞具备非阻塞锁特性即没有获取到锁将直接返回获取锁失败
分布式锁的业务场景
互联网秒杀商品库存 抢优惠券
分布式锁的实现方式有
主要有几种实现方式
基于数据库实现 基于Zookeeper实现 基于Redis实现
如图
分布式锁对比 从理解的难易程度角度从低到高数据库 缓存 Zookeeper
从实现的复杂性角度从低到高Zookeeper 缓存 数据库
从性能角度从高到低缓存 Zookeeper 数据库
从可靠性角度从高到低Zookeeper 缓存 数据库
基于Zookeeper - 分布式锁
实现思想
ZooKeeper是一个为分布式应用提供一致性服务的开源组件它内部是一个分层的文件系统目录树结构规定同一个目录下只能有一个唯一文件名。
基于ZooKeeper实现分布式锁的步骤如下
创建一个目录mylock
线程A想获取锁就在mylock目录下创建临时顺序节点
获取mylock目录下所有的子节点然后获取比自己小的兄弟节点如果不存在则说明当前线程顺序号最小获得锁
线程B获取所有节点判断自己不是最小节点设置监听比自己次小的节点
线程A处理完删除自己的节点线程B监听到变更事件判断自己是不是最小的节点如果是则获得锁。
整个过程如图
业界推荐直接使用Apache的开源库Curator它是一个ZooKeeper客户端Curator提供的InterProcessMutex是分布式锁的实现acquire方法用于获取锁release方法用于释放锁。
使用方式很简单
InterProcessMutex interProcessMutex new InterProcessMutex(client,/anyLock);
interProcessMutex.acquire();
interProcessMutex.release(); 其他分布式锁的核心源码如下
private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception{ boolean haveTheLock false; boolean doDelete false; try { if ( revocable.get() ! null ) { client.getData().usingWatcher(revocableWatcher).forPath(ourPath); } while ((client.getState() CuratorFrameworkState.STARTED) !haveTheLock ) { // 获取当前所有节点排序后的集合 ListString children getSortedChildren(); // 获取当前节点的名称 String sequenceNodeName ourPath.substring(basePath.length() 1); // 1 to include the slash // 判断当前节点是否是最小的节点 PredicateResults predicateResults driver.getsTheLock(client, children, sequenceNodeName, maxLeases); if ( predicateResults.getsTheLock() ) { // 获取到锁 haveTheLock true; } else { // 没获取到锁对当前节点的上一个节点注册一个监听器 String previousSequencePath basePath / predicateResults.getPathToWatch(); synchronized(this){ Stat stat client.checkExists().usingWatcher(watcher).forPath(previousSequencePath); if ( stat ! null ){ if ( millisToWait ! null ){ millisToWait - (System.currentTimeMillis() - startMillis); startMillis System.currentTimeMillis(); if ( millisToWait 0 ){ doDelete true; // timed out - delete our node break; } wait(millisToWait); }else{ wait(); } } } // else it may have been deleted (i.e. lock released). Try to acquire again } } } catch ( Exception e ) { doDelete true; throw e; } finally{ if ( doDelete ){ deleteOurPath(ourPath); } } return haveTheLock;
}
其实 Curator 实现分布式锁的底层原理和上面分析的是差不多的。如图详细描述其原理
另外可基于Zookeeper自身的特性和原生Zookeeper API自行实现分布式锁。
优缺点
优点
可靠性非常高 性能较好 CAP模型属于CP基于ZAB一致性算法实现
**缺点 **
性能并不如Redis主要原因是在写操作即获取锁释放锁都需要在Leader上执行然后同步到follower 实现复杂度高
基于Redis - 分布式锁
实现思想
主要是基于命令SETNX key value
命令官方文档https://redis.io/commands/setnx
用法可参考Redis命令参考
如图
实现思想的具体步骤
获取锁的时候使用setnx加锁并使用expire命令为锁添加一个超时时间超过该时间则自动释放锁锁的value值为一个随机生成的UUID通过此处释放锁的时候进行判断。 获取锁的时候还设置一个获取的超时时间若超过这个时间则放弃获取锁。 释放锁的时候通过UUID判断是不是该锁若是该锁则执行delete进行锁释放。
优缺点
优点
性能非常高可靠性较高CAP模型属于AP
缺点
复杂度较高无一致性算法可靠性并不如Zookeeper锁删除失败 过期时间不好控制非阻塞获取失败后需要轮询不断尝试获取锁比较消耗性能占用cpu资源
Redis分布式锁实现-例子
以减库存接口为例子访问接口的时候自动减商品的库存
方案一
Service
public class RedisLockDemo {Autowiredprivate StringRedisTemplate redisTemplate;public String deduceStock() {ValueOperationsString, String valueOperations redisTemplate.opsForValue();//获取redis中的库存int stock Integer.valueOf(valueOperations.get(stock));if (stock 0) {int newStock stock - 1;valueOperations.set(stock, newStock );System.out.println(扣减库存成功, 剩余库存: newStock);} else {System.out.println(库存已经为0不能继续扣减);}return success;}
}表示
先从Redis中读取stock的值表示商品的库存判断商品库存是否大于0如果大于0则库存减1然后再保存到Redis里面去否则就报错
改进 方案一
这种简单的从Redis读取、判断值再减1保存到Redis的操作很容易在并发场景下出问题
商品超卖
比如
假设商品的库存有50个有3个用户同时访问该接口先是同时读取Redis中商品的库存值即都是读取到了50即同时执行到了这一行
int stock Integer.valueOf(valueOperations.get(stock));然后减1即到了这一行
int newStock stock - 1;此时3个用户的realStock都是49然后3个用户都去设置stock为49那么就会产生库存明明被3个用户抢了理论上是应该减去3的结果库存数只减去了1导致商品超卖。
这种问题的产生原因是因为读取库存、减库存、保存到Redis这几步并不是原子操作
那么可以使用加并发锁synchronized来解决
Service
public class RedisLockDemo {Autowiredprivate StringRedisTemplate redisTemplate;public String deduceStock() {ValueOperationsString, String valueOperations redisTemplate.opsForValue();synchronized (this) {//获取redis中的库存int stock Integer.valueOf(valueOperations.get(stock));if (stock 0) {int newStock stock - 1;valueOperations.set(stock, newStock );System.out.println(扣减库存成功, 剩余库存: newStock);} else {System.out.println(库存已经为0不能继续扣减);}}return success;}
}注意在Java中关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块。
再改进 方案一
以上的代码在单体模式下并没太大问题但是在分布式或集群架构环境下存在问题比如架构如下 在分布式或集群架构下synchronized只能保证当前的主机在同一时刻只能有一个线程执行减库存操作但如图同时有多个请求过来访问的时候不同主机在同一时刻依然是可以访问减库存接口的这就导致问题1商品超卖在集群架构下依然存在。
解决方法
使用如下方案二的分布式锁进行解决 注意方案一并不是分布式锁
方案二
分布式锁的简单实现图如下 简单的实现代码如下
Service
public class RedisLockDemo {Autowiredprivate StringRedisTemplate redisTemplate;public String deduceStock() {ValueOperationsString, String valueOperations redisTemplate.opsForValue();String lockKey product_001;//加锁: setnxBoolean isSuccess valueOperations.setIfAbsent(lockKey, 1);if(null isSuccess || isSuccess) {System.out.println(服务器繁忙, 请稍后重试);return error;}//------ 执行业务逻辑 ----start------int stock Integer.valueOf(valueOperations.get(stock));if (stock 0) {int newStock stock - 1;//执行业务操作减库存valueOperations.set(stock, newStock );System.out.println(扣减库存成功, 剩余库存: newStock);} else {System.out.println(库存已经为0不能继续扣减);}//------ 执行业务逻辑 ----end------//释放锁redisTemplate.delete(lockKey);return success;}
}其实就是对每一个商品加一把锁代码里面是product_001
使用setnx对商品进行加锁如成功说明加锁成功如失败说明有其他请求抢占了该商品的锁则当前请求失败退出加锁成功之后进行扣减库存操作删除商品锁
以上的代码方式是有可能会造成死锁的比如说加锁成功之后扣减库存的逻辑可能抛异常了即并不会执行到释放锁的逻辑那么该商品锁是一直没有释放会成为死锁的其他请求完全无法扣减该商品的
使用try...catch...finally的方式可以解决抛异常的问题如下
Service
public class RedisLockDemo {Autowiredprivate StringRedisTemplate redisTemplate;public String deduceStock() {ValueOperationsString, String valueOperations redisTemplate.opsForValue();String lockKey product_001;try {//加锁: setnxBoolean isSuccess valueOperations.setIfAbsent(lockKey, 1);if(null isSuccess || isSuccess) {System.out.println(服务器繁忙, 请稍后重试);return error;}//------ 执行业务逻辑 ----start------int stock Integer.valueOf(valueOperations.get(stock));if (stock 0) {int newStock stock - 1;//执行业务操作减库存valueOperations.set(stock, newStock );System.out.println(扣减库存成功, 剩余库存: newStock);} else {System.out.println(库存已经为0不能继续扣减);}//------ 执行业务逻辑 ----end------} finally {//释放锁redisTemplate.delete(lockKey);}return success;}
}改进方案二
那么上面的方式是不是能够解决死锁的问题呢
其实不然除了抛异常之外比如程序崩溃、服务器宕机、服务器重启、请求超时被终止、发布、人为kill等都有可能导致释放锁的逻辑没有执行比如对商品加分布式锁成功之后在扣减库存的时候服务器正在执行重启会导致没有执行释放锁。
可以通过对锁设置超时时间来防止死锁的发生使用Redis的expire命令可以对key进行设置超时时间如图
代码实现如下
Service
public class RedisLockDemo {Autowiredprivate StringRedisTemplate redisTemplate;public String deduceStock() {ValueOperationsString, String valueOperations redisTemplate.opsForValue();String lockKey product_001;try {//加锁: setnxBoolean isSuccess valueOperations.setIfAbsent(lockKey, 1);//expire增加超时时间redisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);if(null isSuccess || isSuccess) {System.out.println(服务器繁忙, 请稍后重试);return error;}//------ 执行业务逻辑 ----start------int stock Integer.valueOf(valueOperations.get(stock));if (stock 0) {int newStock stock - 1;//执行业务操作减库存valueOperations.set(stock, newStock );System.out.println(扣减库存成功, 剩余库存: newStock);} else {System.out.println(库存已经为0不能继续扣减);}//------ 执行业务逻辑 ----end------} finally {//释放锁redisTemplate.delete(lockKey);}return success;}
}加锁成功之后把锁的超时时间设置为10秒即10秒之后自动会释放锁避免死锁的发生。
再改进方案二
但是上面的方式同样会产生死锁问题加锁和对锁设置超时时间并不是原子操作在加锁成功之后即将执行设置超时时间的时候系统发生崩溃同样还是会导致死锁。
改进图案如下 对此有两种做法
lua脚本 set原生命令Redis 2.6.12版本及以上
一般是推荐使用set命令Redis官方在2.6.12版本对set命令增加了NX、EX、PX等参数即可以将上面的加锁和设置时间放到一条命令上执行通过set命令即可
命令官方文档https://redis.io/commands/set
用法可参考Redis命令参考
如图
SET key value NX 等同于 SETNX key value命令并且可以使用EX参数来设置过期时间
注意其实目前在Redis 2.6.12版本之后所说的setnx命令并非单单指Redis的SETNX key value命令一般是代指Redis中对set命令加上nx参数进行使用一般不会直接使用SETNX key value命令了
注意Redis2.6.12之前的版本只能通过lua脚本来保证原子性了。
如图
Service
public class RedisLockDemo {Autowiredprivate StringRedisTemplate redisTemplate;public String deduceStock() {ValueOperationsString, String valueOperations redisTemplate.opsForValue();String lockKey product_001;try {//加锁: setnx 和 expire增加超时时间Boolean isSuccess valueOperations.setIfAbsent(lockKey, 1, 10, TimeUnit.SECONDS);if(null isSuccess || isSuccess) {System.out.println(服务器繁忙, 请稍后重试);return error;}//------ 执行业务逻辑 ----start------int stock Integer.valueOf(valueOperations.get(stock));if (stock 0) {int newStock stock - 1;//执行业务操作减库存valueOperations.set(stock, newStock );System.out.println(扣减库存成功, 剩余库存: newStock);} else {System.out.println(库存已经为0不能继续扣减);}//------ 执行业务逻辑 ----end------} finally {//释放锁redisTemplate.delete(lockKey);}return success;}
}再再次改进 方案二
以上的方式其实还是存在着问题在高并发场景下会存在问题超时时间设置不合理导致的问题
大概的流程图可参考 流程
进程A加锁之后扣减库存的时间超过设置的超时时间这里设置的锁是10秒在第10秒的时候由于时间到期了所以进程A设置的锁被Redis释放了T5刚好进程B请求进来了加锁成功T6进程A操作完成扣减库存之后把进程B设置的锁给释放了刚好进程C请求进来了加锁成功 进程B操作完成之后也把进程C设置的锁给释放了以此类推…
解决方法也很简单
加锁的时候把值设置为唯一值比如说UUID这种随机数释放锁的时候获取锁的值判断value是不是当前进程设置的唯一值如果是再去删除
如图
实现的代码如下
Service
public class RedisLockDemo {Autowiredprivate StringRedisTemplate redisTemplate;public String deduceStock() {ValueOperationsString, String valueOperations redisTemplate.opsForValue();String lockKey product_001;String clientId UUID.randomUUID().toString();try {//加锁: setnx 和 expire增加超时时间Boolean isSuccess valueOperations.setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);if(null isSuccess || isSuccess) {System.out.println(服务器繁忙, 请稍后重试);return error;}//------ 执行业务逻辑 ----start------int stock Integer.valueOf(valueOperations.get(stock));if (stock 0) {int newStock stock - 1;//执行业务操作减库存valueOperations.set(stock, newStock );System.out.println(扣减库存成功, 剩余库存: newStock);} else {System.out.println(库存已经为0不能继续扣减);}//------ 执行业务逻辑 ----end------} finally {if (clientId.equals(valueOperations.get(lockKey))) {//释放锁redisTemplate.delete(lockKey);}}return success;}
}分段锁
怎么在高并发的场景去实现一个高性能的分布式锁呢
电商网站在大促的时候并发量很大
1若抢购不是同一个商品则可以增加Redis集群的cluster来实现因为不是同一个商品所以通过计算 key 的hash会落到不同的 cluster上
2若抢购的是同一个商品则计算key的hash值会落同一个cluster上所以加机器也是没有用的。
针对第二个问题可以使用库存分段锁的方式去实现。
分段锁
假如产品1有200个库存可以将这200个库存分为10个段存储每段20个每段存储到一个cluster上将key使用hash计算使这些key最后落在不同的cluster上。
每个下单请求锁了一个库存分段然后在业务逻辑里面就对数据库或者是Redis中的那个分段库存进行操作即可包括查库存 - 判断库存是否充足 - 扣减库存。
具体可以参照 ConcurrentHashMap 的源码去实现它使用的就是分段锁。
高性能分布式锁具体可参考链接每秒上千订单场景下的分布式锁高并发优化实践【石杉的架构笔记】
原理如图
基于数据库 - 分布式锁
实现思想
主要有两种方式
悲观锁 乐观锁
A. 悲观锁排他锁
利用select * form table where xxyy for update 排他锁
注意这里需要注意的是 where xxyyxx字段必须要走索引否则会锁表。有些情况下比如表不大mysql优化器会不走这个索引导致锁表问题。
**核心思想**以「悲观的心态」操作资源无法获得锁成功就一直阻塞着等待。
注意该方式有很多缺陷一般不建议使用。
实现
创建一张资源锁表
CREATE TABLE resource_lock (id int(4) NOT NULL AUTO_INCREMENT COMMENT 主键,resource_name varchar(64) NOT NULL DEFAULT COMMENT 锁定的资源名,owner varchar(64) NOT NULL DEFAULT COMMENT 锁拥有者,desc varchar(1024) NOT NULL DEFAULT 备注信息,update_time timestamp NOT NULL DEFAULT COMMENT 保存数据时间自动生成,PRIMARY KEY (id),UNIQUE KEY uidx_resource_name (resource_name ) USING BTREE
) ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT锁定中的资源;注意resource_name 锁资源名称必须有唯一索引
使用事务查询更新
Transaction
public void lock(String name) {ResourceLock rlock exeSql(select * from resource_lock where resource_name name for update);if (rlock null) {exeSql(insert into resource_lock(reosurce_name,owner,count) values (name, ip,0));}
}使用 for update 锁定的资源 :
如果执行成功会立即返回执行插入数据库后续再执行一些其他业务逻辑直到事务提交执行结束
如果执行失败就会一直阻塞着。
可以在数据库客户端工具上测试出来这个效果当在一个终端执行了 for update不提交事务。在另外的终端上执行相同条件的 for update会一直卡着
虽然也能实现分布式锁的效果但是会存在性能瓶颈。
优点
简单易用好理解保障数据强一致性。
缺点
1在 RR 事务级别select 的 for update 操作是基于间隙锁gap lock 实现的是一种悲观锁的实现方式所以存在阻塞问题。
2高并发情况下大量请求进来会导致大部分请求进行排队影响数据库稳定性也会耗费服务的CPU等资源。
当获得锁的客户端等待时间过长时会提示
[40001][1205] Lock wait timeout exceeded; try restarting transaction
高并发情况下也会造成占用过多的应用线程导致业务无法正常响应。
3如果优先获得锁的线程因为某些原因一直没有释放掉锁可能会导致死锁的发生。
4锁的长时间不释放会一直占用数据库连接可能会将数据库连接池撑爆影响其他服务。
5MySql数据库会做查询优化即便使用了索引优化时发现全表扫效率更高则可能会将行锁升级为表锁此时可能就更悲剧了。
6不支持可重入特性并且超时等待时间是全局的不能随便改动。
B. 乐观锁
所谓乐观锁与悲观锁最大区别在于基于 CAS思想 表中添加一个时间戳或者是版本号的字段来实现update xx set versionnew_version where xxyy and versionOld_version通过增加递增的版本号字段实现乐观锁。
不具有互斥性不会产生锁等待而消耗资源操作过程中认为不存在并发冲突只有update version失败后才能觉察到。
抢购、秒杀就是用了这种实现以防止超卖的现象。
实现
创建一张资源锁表
CREATE TABLE resource (id int(4) NOT NULL AUTO_INCREMENT COMMENT 主键,resource_name varchar(64) NOT NULL DEFAULT COMMENT 资源名,share varchar(64) NOT NULL DEFAULT COMMENT 状态,version int(4) NOT NULL DEFAULT COMMENT 版本号,desc varchar(1024) NOT NULL DEFAULT 备注信息,update_time timestamp NOT NULL DEFAULT COMMENT 保存数据时间自动生成,PRIMARY KEY (id),UNIQUE KEY uidx_resource_name (resource_name ) USING BTREE
) ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT资源;为表添加一个字段版本号或者时间戳都可以。通过版本号或者时间戳来保证多线程同时间操作共享资源的有序性和正确性。
伪代码实现
Resrouce resource exeSql(select * from resource where resource_name xxx);
boolean succ exeSql(update resource set version newVersion ... where resource_name xxx and version oldVersion);if (!succ) {// 发起重试
}实际代码中可以写个while循环不断重试版本号不一致更新失败重新获取新的版本号直到更新成功。
优缺点 优点
实现简单复杂度低 保障数据一致性
缺点
性能低并且有锁表的风险 可靠性差 非阻塞操作失败后需要轮询占用CPU资源 长时间不commit或者是长时间轮询可能会占用较多的连接资源
0-2 分布式事务
参考文章分布式事务
锁的实现原理
参考文章url:
锁的 happens-before关系 happens-before规则
程序顺序规则在一个线程中前面的操作 happens-before后面的操作锁规则对同一个锁解锁 happens-before加锁。传递性规则A happens-before BB happens-before C则A happens-before C
从这段代码看看happens-before关系线程A先执行store()线程B后执行load() 这里有13个happens-before关系。①⑤是线程A的程序顺序关系⑥~⑩是线程B的程序顺序关系⑪是锁规则关系⑫⑬是传递性关系
int value 0;
boolean finish 0;//线程A
void store(){//A加锁前的操作synchronized(this){ //B加锁value 1; //C写valuefinish true; //D写finish} //E解锁//F解锁后的操作
}//线程B
void load(){//G加锁前的操作synchronized(this){ //H加锁if(finish){ //I读finishassert value 1; //J读value}} //K解锁//L解锁后的操作
}锁的图解 从happens-before关系分析可见性 ①~⑩根据程序顺序规则只要不重排序数据依赖的指令执行结果就是正确的就可以保证在单线程内的可见性。
⑪根据锁规则E happens-before H也就是线程A解锁 happens-before 线程B加锁。
⑫根据传递性规则线程A解锁前的操作都需要对线程B加锁可见ABCDE happens-before H也就是线程A解锁及其先前操作 happens-before 线程B加锁。
⑬再根据传递性规则线程A解锁前的操作都需要对线程B加锁之后的操作可见ABCDE happens-before HIJKL最终得出线程A解锁及其先前操作 happens-before 线程B加锁及其后续操作。
这样来看为了保证解锁及其之前操作的可见性需要把解锁线程的本地内存刷新到主内存去。同时为了保证加锁线程读到最新的值需要将本地内存的共享变量设为无效重新从主内存中读取。
synchronized 锁的实现原理
参考url:
synchronized 锁的实现原理
redis中有哪些锁
分布式锁的特征
互斥性任意时刻只有一个客户端能持有锁锁超时释放 持有锁超时可以释放锁防止死锁可重入锁 一个线程获取了锁后可以再次对其请求加锁高可用高性能加锁和解锁都需要开销尽可能低同时也要保证高可用安全性锁只能被持有的客户端删除不能被其他持不到锁的客户删除
参考 redis分布式锁的实现方案
redis中的锁又是怎么样去实现的
锁有哪些类型
对事务如何理解
一、 Java集合
1.2 流程图关系 1.3 底层实现
ArrayList底层是数组 默认长度为0调用add以后看情况不指定长度默认长度为10
ArrayList的扩容机制_arraylist扩容-CSDN博客
LinkedList底层是链表
Vector底层是数组
HashSet底层是哈希表
TreeSet红黑树
HashMap数组链表
Hashtable数组链表
LinkedHashMap数组链表红黑树
1.4 集合与数组的区别
1.4.1 元素类型
集合引用类型存储基本类型是自动装箱
数组基本类型、引用类型
1.4.2 元素个数
集合不固定、可任意扩展
数组固定不能改变容量
1.5 集合的好处
不受容器大小限制可以随时添加、删除元素提供了大量操作元素的方法判断、获取等
List集合
List集合的特点
可重复性可以添加相同的元素、有序存取顺序相同
List的主要方法有
add、get、remove、set、iterator、contains、addAll、removeAll、indexOf、toArray、clear、isEmpty
1.6 List集合我们以ArrayList集合为例
ArrayList集合
java.util.ArrayList是大小可变的数组的实现存储在内的数据称为元素。此类提供一些方法来操作内部存储的元素。ArrayList中可以不断添加元素其大小也自动增长。
java.util.ArrayList集合数据存储的结构是数组结构。元素增删慢查找快由于日常开发中使用最多的功能为查询数据、遍历数据所以ArrayList集合是最常用的集合。
泛型即泛指任意类型幼教参数化类型对具体类型的使用起到辅助作用类似于方法的参数
1.7 迭代器的常用方法
next()返回迭代的下一个元素
hasNext()如果仍有元素可以迭代则返回true
注意列表迭代器是List体系独有的遍历方式可以在对集合遍历的同时进行添加、删除等操作
但是必须通过调用列表迭代器的方法来实现
1.8 ArrayList、LinkedList和Vector的区别 线程同步Vector线程安全ArrayList线程不安全因为Vector的实现有synchronized锁 效率问题Vector效率低ArrayList效率高 增长数量Vector以2倍增长ArrayList以1.5倍增长
1.8.1 说出ArrayList,Vector, LinkedList的存储性能和特性
(1) ArrayList和Vector使用数组存储元素LinkedList使用链表存储元素
(2) ArrayList和Vector插入删除数据时需要搬运数据效率较差LinkedList使用链表不需要搬运数据效率高。
(3) ArrayList和Vectory查询时按数组下标查询不需要遍历效率高LinkedList需要遍历查询效率底。
1.8.2 多线程场景下如何使用 ArrayList
ArrayList 不是线程安全的如果遇到多线程场景可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样
ListString synchronizedList Collections.synchronizedList(list);
synchronizedList.add(aaa);
synchronizedList.add(bbb);
for (int i 0; i synchronizedList.size(); i) {System.out.println(synchronizedList.get(i));
}1.8.3 为什么 ArrayList 的 elementData 加上 transient 修饰
ArrayList 中的数组定义如下
private transient Object[] elementData;public class ArrayListE extends AbstractListEimplements ListE, RandomAccess, Cloneable, java.io.Serializable可以看到 ArrayList 实现了 Serializable 接口这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化重写了 writeObject 实现
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{*// Write out element count, and any hidden stuff*int expectedModCount modCount;s.defaultWriteObject();*// Write out array length*s.writeInt(elementData.length);*// Write out all elements in the proper order.*for (int i0; isize; i)s.writeObject(elementData[i]);if (modCount ! expectedModCount) {throw new ConcurrentModificationException();
}每次序列化时先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素然后遍历 elementData只序列化已存入的元素这样既加快了序列化的速度又减小了序列化之后的文件大小。
1.9 Set集合的特点
不可重复元素具有唯一性、无序元素的存取顺序是不固定的
Set的主要方法有
add、remove、iterator、contains、addAll、removeAll、toArray、clear、isEmpty
HashSet内部的数据结构是哈希表是线程不安全的。
HashSet中保证集合中元素是唯一的方法通过对象的hashCode和equals方法来完成对象唯一性的判断。
如果对象的hashCode值不同则不用判断equals方法就直接存到HashSet中。
如果对象的hashCode值相同需要用equals方法进行比较如果结果为true则视为相同元素不存如果结果为false视为不同元素进行存储。
注意如果元素要存储到HashCode中必须覆盖hashCode方法和equals方法。
TreeSet可以对Set集合中的元素进行排序是线程不安全的。
**TreeSet:**中判断元素唯一性的方法是根据比较方法的返回结果是否为0如果是0则是相同元素不存如果不是0则是不同元素存储。
1.9.1 说一下 HashSet 的实现原理
HashSet 是基于 HashMap 实现的HashSet的值存放于HashMap的key上HashMap的value统一为PRESENT因此 HashSet 的实现比较简单相关 HashSet 的操作基本上都是直接调用底层 HashMap 的相关方法来完成HashSet 不允许重复的值。
1.9.2 HashSet如何检查重复HashSet是如何保证数据不可重复的
向HashSet 中add ()元素时判断元素是否存在的依据不仅要比较hash值同时还要结合equles 方法比较。 HashSet 中的add ()方法会使用HashMap 的put()方法。
HashMap 的 key 是唯一的由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key并且在HashMap中如果K/V相同时会用新的V覆盖掉旧的V然后返回旧的V。所以不会重复 HashMap 比较key是否相等是先比较hashcode 再比较equals 。
以下是HashSet 部分源码
private static final Object PRESENT new Object();
private transient HashMapE,Object map;
public HashSet() {map new HashMap();
}
public boolean add(E e) {// 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值return map.put(e, PRESENT)null;
}1.10 TreeSet对元素进行排序的方式
元素自身具备比较功能即自然排序需要实现Comparable接口并覆盖其compareTo方法。
元素自身不具备比较功能则需要实现Comparator接口并覆盖其compare方法。
注意LinkedHashSet是一种有序的Set集合即其元素的存入和输出的顺序是相同的。
1.11 ListSetMap集合的特点 区别
List、Set、Map 是否继承自 Collection 接口List、Map、Set 三个接口存取元素时各有什么特点
Java 容器分为 Collection 和 Map 两大类Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、ListMap接口不是collection的子接口。
Collection集合主要有List和Set两大接口
List一个有序元素存入集合的顺序和取出的顺序一致容器元素可以重复可以插入多个null元素元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。 Set一个无序存入和取出顺序有可能不一致容器不可以存储重复元素只允许存入一个null元素必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。 Map是一个键值对集合存储键、值和之间的映射。 Key无序唯一value 不要求有序允许重复。Map没有继承于Collection接口从Map集合中检索元素时只要给出键KEY对象就会返回对应的值对象。
Map 的常用实现类HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap
关系图如下
1.12 HashMap和Hashtable的区别 线程同步Hashtable线程安全HashMap线程不安全 效率问题Hashtable效率低HashMap效率高 HashMap可以使用null作为keyHashtable不可以使用null为key HashMap使用的是新实现继承AbstractMap而Hashtable是继承Dictionary类实现比较老 Hash算法不同HashMap的hash算法比Hashtable的hash算法效率高 HashMap把Hashtable的contains方法去掉了改成containsValue和containsKey。因为contains方法容易让人引起误解。 取值不同HashMap用的是Iterator接口而Hashtable中还有使用Enumeration接口
1.3 HashSet 与 HashMap的区别 1.4 说一下HashMap的实现原理(非常重要)
HashMap概述 HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作并允许使用null值和null键。此类不保证映射的顺序特别是它不保证该顺序恒久不变。
HashMap的数据结构 在Java编程语言中最基本的结构就是两种一个是数组另外一个是模拟指针引用所有的数据结构都可以用这两个基本结构来构造的HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构即数组和链表的结合体。
①HashMap的工作原理
HashMap基于hashing原理我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时它调用键对象的hashCode()方法来计算hashcode让后找到bucket位置来储存值对象。当获取对象时通过键对象的equals()方法找到正确的键值对然后返回值对象。HashMap使用链表来解决碰撞问题当发生碰撞了对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。
HashMap存储结构
这里需要区分一下JDK1.7和 JDK1.8之后的 HashMap 存储结构。在JDK1.7及之前是用数组加链表的方式存储的。
但是众所周知当链表的长度特别长的时候查询效率将直线下降查询的时间复杂度为 O(n)。因此JDK1.8 把它设计为达到一个特定的阈值之后就将链表转化为红黑树。
这里简单说下红黑树的特点
每个节点只有两种颜色红色或者黑色 根节点必须是黑色 每个叶子节点NIL都是黑色的空节点 从根节点到叶子节点不能出现两个连续的红色节点 从任一节点出发到它下边的子节点的路径包含的黑色节点数目都相同 由于红黑树是一个自平衡的二叉搜索树因此可以使查询的时间复杂度降为O(logn)。红黑树不是本文重点不了解的童鞋可自行查阅相关资料哈
常用的变量
在 HashMap源码中比较重要的常用变量主要有以下这些。还有两个内部类来表示普通链表的节点和红黑树节点 //默认的初始化容量为16必须是2的n次幂
static final int DEFAULT_INITIAL_CAPACITY 1 4; // aka 16//最大容量为 2^30
static final int MAXIMUM_CAPACITY 1 30;//默认的加载因子0.75乘以数组容量得到的值用来表示元素个数达到多少时需要扩容。
//为什么设置 0.75 这个值呢简单来说就是时间和空间的权衡。
//若小于0.75如0.5则数组长度达到一半大小就需要扩容空间使用率大大降低
//若大于0.75如0.8则会增大hash冲突的概率影响查询效率。
static final float DEFAULT_LOAD_FACTOR 0.75f;//刚才提到了当链表长度过长时会有一个阈值超过这个阈值8就会转化为红黑树
static final int TREEIFY_THRESHOLD 8;//当红黑树上的元素个数减少到6个时就退化为链表
static final int UNTREEIFY_THRESHOLD 6;//链表转化为红黑树除了有阈值的限制还有另外一个限制需要数组容量至少达到64才会树化。
//这是为了避免数组扩容和树化阈值之间的冲突。
static final int MIN_TREEIFY_CAPACITY 64;//存放所有Node节点的数组
transient NodeK,V[] table;//存放所有的键值对
transient SetMap.EntryK,V entrySet;//map中的实际键值对个数即数组中元素个数
transient int size;//每次结构改变时都会自增fail-fast机制这是一种错误检测机制。
//当迭代集合的时候如果结构发生改变则会发生 fail-fast抛出异常。
transient int modCount;//数组扩容阈值
int threshold;//加载因子
final float loadFactor; //普通单向链表节点类
static class NodeK,V implements Map.EntryK,V {//key的hash值put和get的时候都需要用到它来确定元素在数组中的位置final int hash;final K key;V value;//指向单链表的下一个节点NodeK,V next;Node(int hash, K key, V value, NodeK,V next) {this.hash hash;this.key key;this.value value;this.next next;}
}//转化为红黑树的节点类
static final class TreeNodeK,V extends LinkedHashMap.EntryK,V {//当前节点的父节点TreeNodeK,V parent; //左孩子节点TreeNodeK,V left;//右孩子节点TreeNodeK,V right;//指向前一个节点TreeNodeK,V prev; // needed to unlink next upon deletion//当前节点是红色或者黑色的标识boolean red;TreeNode(int hash, K key, V val, NodeK,V next) {super(hash, key, val, next);}
} HashMap 构造函数
HashMap有四个构造函数可供我们使用一起来看下
//默认无参构造指定一个默认的加载因子
public HashMap() {this.loadFactor DEFAULT_LOAD_FACTOR;
}//可指定容量的有参构造但是需要注意当前我们指定的容量并不一定就是实际的容量下面会说
public HashMap(int initialCapacity) {//同样使用默认加载因子this(initialCapacity, DEFAULT_LOAD_FACTOR);
}//可指定容量和加载因子但是笔者不建议自己手动指定非0.75的加载因子
public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity 0)throw new IllegalArgumentException(Illegal initial capacity: initialCapacity);if (initialCapacity MAXIMUM_CAPACITY)initialCapacity MAXIMUM_CAPACITY;if (loadFactor 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException(Illegal load factor: loadFactor);this.loadFactor loadFactor;//这里就是把我们指定的容量改为一个大于它的的最小的2次幂值如传过来的容量是14则返回16//注意这里按理说返回的值应该赋值给 capacity即保证数组容量总是2的n次幂为什么这里赋值给了 threshold 呢//先卖个关子等到 resize 的时候再说this.threshold tableSizeFor(initialCapacity);
}//可传入一个已有的map
public HashMap(Map? extends K, ? extends V m) {this.loadFactor DEFAULT_LOAD_FACTOR;putMapEntries(m, false);
}//把传入的map里边的元素都加载到当前map
final void putMapEntries(Map? extends K, ? extends V m, boolean evict) {int s m.size();if (s 0) {if (table null) { // pre-sizefloat ft ((float)s / loadFactor) 1.0F;int t ((ft (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY);if (t threshold)threshold tableSizeFor(t);}else if (s threshold)resize();for (Map.Entry? extends K, ? extends V e : m.entrySet()) {K key e.getKey();V value e.getValue();//put方法的具体实现后边讲putVal(hash(key), key, value, false, evict);}}
}tableSizeFor()
上边的第三个构造函数中调用了 tableSizeFor 方法这个方法是怎么实现的呢
static final int tableSizeFor(int cap) {int n cap - 1;n | n 1;n | n 2;n | n 4;n | n 8;n | n 16;return (n 0) ? 1 : (n MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n 1;
}我们以传入参数为14 来举例计算这个过程。
首先14传进去之后先减1n此时为13。然后是一系列的无符号右移运算。
//13的二进制
0000 0000 0000 0000 0000 0000 0000 1101
//无右移1位高位补0
0000 0000 0000 0000 0000 0000 0000 0110
//然后把它和原来的13做或运算得到此时的n值
0000 0000 0000 0000 0000 0000 0000 1111
//再以上边的值右移2位
0000 0000 0000 0000 0000 0000 0000 0011
//然后和第一次或运算之后的 n 值再做或运算此时得到的n值
0000 0000 0000 0000 0000 0000 0000 1111
...
//我们会发现再执行右移 4,8,16位同样n的值不变
//当n小于0时返回1否则判断是否大于最大容量是的话返回最大容量否则返回 n1
return (n 0) ? 1 : (n MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n 1;
//很明显我们这里返回的是 n1 的值
0000 0000 0000 0000 0000 0000 0000 11111
0000 0000 0000 0000 0000 0000 0001 0000将它转为十进制就是 2^4 16 。我们会发现一个规律以上的右移运算最终会把最低位的值都转化为 1111 这样的结构然后再加1就是1 0000 这样的结构它一定是 2的n次幂。因此这个方法返回的就是大于当前传入值的最小最接近当前值的一个2的n次幂的值。
put()方法详解
//put方法会先调用一个hash()方法得到当前key的一个hash值
//用于确定当前key应该存放在数组的哪个下标位置
//这里的 hash方法我们姑且先认为是key.hashCode()其实不是的一会儿细讲
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}//把hash值和当前的keyvalue传入进来
//这里onlyIfAbsent如果为true表明不能修改已经存在的值因此我们传入false
//evict只有在方法 afterNodeInsertion(boolean evict) { }用到可以看到它是一个空实现因此不用关注这个参数
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {NodeK,V[] tab; NodeK,V p; int n, i;//判断table是否为空如果空的话会先调用resize扩容if ((tab table) null || (n tab.length) 0)n (tab resize()).length;//根据当前key的hash值找到它在数组中的下标判断当前下标位置是否已经存在元素//若没有则把key、value包装成Node节点直接添加到此位置。// i (n - 1) hash 是计算下标位置的为什么这样算后边讲if ((p tab[i (n - 1) hash]) null)tab[i] newNode(hash, key, value, null);else { //如果当前位置已经有元素了分为三种情况。NodeK,V e; K k;//1.当前位置元素的hash值等于传过来的hash并且他们的key值也相等//则把p赋值给e跳转到①处后续需要做值的覆盖处理if (p.hash hash ((k p.key) key || (key ! null key.equals(k))))e p;//2.如果当前是红黑树结构则把它加入到红黑树 else if (p instanceof TreeNode)e ((TreeNodeK,V)p).putTreeVal(this, tab, hash, key, value);else {//3.说明此位置已存在元素并且是普通链表结构则采用尾插法把新节点加入到链表尾部for (int binCount 0; ; binCount) {if ((e p.next) null) {//如果头结点的下一个节点为空则插入新节点p.next newNode(hash, key, value, null);//如果在插入的过程中链表长度超过了8则转化为红黑树if (binCount TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);//插入成功之后跳出循环跳转到①处break;}//若在链表中找到了相同key的话直接退出循环跳转到①处if (e.hash hash ((k e.key) key || (key ! null key.equals(k))))break;p e;}}//①//说明发生了碰撞e代表的是旧值因此节点位置不变但是需要替换为新值if (e ! null) { // existing mapping for keyV oldValue e.value;//用新值替换旧值并返回旧值。if (!onlyIfAbsent || oldValue null)e.value value;//看方法名字即可知这是在node被访问之后需要做的操作。其实此处是一个空实现//只有在 LinkedHashMap才会实现用于实现根据访问先后顺序对元素进行排序hashmap不提供排序功能// Callbacks to allow LinkedHashMap post-actions//void afterNodeAccess(NodeK,V p) { }afterNodeAccess(e);return oldValue;}}//fail-fast机制modCount;//如果当前数组中的元素个数超过阈值则扩容if (size threshold)resize();//同样的空实现afterNodeInsertion(evict);return null;
}
hash()计算原理
前面 put 方法中说到需要先把当前key进行哈希处理我们看下这个方法是怎么实现的。
static final int hash(Object key) {int h;return (key null) ? 0 : (h key.hashCode()) ^ (h 16);
}运行一段程序把它的 hashCode的二进制打印出来如下。
public static void main(String[] args) {Object o new Object();int hash o.hashCode();System.out.println(hash);System.out.println(Integer.toBinaryString(hash));}
//1836019240
//1101101011011110110111000101000然后进行 (h key.hashCode()) ^ (h 16) 这一段运算。
//h原来的值
0110 1101 0110 1111 0110 1110 0010 1000
//无符号右移16位其实相当于把低位16位舍去只保留高16位
0000 0000 0000 0000 0110 1101 0110 1111
//然后高16位和原 h进行异或运算
0110 1101 0110 1111 0110 1110 0010 1000
^
0000 0000 0000 0000 0110 1101 0110 11110110 1101 0110 1111 0000 0011 0100 0111
resize() 扩容机制
在上边 put 方法中我们会发现当数组为空的时候会调用 resize 方法当数组的 size 大于阈值的时候也会调用 resize方法。 那么看下 resize 方法都做了哪些事情吧。
final NodeK,V[] resize() {//旧数组NodeK,V[] oldTab table;//旧数组的容量int oldCap (oldTab null) ? 0 : oldTab.length;//旧数组的扩容阈值注意看这里取的是当前对象的 threshold 值下边的第2种情况会用到。int oldThr threshold;//初始化新数组的容量和阈值分三种情况讨论。int newCap, newThr 0;//1.当旧数组的容量大于0时说明在这之前肯定调用过 resize扩容过一次才会导致旧容量不为0。//为什么这样说呢之前我在 tableSizeFor 卖了个关子需要注意的是它返回的值是赋给了 threshold 而不是 capacity。//我们在这之前压根就没有在任何地方看到过它给 capacity 赋初始值。if (oldCap 0) {//容量达到了最大值if (oldCap MAXIMUM_CAPACITY) {threshold Integer.MAX_VALUE;return oldTab;}//新数组的容量和阈值都扩大原来的2倍else if ((newCap oldCap 1) MAXIMUM_CAPACITY oldCap DEFAULT_INITIAL_CAPACITY)newThr oldThr 1; // double threshold}//2.到这里说明 oldCap 0并且 oldThr(threshold) 0这就是 map 初始化的时候第一次调用 resize的情况//而 oldThr的值等于 threshold此时的 threshold 是通过 tableSizeFor 方法得到的一个2的n次幂的值(我们以16为例)。//因此需要把 oldThr 的值也就是 threshold 赋值给新数组的容量 newCap以保证数组的容量是2的n次幂。//所以我们可以得出结论当map第一次 put 元素的时候就会走到这个分支把数组的容量设置为正确的值2的n次幂)//但是此时 threshold 的值也是2的n次幂这不对啊它应该是数组的容量乘以加载因子才对。别着急这个会在③处理。else if (oldThr 0) // initial capacity was placed in thresholdnewCap oldThr;//3.到这里说明 oldCap 和 oldThr 都是小于等于0的。也说明我们的map是通过默认无参构造来创建的//于是数组的容量和阈值都取默认值就可以了即 16 和 12。else { // zero initial threshold signifies using defaultsnewCap DEFAULT_INITIAL_CAPACITY;newThr (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}//③ 这里就是处理第2种情况因为只有这种情况 newThr 才为0//因此计算 newThr用 newCap即16 乘以加载因子 0.75得到 12 并把它赋值给 thresholdif (newThr 0) {float ft (float)newCap * loadFactor;newThr (newCap MAXIMUM_CAPACITY ft (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}//赋予 threshold 正确的值表示数组下次需要扩容的阈值此时就把原来的 16 修正为了 12。threshold newThr;SuppressWarnings({rawtypes,unchecked})//我们可以发现在构造函数时并没有创建数组在第一次调用put方法导致resize的时候才会把数组创建出来。这是为了延迟加载提高效率。NodeK,V[] newTab (NodeK,V[])new Node[newCap];table newTab;//如果原来的数组不为空那么我们就需要把原来数组中的元素重新分配到新的数组中//如果是第2种情况由于是第一次调用resize此时数组肯定是空的因此也就不需要重新分配元素。if (oldTab ! null) {//遍历旧数组for (int j 0; j oldCap; j) {NodeK,V e;//取到当前下标的第一个元素如果存在则分三种情况重新分配位置if ((e oldTab[j]) ! null) {oldTab[j] null;//1.如果当前元素的下一个元素为空则说明此处只有一个元素//则直接用它的hash()值和新数组的容量取模就可以了得到新的下标位置。if (e.next null)newTab[e.hash (newCap - 1)] e;//2.如果是红黑树结构则拆分红黑树必要时有可能退化为链表else if (e instanceof TreeNode)((TreeNodeK,V)e).split(this, newTab, j, oldCap);//3.到这里说明这是一个长度大于 1 的普通链表则需要计算并//判断当前位置的链表是否需要移动到新的位置else { // preserve order// loHead 和 loTail 分别代表链表旧位置的头尾节点NodeK,V loHead null, loTail null;// hiHead 和 hiTail 分别代表链表移动到新位置的头尾节点NodeK,V hiHead null, hiTail null;NodeK,V next;do {next e.next;//如果当前元素的hash值和oldCap做与运算为0则原位置不变if ((e.hash oldCap) 0) {if (loTail null)loHead e;elseloTail.next e;loTail e;}//否则需要移动到新的位置else {if (hiTail null)hiHead e;elsehiTail.next e;hiTail e;}} while ((e next) ! null);//原位置不变的一条链表数组下标不变if (loTail ! null) {loTail.next null;newTab[j] loHead;}//移动到新位置的一条链表数组下标为原下标加上旧数组的容量if (hiTail ! null) {hiTail.next null;newTab[j oldCap] hiHead;}}}}}return newTab;
}上边还有一个非常重要的运算我们没有讲解。就是下边这个判断它用于把原来的普通链表拆分为两条链表位置不变或者放在新的位置。
if ((e.hash oldCap) 0) {} else {}get()方法
有了前面的基础get方法就比较简单了。
public V get(Object key) {NodeK,V e;//如果节点为空则返回null否则返回节点的value。这也说明hashMap是支持value为null的。//因此我们就明白了为什么hashMap支持Key和value都为nullreturn (e getNode(hash(key), key)) null ? null : e.value;
}final NodeK,V getNode(int hash, Object key) {NodeK,V[] tab; NodeK,V first, e; int n; K k;//首先要确保数组不能为空然后取到当前hash值计算出来的下标位置的第一个元素if ((tab table) ! null (n tab.length) 0 (first tab[(n - 1) hash]) ! null) {//若hash值和key都相等则说明我们要找的就是第一个元素直接返回if (first.hash hash // always check first node((k first.key) key || (key ! null key.equals(k))))return first;//如果不是的话就遍历当前链表或红黑树if ((e first.next) ! null) {//如果是红黑树结构则找到当前key所在的节点位置if (first instanceof TreeNode)return ((TreeNodeK,V)first).getTreeNode(hash, key);//如果是普通链表则向后遍历查找直到找到或者遍历到链表末尾为止。do {if (e.hash hash ((k e.key) key || (key ! null key.equals(k))))return e;} while ((e e.next) ! null);}}//否则说明没有找到返回nullreturn null;
}为什么HashMap链表会形成死循环
准确的讲应该是 JDK1.7 的 HashMap 链表会有死循环的可能因为JDK1.7是采用的头插法在多线程环境下有可能会使链表形成环状从而导致死循环。JDK1.8做了改进用的是尾插法不会产生死循环。
那么链表是怎么形成环状的呢
关于这一点的解释我发现网上文章抄来抄去的而且都来自左耳朵耗子更惊奇的是连配图都是一模一样的。别问我为什么知道因为我也看过耗子叔的文章哈哈。然而菜鸡的我那篇文章并没有看懂。。。
我实在看不下去了于是一怒之下就有了这篇文章。我会照着源码一步一步的分析变量之间的关系怎么变化的并有配图哦。
我们从 put()方法开始最终找到线程不安全的那个方法。这里省略中间不重要的过程我只把方法的跳转流程贴出来
//添加元素方法 - 添加新节点方法 - 扩容方法 - 把原数组元素重新分配到新数组中
put() -- addEntry() -- resize() -- transfer()问题就发生在 transfer 这个方法中。 我们假设原数组容量只有2其中一条链表上有两个元素 A,B如下图 现在有两个线程都执行 transfer 方法。每个线程都会在它们自己的工作内存生成一个newTable 的数组用于存储变化后的链表它们互不影响这里互不影响指的是两个新数组本身互不影响。但是需要注意的是它们操作的数据却是同一份。
因为真正的数组中的内容在堆中存储它们指向的是同一份数据内容。就相当于有两个不同的引用 XY但是它们都指向同一个对象 Z。这里 X、Y就是两个线程不同的新数组Z就是堆中的AB 等元素对象。
假设线程一执行到了上图1中所指的代码①处恰好 CPU 时间片到了线程被挂起不能继续执行了。 记住此时线程一中记录的 e A e.next B。
然后线程二正常执行扩容后的数组长度为 4 假设 AB两个元素又碰撞到了同一个桶中。然后通过几次 while 循环后采用头插法最终呈现的结构如下
此时线程一解挂继续往下执行。注意此时线程一记录的还是 e Ae.next B因为它还未感知到最新的变化。
我们主要关注图1中标注的①②③④处的变量变化
/**
* next e.next
* e.next newTable[i]
* newTable[i] e;
* e next;
*///第一次循环,(伪代码)
eA;nextB;
e.nextnull //此时线程一的新数组刚初始化完成还没有元素
newTab[i] A-null //把A节点头插到新数组中
eB; //下次循环的e值
第一次循环结束后线程一新数组的结构如下图 然后由于 eB不为空进入第二次循环。
//第二次循环
eB;nextA; //此时AB的内容已经被线程二修改为 B-A-null然后被线程一读到所以B的下一个节点指向A
e.nextA-null // A-null 为第一次循环后线程一新数组的结构
newTab[i] B-A-null //新节点B插入之后线程一新数组的结构
eA; //下次循环的 e 值
第二次循环结束后线程一新数组的结构如下图 此时由于 eA不为空继续循环。 这时有的同学可能就会问了就算他们成环了又怎样跟死循环有什么关系
我们看下 get() 方法最终调用 getEntry 方法 可以看到查找元素时只要 e 不为空就会一直循环查找下去。若有某个元素 C 的 hash 值也落在了和 AB元素同一个桶中则会由于 AB互相指向e.next 永远不为空就会形成死循环。
1.4.1 HashMap 基于 Hash 算法实现的
当我们往Hashmap中put元素时利用key的hashCode重新hash计算出当前对象的元素在数组中的下标 存储时如果出现hash值相同的key此时有两种情况。(1)如果key相同则覆盖原始值(2)如果key不同出现冲突则将当前的key-value放入链表中 获取时直接找到hash值对应的下标在进一步判断key是否相同从而找到对应值。 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题核心就是使用了数组的存储方式然后将冲突的key的对象放入链表中一旦发现冲突就在链表中做进一步的对比。
1.5 HashMap的扩容操作是怎么实现的
HashMap长度是默认的16length - 1的结果 : 十进制 : 15
具体实现机制参考 hashMap扩容机制
①.在jdk1.8中resize方法是在hashmap中的键值对大于阀值时或者初始化时就调用resize方法进行扩容
②.每次扩展的时候都是扩展2倍
③.扩展后Node对象的位置要么在原位置要么移动到原偏移量两倍的位置。
在putVal()中我们看到在这个函数里面使用到了2次resize()方法resize()方法表示的在进行第一次初始化时会对其进行扩容或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发这也是JDK1.8版本的一个优化的地方在1.7中扩容之后需要重新去计算其Hash值根据Hash值对其进行分发但在1.8版本中则是根据在同一个桶的位置中进行判断(e.hash oldCap)是否为0重新进行hash分配后该元素的位置要么停留在原始位置要么移动到原始位置增加的数组大小这个位置上
1.6 HashMap是怎么解决哈希冲突的
答在解决这个问题之前我们首先需要知道什么是哈希冲突而在了解哈希冲突之前我们还要知道什么是哈希才行
1.7 什么是哈希
Hash一般翻译为“散列”也有直接音译为“哈希”的这就是把任意长度的输入通过散列算法变换成固定长度的输出该输出就是散列值哈希值这种转换是一种压缩映射也就是散列值的空间通常远小于输入的空间不同的输入可能会散列成相同的输出所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
1.8 所有散列函数都有如下一个基本特性
根据同一散列函数计算出的散列值如果不同那么输入值肯定也不同。但是根据同一散列函数计算出的散列值如果相同输入值不一定相同。
1.9 什么是哈希冲突
当两个不同的输入值根据同一散列函数计算出相同的散列值的现象我们就把它叫做碰撞哈希碰撞。
二、 Java基础-IO流反射堆与栈面向对象三大特性String、StringBuffer、StringBuilder
自动装箱与拆箱
装箱将基本类型用它们对应的引用类型包装起来
拆箱将包装类型转换为基本数据类型
基本数据类型与包装类的区别int 和 Integer 有什么区别
1、Integer是int的包装类int则是java的一种基本数据类型 2、Integer变量必须实例化后才能使用而int变量不需要 3、Integer实际是对象的引用当new一个Integer时实际上是生成一个指针指向此对象而int则是直接存储数据值 4、Integer的默认值是nullint的默认值是0
应用场景的区别
比如要体现出 考试成绩为0和缺考的区别的时候 用Integer可以 int不行
比如用容器的时候 ArrayList等职能放对象不能放基本数据类型。 将基本数据类型封装成对象的好处是
1、在对象中可以定义更多的功能方法操作该数据。例如基本数据类型和字符串直接的转换。 2、编码过程中只接收对象的情况例如List中只存入对象不能存入基本数据类型。
3、使用场景 大部分的情况下这两种类型没有太大得区别。根据以上两点的分析基本类型的存取速度会更快对象中有更多功能方法来操作数据要根据实际需要定义属性。
借鉴网上学生成绩的例子没来考试成绩是0还是null如果你觉得是0就用int如果你认为是null就用Integer。
堆和栈的区别
那数据存放在堆中和栈中有什么区别呢
堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的或者说是可以动态变化的但是在栈中一个对象只对应了一个4btye的引用堆栈分离的好处。
为什么不把基本类型放堆中呢因为其占用的空间一般是1~8个字节——需要空间比较少而且因为是基本类型所以不会出现动态增长的情况——长度固定因此栈中存储就够了如果把他存在堆中是没有什么意义的还会浪费空间后面说明。可以这么说基本类型和对象的引用都是存放在栈中而且都是几个字节的一个数因此在程序运行时他们的处理方式是统一的。但是基本类型、对象引用和对象本身就有所区别了因为一个是栈中的数据一个是堆中的数据。最常见的一个问题就是Java中参数传递时的问题。
栈
1栈的存取速度比堆快仅次于直接位于CPU的寄存器。
2栈中的数据的大小和生存周期是确定的。
3栈中的数据可以共享。
堆
1堆可以动态的分配内存大小生存期也不必告诉编译器。
2堆在运行时动态分配内存存取速度慢。
综上所述可以简单的理解为为了高效可以把一些数值小简单的变量存放在栈中。堆和栈
堆Heap与栈Stack是开发人员必须面对的两个概念在理解这两个概念时需要放到具体的场景下因为不同场景下堆与栈代表不同的含义。一般情况下有两层含义 1程序内存布局场景下堆与栈表示两种内存管理方式 2数据结构场景下堆与栈表示两种常用的数据结构。
在说堆和栈之前我们先说一下JVM虚拟机内存的划分
Java程序在运行时都要开辟空间任何软件在运行时都要在内存中开辟空间Java虚拟机运行时也是要开辟空间的。JVM运行时在内存中开辟一片内存区域启动时在自己的内存区域中进行更细致的划分因为虚拟机中每一片内存处理的方式都不同所以要单独进行管理。
JVM内存的划分有五片 1. 寄存器2. 本地方法区3. 方法区4. 栈内存5. 堆内存。重点来说一下堆和栈
栈内存:栈内存首先是一片内存区域存储的都是局部变量凡是定义在方法中的都是局部变量方法外的是全局变量for循环内部定义的也是局部变量是先加载函数才能进行局部变量的定义所以方法先进栈然后再定义变量变量有自己的作用域一旦离开作用域变量就会被释放。栈内存的更新速度很快因为局部变量的生命周期都很短。
堆内存:存储的是数组和对象其实数组就是对象凡是new建立的都是在堆中堆中存放的都是实体对象实体用于封装数据而且是封装多个实体的多个属性如果一个数据消失这个实体也没有消失还可以用所以堆是不会随时释放的但是栈不一样栈里存放的都是单个变量变量被释放了那就没有了。堆里的实体虽然不会被释放但是会被当成垃圾Java有垃圾回收机制不定时的收取。
下面我们通过一个图例详细讲一下堆和栈
比如主函数里的语句 int [] arrnew int [3];在内存中是怎么被定义的
主函数先进栈在栈中定义一个变量arr,接下来为arr赋值但是右边不是一个具体值是一个实体。实体创建在堆里在堆里首先通过new关键字开辟一个空间内存在存储数据的时候都是通过地址来体现的地址是一块连续的二进制然后给这个实体分配一个内存地址。数组都是有一个索引数组这个实体在堆内存中产生之后每一个空间都会进行默认的初始化这是堆内存的特点未初始化的数据是不能用的但在堆里是可以用的因为初始化过了但是在栈里没有不同的类型初始化的值不一样。所以堆和栈里就创建了变量和实体 那么堆和栈是怎么联系起来的呢?
我们刚刚说过给堆分配了一个地址把堆的地址赋给arrarr就通过地址指向了数组。所以arr想操纵数组时就通过地址而不是直接把实体都赋给它。这种我们不再叫他基本数据类型而叫引用数据类型。称为arr引用了堆内存当中的实体。可以理解为c或c的指针Java成长自c和c很像优化了c 如果当int [] arrnull;
arr不做任何指向null的作用就是取消引用数据类型的指向。
当一个实体没有引用数据类型指向的时候它在堆内存中不会被释放而被当做一个垃圾在不定时的时间内自动回收因为Java有一个自动回收机制而c没有需要程序员手动回收如果不回收就越堆越多直到撑满内存溢出所以Java在内存管理上优于c。自动回收机制程序自动监测堆里是否有垃圾如果有就会自动的做垃圾回收的动作但是什么时候收不一定。
堆与栈的区别 很明显
1.栈内存存储的是局部变量而堆内存存储的是实体
2.栈内存的更新速度要快于堆内存因为局部变量的生命周期很短
3.栈内存存放的变量生命周期一旦结束就会被释放而堆内存存放的实体会被垃圾回收机制不定时的回收。
延伸关于Integer和int的比较
1、由于Integer变量实际上是对一个Integer对象的引用所以两个通过new生成的Integer变量永远是不相等的因为new生成的是两个对象其内存地址不同。
Integer i new Integer(100);
Integer j new Integer(100);
System.out.print(i j); //false2、Integer变量和int变量比较时只要两个变量的值是向等的则结果为true因为包装类Integer和基本数据类型int比较时java会自动拆包装为int然后进行比较实际上就变为两个int变量的比较
Integer i new Integer(100);
int j 100
System.out.print(i j); //true3、非new生成的Integer变量和new Integer()生成的变量比较时结果为false。因为非new生成的Integer变量指向的是java常量池中的对象而new Integer()生成的变量指向堆中新建的对象两者在内存中的地址不同
Integer i new Integer(100);
Integer j 100;
System.out.print(i j); //false4、对于两个非new生成的Integer对象进行比较时如果两个变量的值在区间-128到127之间则比较结果为true如果两个变量的值不在此区间则比较结果为false
Integer i 100;
Integer j 100;
System.out.print(i j); //true
Integer i 128;
Integer j 128;
System.out.print(i j); //false对于第4条的原因 java在编译Integer i 100 ;时会翻译成为Integer i Integer.valueOf(100)而java API中对Integer类型的valueOf的定义如下
public static Integer valueOf(int i){
assert IntegerCache.high 127;
if (i IntegerCache.low i IntegerCache.high){
return IntegerCache.cache[i (-IntegerCache.low)];}
return new Integer(i);
}java对于-128到127之间的数会进行缓存Integer i 127时会将127进行缓存下次再写Integer j 127时就会直接从缓存中取就不会new了
Java 是一个近乎纯洁的面向对象编程语言但是为了编程的方便还是引入了基本数据类型但是为了能够将这些基本数据类型当成对象操作Java 为每一个基本数据类型都引入了对应的包装类型wrapper classint 的包装类就是 Integer从 Java 5 开始引入了自动装箱/拆箱机制使得二者可以相互转换。
Java 为每个原始类型提供了包装类型
原始类型: booleancharbyteshortintlongfloatdouble
包装类型BooleanCharacterByteShortIntegerLongFloatDouble
Integer a 127 与 Integer b 127相等吗 对于对象引用类型比较的是对象的内存地址。 对于基本数据类型比较的是值。
如果整型字面量的值在-128到127之间那么自动装箱时不会new新的Integer对象而是直接引用常量池中的Integer对象超过范围 a1b1的结果是false
Double和double的区别
1、Double是java定义的类而double是预定义数据类型8种中的一种 2、Double就好比是对double类型的封装内置很多方法可以实现String到double的转换以及获取各种double类型的属性值MAX_VALUE、SIZE等等
基于上述两点如果你在普通的定义一个浮点类型的数据两者都可以但是Double是类所以其对象是可以为NULL的而double定义的不能为NULL如果你要将一些数字字符串那么就应该使用Double类型了其内部帮你实现了强转。
面向对象和面向过程的区别
面向过程
优点性能比面向对象高因为类调用时需要实例化开销比较大比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发性能是最重要的因素。
缺点没有面向对象易维护、易复用、易扩展
面向对象
优点易维护、易复用、易扩展由于面向对象有封装、继承、多态性的特性可以设计出低耦合的系统使系统更加灵活、更加易于维护
缺点性能比面向过程低
2.1 面向对象三大特性
2.1.1 封装 继承 多态
抽象抽象是将一类对象的共同特征总结出来构造类的过程包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为并不关注这些行为的细节是什么。
封装
封装把一个对象的属性私有化同时提供一些可以被外界访问的属性的方法如果属性不想被外界访问我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法那么这个类也没有什么意义了。
继承
继承是使用已存在的类的定义作为基础建立新类的技术新类的定义可以增加新的数据或新的功能也可以用父类的功能但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。
多态
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定而是在程序运行期间才确定即一个引用变量到底会指向哪个类的实例对象该引用变量发出的方法调用到底是哪个类中实现的方法必须在由程序运行期间才能决定。
在Java中有两种形式可以实现多态继承多个子类对同一方法的重写和接口实现接口并覆盖接口中同一方法。
2.1.2 其中Java 面向对象编程三大特性封装 继承 多态
封装隐藏对象的属性和实现细节仅对外提供公共访问方式将变化隔离便于使用提高复用性和安全性。
继承继承是使用已存在的类的定义作为基础建立新类的技术新类的定义可以增加新的数据或新的功能也可以用父类的功能但不能选择性地继承父类。通过使用继承可以提高代码复用性。继承是多态的前提。
2.1.3 关于继承如下 3 点请记住
子类拥有父类非 private 的属性和方法。
子类可以拥有自己属性和方法即子类可以对父类进行扩展。
子类可以用自己的方式实现父类的方法。
多态性父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。
在Java中有两种形式可以实现多态继承多个子类对同一方法的重写和接口实现接口并覆盖接口中同一方法。
方法重载overload实现的是编译时的多态性也称为前绑定而方法重写override实现的是运行时的多态性也称为后绑定。
一个引用变量到底会指向哪个类的实例对象该引用变量发出的方法调用到底是哪个类中实现的方法必须在由程序运行期间才能决定。运行时的多态是面向对象最精髓的东西要实现多态需要做两件事
方法重写子类继承父类并重写父类中已有的或抽象的方法 对象造型用父类型引用子类型对象这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为。
2.1.4 什么是多态机制Java语言是如何实现多态的
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定而是在程序运行期间才确定即一个引用变量倒底会指向哪个类的实例对象该引用变量发出的方法调用到底是哪个类中实现的方法必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类这样不用修改源程序代码就可以让引用变量绑定到各种不同的类实现上从而导致该引用调用的具体方法随之改变即不修改程序代码就可以改变程序运行时所绑定的具体代码让程序可以选择多个运行状态这就是多态性。
多态分为编译时多态和运行时多态。其中编辑时多态是静态的主要是指方法的重载它是根据参数列表的不同来区分不同的函数通过编辑之后会变成两个不同的函数在运行时谈不上多态。而运行时多态是动态的它是通过动态绑定来实现的也就是我们所说的多态性。
2.1.5 Java实现多态有三个必要条件继承、重写、向上转型。
继承在多态中必须存在有继承关系的子类和父类。
重写子类对父类中某些方法进行重新定义在调用这些方法时就会调用子类的方法。
向上转型在多态中需要将子类的引用赋给父类对象只有这样该引用才能够具备技能调用父类的方法和子类的方法。
只有满足了上述三个条件我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象从而达到执行不同的行为。
对于Java而言它多态的实现机制遵循一个原则当超类对象引用变量引用子类对象时被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法但是这个被调用的方法必须是在超类中定义过的也就是说被子类覆盖的方法。
2.2 类与接口
2.2.1 抽象类和接口的对比
抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。 2.2.3 普通类和抽象类有哪些区别
普通类不能包含抽象方法抽象类可以包含抽象方法。 抽象类不能直接实例化普通类可以直接实例化。
2.2.4 抽象类能使用 final 修饰吗
不能定义抽象类就是让其他类继承的如果定义为 final 该类就不能被继承这样彼此就会产生矛盾所以 final 不能修饰抽象类
2.2.5 重写与重载
构造器constructor是否可被重写override 构造器不能被继承因此不能被重写但可以被重载。
2.2.6 重载Overload和重写Override的区别。
重载的方法能否根据返回类型进行区分
方法的重载和重写都是实现多态的方式区别在于前者实现的是编译时的多态性而后者实现的是运行时的多态性。
重载发生在同一个类中方法名相同参数列表不同参数类型不同、个数不同、顺序不同与方法返回值和访问修饰符无关即重载的方法不能根据返回类型进行区分
重写发生在父子类中方法名、参数列表必须相同返回值小于等于父类抛出的异常小于等于父类访问修饰符大于等于父类里氏代换原则如果父类方法访问修饰符为private则子类中就不是重写。
2.2.7 对象相等判断
(1) 和 equals 的区别是什么 : 它的作用是判断两个对象的地址是不是相等。即判断两个对象是不是同一个对象。(基本数据类型 比较的是值引用数据类型 比较的是内存地址)
equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况
情况1类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时等价于通过“”比较这两个对象。
情况2类覆盖了 equals() 方法。一般我们都覆盖 equals() 方法来两个对象的内容相等若它们的内容相等则返回 true (即认为这两个对象相等)。
说明
String中的equals方法是被重写过的因为object的equals方法是比较的对象的内存地址而String的equals方法比较的是对象的值。 当创建String类型的对象时虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。
2.2.8 hashCode 与 equals (重要)
(1) HashSet如何检查重复
两个对象的 hashCode() 相同则 equals() 也一定为 true对吗
(2) hashCode和equals方法的关系
面试官可能会问你“你重写过 hashcode 和 equals 么为什么重写equals时必须重写hashCode方法”
hashCode()介绍
(3) hashCode() 的作用
获取哈希码也称为散列码它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中这就意味着Java中的任何类都包含有hashCode()函数。
散列表存储的是键值对(key-value)它的特点是能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码可以快速找到所需要的对象
2.3 反射-反射-反射-反射
2.3.1 什么是反射机制
JAVA反射机制是在运行状态中对于任意一个类都能够知道这个类的所有属性和方法对于任意一个对象都能够调用它的任意一个方法和属性这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
静态编译和动态编译
**静态编译**在编译时确定类型绑定对象 **动态编译**运行时确定类型绑定对象
2.3.2 反射机制优缺点
优点 运行期类型的判断动态加载类提高代码灵活度。 缺点 性能瓶颈反射相当于一系列解释操作通知 JVM 要做的事情性能比直接的java代码要慢很多。
2.3.3 请简介你对Java反射的理解以及使用场景
Java反射指的是在运行状态中对任意一个类都能够知道这个类的所有属性和方法对于任意一个的对象都能调用它的任意一个方法和属性这种动态获取的信息以及动态调用对象的方法的功能称为Java反射
Java反射使用的场景在Java编码时知道类和对象的具体信息此时直接对类和对象进行操作即可在Java编码时不知道类和对象的具体信息时此时使用Java反射来获取
3. io流 - IO、NIO、BIO、AIO
java 中 IO 流分为几种?
按照流的流向分可以分为输入流和输出流
按照操作单元划分可以划分为字节流和字符流
按照流的角色划分为节点流和处理流。
Java Io流共涉及40多个类这些类看上去很杂乱但实际上很有规则而且彼此之间存在非常紧密的联系 Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。
InputStream/Reader: 所有的输入流的基类前者是字节输入流后者是字符输入流。 OutputStream/Writer: 所有输出流的基类前者是字节输出流后者是字符输出流。 IO流主要的分类方式有以下3种 按数据流的方向输入流、输出流 按处理数据单位字节流、字符流 按功能节点流、处理流
输入流与输出流
输入与输出是相对于应用程序而言的比如文件读写读取文件是输入流写文件是输出流这点很容易搞反。
字节流与字符流
字节流和字符流的用法几乎完成全一样区别在于字节流和字符流所操作的数据单元不同字节流操作的单元是数据单元是8位的字节字符流操作的是数据单元为16位的字符。
字节流和字符流的其他区别
字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件。字符流一般用于处理纯文本类型的文件如TXT文件等但不能处理图像视频等非文本文件。用一句话说就是字节流可以处理一切文件而字符流只能处理纯文本文件。
字节流本身没有缓冲区缓冲字节流相对于字节流效率提升非常高。而字符流本身就带有缓冲区缓冲字符流相对于字符流效率提升就不是那么大了。详见文末效率对比。
节点流和处理流
节点流直接操作数据读写的流类比如FileInputStream
处理流对一个已存在的流的链接和封装通过对数据进行处理为程序提供功能强大、灵活的读写功能例如BufferedInputStream缓冲字节流
按操作方式分类结构图 按操作对象分类结构图 IO和NIO的区别
链接 IO与NIO区别_io和nio的区别_Leida_hzm的博客-CSDN博客 NIO与IO区别
IO是面向流的NIO是面向缓冲区的Java IO面向流意味着每次从流中读一个或多个字节直至读取所有字节它们没有被缓存在任何地方NIO则能前后移动流中的数据因为是面向缓冲区的IO流是阻塞的NIO流是不阻塞的Java IO的各种流是阻塞的。这意味着当一个线程调用read() 或 write()时该线程被阻塞直到有一些数据被读取或数据完全写入。该线程在此期间不能再干任何事情了Java NIO的非阻塞模式使一个线程从某通道发送请求读取数据但是它仅能得到目前可用的数据如果目前没有数据可用时就什么都不会获取。NIO可让您只使用一个或几个单线程管理多个通道网络连接或文件但付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。 非阻塞写也是如此。一个线程请求写入一些数据到某通道但不需要等待它完全写入这个线程同时可以去做别的事情。选择器 Java NIO的选择器允许一个单独的线程来监视多个输入通道你可以注册多个通道使用一个选择器然后使用一个单独的线程来“选择”通道这些通道里已经有可以处理的输入或者选择已准备写入的通道。这种选择机制使得一个单独的线程很容易来管理多个通道。
NIO的优势 1.优势在于一个线程管理多个通道但是数据的处理将会变得复杂 2.如果需要管理同时打开的成千上万个连接这些连接每次只是发送少量的数据采用这种 传统IO的优势 1.适用于一个线程管理一个通道的情况因为其中的流数据的读取是阻塞的 2.如果需要管理同时打开不太多的连接这些连接会发送大量的数据
BIO,NIO,AIO 有什么区别?
BIOBlock IO 同步阻塞式 IO就是我们平常使用的传统 IO它的特点是模式简单使用方便并发处理能力低。 NIONon IO 同步非阻塞 IO是传统 IO 的升级客户端和服务器端通过 Channel通道通讯实现了多路复用。 AIOAsynchronous IO 是 NIO 的升级也叫 NIO2实现了异步非堵塞 IO 异步 IO 的操作基于事件和回调机制。 详细分析
BIO (Blocking I/O): 同步阻塞I/O模式数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高小于单机1000的情况下这种模型是比较不错的可以让每一个连接专注于自己的 I/O 并且编程模型简单也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗可以缓冲一些系统处理不了的连接或请求。但是当面对十万甚至百万级连接的时候传统的 BIO 模型是无能为力的。因此我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
NIO (New I/O): NIO是一种同步非阻塞的I/O模型在Java 1.4 中引入了NIO框架对应 java.nio 包提供了 Channel , SelectorBuffer等抽象。NIO中的N可以理解为Non-blocking不单纯是New。它支持面向缓冲的基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样比较简单但是性能和可靠性都不好非阻塞模式正好与之相反。对于低负载、低并发的应用程序可以使用同步阻塞I/O来提升开发速率和更好的维护性对于高负载、高并发的网络应用应使用 NIO 的非阻塞模式来开发
AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的也就是应用操作之后会直接返回不会堵塞在那里当后台处理完成操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写虽然 NIO 在网络操作中提供了非阻塞的方法但是 NIO 的 IO 行为还是同步的。对于 NIO 来说我们的业务线程是在 IO 操作准备好时得到通知接着就由这个线程自行进行 IO 操作IO操作本身是同步的。查阅网上相关资料我发现就目前来说 AIO 的应用还不是很广泛Netty 之前也尝试使用过 AIO不过又放弃了。
Files的常用方法都有哪些
Files. exists()检测文件路径是否存在。 Files. createFile()创建文件。 从 Files. createDirectory()创建文件夹。 Files. delete()删除一个文件或目录。 Files. copy()复制文件。 Files. move()移动文件。 Files. size()查看文件个数。 Files. read()读取文件。 Files. write()写入文件。
4. String、StringBuffer、StringBuilder
字符型常量和字符串常量的区别
形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的若干个字符 含义上: 字符常量相当于一个整形值(ASCII值),可以参加表达式运算 字符串常量代表一个地址值(该字符串在内存中存放位置) 占内存大小 字符常量只占一个字节 字符串常量占若干个字节(至少一个字符结束标志)
什么是字符串常量池
字符串常量池位于堆内存中专门用来存储字符串常量可以提高内存的使用率避免开辟多块空间存储相同的字符串在创建字符串时 JVM 会首先检查字符串常量池如果该字符串已经存在池中则返回它的引用如果不存在则实例化一个字符串放到池中并返回其引用。
String 是最基本的数据类型吗
不是。Java 中的基本数据类型只有 8 个 byte、short、int、long、float、double、char、boolean除了基本类型primitive type剩下的都是引用类型referencetypeJava 5 以后引入的枚举类型也算是一种比较特殊的引用类型。
这是很基础的东西但是很多初学者却容易忽视Java 的 8 种基本数据类型中不包括 String基本数据类型中用来描述文本数据的是 char但是它只能表示单个字符比如 ‘a’,‘好’ 之类的如果要描述一段文本就需要用多个 char 类型的变量也就是一个 char 类型数组比如“你好” 就是长度为2的数组 char[] chars {‘你’,‘好’};
但是使用数组过于麻烦所以就有了 StringString 底层就是一个 char 类型的数组只是使用的时候开发者不需要直接操作底层数组用更加简便的方式即可完成对字符串的使用。
String有哪些特性
不变性String 是只读字符串是一个典型的 immutable 对象对它进行任何操作其实都是创建一个新的对象再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时可以保证数据的一致性。
常量池优化String 对象创建之后会在字符串常量池中进行缓存如果下次创建同样的对象时会直接返回缓存的引用。
final使用 final 来定义 String 类表示 String 类不能被继承提高了系统的安全性。
String为什么是不可变的吗
简单来说就是String类利用了final修饰的char类型数组存储字符源码如下所示
/** The value is used for character storage. */
private final char value[];String类被final修饰不可被继承 如何将字符串反转
使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。
数组有没有 length()方法String 有没有 length()方法
数组没有 length()方法 有 length 的属性。String 有 length()方法。JavaScript中获得字符串的长度是通过 length 属性得到的这一点容易和 Java 混淆。
String 类的常用方法都有那些
indexOf()返回指定字符的索引。 charAt()返回指定索引处的字符。 replace()字符串替换。 trim()去除字符串两端空白。 split()分割字符串返回一个分割后的字符串数组。 getBytes()返回字符串的 byte 类型数组。 length()返回字符串长度。 toLowerCase()将字符串转成小写字母。 toUpperCase()将字符串转成大写字符。 substring()截取字符串。 equals()字符串比较。
在使用 HashMap 的时候用 String 做 key 有什么好处
HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置因为字符串是不可变的所以当创建字符串时它的 hashcode 被缓存下来不需要再次计算所以相比于其他对象更快。
String和StringBuffer、StringBuilder的区别
String为什么是不可变的?
可变性
String类中使用字符数组保存字符串private final char value[]所以string对象是不可变的。StringBuilder与StringBuffer都继承自AbstractStringBuilder类在AbstractStringBuilder中也是使用字符数组保存字符串char[] value这两种对象都是可变的。
线程安全性
String中的对象是不可变的也就可以理解为常量线程安全。AbstractStringBuilder是StringBuilder与StringBuffer的公共父类定义了一些字符串的基本操作如expandCapacity、append、insert、indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁所以是线程安全的。StringBuilder并没有对方法进行加同步锁所以是非线程安全的。
性能
每次对String 类型进行改变的时候都会生成一个新的String对象然后将指针指向新的String 对象。StringBuffer每次都会对StringBuffer对象本身进行操作而不是生成新的对象并改变对象引用。相同情况下使用StirngBuilder 相比使用StringBuffer 仅能获得10%~15% 左右的性能提升但却要冒多线程不安全的风险。
对于三者使用的总结
如果要操作少量的数据用 String
单线程操作字符串缓冲区 下操作大量数据 StringBuilder
多线程操作字符串缓冲区 下操作大量数据 StringBuffer
5. maven常用打包命令
1、mvn compile 编译,将Java 源程序编译成 class 字节码文件。
2、mvn test 测试并生成测试报告
3、mvn clean 将以前编译得到的旧的 class 字节码文件删除
4、mvn pakage 打包,动态 web工程打 war包Java工程打 jar 包。
5、mvn install 将项目生成 jar 包放在仓库中以便别的模块调用
6、mvn clean install -Dmaven.test.skiptrue 抛弃测试用例打包
总结
Java程序设计语言对对象采用的不是引用调用实际上对象引用是按值传递的。
下面再总结一下Java中方法参数的使用情况
一个方法不能修改一个基本数据类型的参数即数值型或布尔型》 一个方法可以改变一个对象参数的状态。 一个方法不能让对象参数引用一个新的对象。
值传递和引用传递有什么区别
**值传递**指的是在方法调用时传递的参数是按值的拷贝传递传递的是值的拷贝也就是说传递后就互不相关了。
**引用传递**指的是在方法调用时传递的参数是按引用进行传递其实传递的引用的地址也就是变量所对应的内存空间的地址。传递的是值的引用也就是说传递前和传递后都指向同一个引用也就是同一个内存空间。
Java包 JDK 中常用的包有哪些
**java.lang**这个是系统的基础类 **java.io**这里面是所有输入输出有关的类比如文件操作等 **java.nio**为了完善 io 包中的功能提高 io 包中性能而写的一个新包 **java.net**这里面是与网络有关的类 **java.util**这个是系统辅助类特别是集合类 **java.sql**这个是数据库操作的类。
import java和javax有什么区别
刚开始的时候 JavaAPI 所必需的包是 java 开头的包javax 当时只是扩展 API 包来说使用。然而随着时间的推移javax 逐渐的扩展成为 Java API 的组成部分。但是将扩展从 javax 包移动到 java 包将是太麻烦了最终会破坏一堆现有的代码。因此最终决定 javax 包将成为标准API的一部分。
所以实际上java和javax没有区别。这都是一个名字。
三、 多线程
-1 高并发
参考文章 【多线程高并发编程】二 实现多线程的几种方式
〇、使用多线程的场景
1. 为什么使用多线程
通俗的解释一下多线程先
多线程用于堆积处理就像一个大土堆一个推土机很慢那么10个推土机一起来处理当然速度就快了不过由于位置的限制如果20个推土机那么推土机之间会产生相互的避让相互摩擦相互拥挤反而不如10个处理的好所以多线程处理线程数要开的恰当就可以提高效率。
多线程使用的目的
1、吞吐量做WEB容器帮你做了多线程但是它只能帮你做请求层面的简单的说就是一个请求一个线程(如struts2是多线程的每个客户端请求创建一个实例保证线程安全)或多个请求一个线程如果是单线程那只能是处理一个用户的请求。
2、伸缩性通过增加CPU核数来提升性能。
多线程的使用场景
1、常见的浏览器、Web服务(现在写的web是中间件帮你完成了线程的控制)web处理请求各种专用服务器(如游戏服务器)
2、servlet多线程
3、FTP下载多线程操作文件
4、数据库用到的多线程
5、分布式计算
6、tomcattomcat内部采用多线程上百个客户端访问同一个WEB应用tomcat接入后就是把后续的处理扔给一个新的线程来处理这个新的线程最后调用我们的servlet程序比如doGet或者dpPost方法
7、后台任务如定时向大量(100W以上)的用户发送邮件定期更新配置文件、任务调度(如quartz)一些监控用于定期信息采集
8、自动作业处理比如定期备份日志、定期备份数据库
9、异步处理如发微博、记录日志
10、页面异步处理比如大批量数据的核对工作(有10万个手机号码核对哪些是已有用户)
11、数据库的数据分析(待分析的数据太多)数据迁移
12、多步骤的任务处理可根据步骤特征选用不同个数和特征的线程来协作处理多任务的分割由一个主线程分割给多个线程完成
1. 线程概述
1.1 线程和进程
进程是处于运行过程中的程序并且具有一定的独立功能进程是系统进行资源分配和调度的一个独立单位。
线程也被称为轻量级进程线程是进程的组成部分一个进程可以拥有多个线程一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量但不拥有系统资源它与父进程的其它线程共享该进程所拥有的全部资源。一个线程可以创建和撤销另一个线程同一个进程中的多个线程之间可以并发执行。
1.2 并发和并行
并行指在同一时刻有多条指令在多个处理器上同时执行并发指在同一时刻只能有一条指令执行但多个进程指令被快速轮换执行使得在宏观上具有多个进程同时执行的效果。
1.3 多线程的优势
1进程之间不能共享内存但线程之间共享内存却非常容易。
2系统创建进程时需要为该进程重新分配系统资源但创建线程代价小得多因此使用多线程来实现多任务并发比多进程的效率高。
3java语言内置了多线程功能支持而不是单纯地作为底层操作系统的调度方式从而简化了java的多线程编程。
1.4 程序运行原理
分时调度
所有线程轮流使用 CPU 的使用权平均分配每个线程占用 CPU 的时间。
抢占式调度
优先让优先级高的线程使用 CPU如果线程的优先级相同那么会随机选择一个(线程随机性)Java使用的为抢占式调度。
1.5 主线程
jvm启动后必然有一个执行路径(线程)从main方法开始的一直执行到main方法结束这个线程在java中称之为主线程。当程序的主线程执行时如果遇到了循环而导致程序在指定位置停留时间过长则无法马上执行下面的程序需要等待循环结束后能够执行。
1.6 线程的 6 种状态
就像生物从出生到长大、最终死亡的过程一样线程也有自己的生命周期在 Java 中线程的生命周期中一共有 6 种状态。 New新创建 Runnable可运行 Blocked被阻塞 Waiting等待 Timed Waiting计时等待 Terminated被终止
如果想要确定线程当前的状态可以通过 getState() 方法并且线程在任何时刻只可能处于 1 种状态。
运行状态可能会有阻塞 2. 线程的创建和启动
2.1 Thread类
Java使用Thread类代表线程所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务实际上就是执行一段程序流。Java使用县城执行体来表示这段流。
2.2创建线程有哪几种方法
2.2.1 继承Thread类重写Run方法其中Thread类本身也是实现了Runnable接口
1定义Thread类的子类并重写该类的run方法该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。 2创建Thread子类的实例即创建了线程对象。 3调用线程对象的start()方法来启动该线程。
示例代码
package com.thread;
public class FirstThreadTest extends Thread{int i 0;//重写run方法run方法的方法体就是现场执行体public void run(){for(;i100;i){System.out.println(getName() i);}}public static void main(String[] args){for(int i 0;i 100;i){System.out.println(Thread.currentThread().getName() : i);if(i20){new FirstThreadTest().start();new FirstThreadTest().start();}}}
}2.2.2 实现Runnable接口重写run方法
1定义runnable接口的实现类并重写该接口的run()方法该run()方法的方法体同样是该线程的线程执行体。
2创建 Runnable实现类的实例并依此实例作为Thread的target来创建Thread对象该Thread对象才是真正的线程对象。
3调用线程对象的start()方法来启动该线程。
示例代码
public class RunnableThreadTest implements Runnable{private int i;public void run() {for(i 0;i 100;i){System.out.println(Thread.currentThread().getName() i);}}public static void main(String[] args){for(int i 0;i 100;i) {System.out.println(Thread.currentThread().getName() i);if(i20) {RunnableThreadTest rtt new RunnableThreadTest();new Thread(rtt,新线程1).start();new Thread(rtt,新线程2).start();}}}
}2.2.3 实现 Callable 接口重写 call方法有返回值
通过Callable和Future创建线程
1创建Callable接口的实现类并实现**call()方法该call()**方法将作为线程执行体并且有返回值。
2创建Callable实现类的实例使用FutureTask类来包装Callable对象该FutureTask对象封装了该Callable对象的**call()**方法的返回值。
3使用FutureTask对象作为Thread对象的target创建并启动新线程。
4调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
实例代码
package com.thread;import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class CallableThreadTest implements CallableInteger {public static void main(String[] args) {CallableThreadTest ctt new CallableThreadTest();FutureTaskInteger ft new FutureTask(ctt);for (int i 0; i 100; i) {System.out.println(Thread.currentThread().getName() 的循环变量i的值 i);if (i 20) {new Thread(ft, 有返回值的线程).start();}}try {System.out.println(子线程的返回值 ft.get());} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}Overridepublic Integer call() throws Exception {int i 0;for (; i 100; i) {System.out.println(Thread.currentThread().getName() i);}return i;}}2.2.4 通过线程池创建线程
创建线程的三种方式的对比
采用实现Runnable、Callable接口的方式创见多线程时 优势 线程类只是实现了Runnable接口或Callable接口还可以继承其他类。 在这种方式下多个线程可以共享同一个target对象所以非常适合多个相同线程来处理同一份资源的情况从而可以将CPU、代码和数据分开形成清晰的模型较好地体现了面向对象的思想。 劣势 编程稍微复杂如果要访问当前线程则必须使用Thread.currentThread()方法。使用继承Thread类的方式创建多线程时 优势 编写简单如果需要访问当前线程则无需使用Thread.currentThread()方法直接使用this即可获得当前线程。 劣势 线程类已经继承了Thread类所以不能再继承其他父类。
4 线程池的核心参数有哪些 为什么使用线程池
使用线程池最大的原因就是可以根据系统的需求和硬件环境灵活的控制线程的数量,且可以对所有线程进行统一的管理和控制,从而提高系统的运行效率,降低系统运行运行压力;当然了,使用线程池的原因不仅仅只有这些,我们可以从线程池自身的优点上来进一步了解线程池的好处;
使用线程池的优势有哪些
线程和任务分离,提升线程重用性;控制线程并发数量,降低服务器压力,统一管理所有线程;提升系统响应速度,假如创建线程用的时间为T1执行任务用的时间为T2,销毁线程用的时间为T3那么使用线程池就免去了T1和T3的时间
构造方法:
public ThreadPoolExecutor(int corePoolSize, //核心线程数量int maximumPoolSize,// 最大线程数long keepAliveTime, // 最大空闲时间TimeUnit unit, // 时间单位BlockingQueueRunnable workQueue, // 任务队列ThreadFactory threadFactory, // 线程工厂RejectedExecutionHandler handler // 饱和处理机制)
{ ... }
4个参数的设计:
1:核心线程数(corePoolSize) 核心线程数的设计需要依据任务的处理时间和每秒产生的任务数量来确定,例如:执行一个任务需要0.1秒,系统百分之80的时间每秒都会产生100个任务,那么要想在1秒内处理完这100个任务,就需要10个线程,此时我们就可以设计核心线程数为10;当然实际情况不可能这么平均,所以我们一般按照8020原则设计即可,既按照百分之80的情况设计核心线程数,剩下的百分之20可以利用最大线程数处理; 2:任务队列长度(workQueue) 任务队列长度一般设计为:核心线程数/单个任务执行时间*2即可;例如上面的场景中,核心线程数设计为10,单个任务执行时间为0.1秒,则队列长度可以设计为200; 3:最大线程数(maximumPoolSize) 最大线程数的设计除了需要参照核心线程数的条件外,还需要参照系统每秒产生的最大任务数决定:例如:上述环境中,如果系统每秒最大产生的任务是1000个,那么,最大线程数(最大任务数-任务队列长度)*单个任务执行时间;既: 最大线程数(1000-200)*0.180个; 4:最大空闲时间(keepAliveTime) 这个参数的设计完全参考系统运行环境和硬件压力设定,没有固定的参考值,用户可以根据经验和系统产生任务的时间间隔合理设置一个值即可;
四、Spring
什么是spring? Spring是一个轻量级Java开发框架最早有Rod Johnson创建目的是为了解决企业级应用开发的业务逻辑层和其他各层的耦合问题。它是一个分层的JavaSE/JavaEE full-stack一站式轻量级开源框架为开发Java应用程序提供全面的基础架构支持。Spring负责基础架构因此Java开发者可以专注于应用程序的开发。
Spring最根本的使命是解决企业级应用开发的复杂性即简化Java开发。
Spring可以做很多事情它为企业级开发提供给了丰富的功能但是这些功能的底层都依赖于它的两个核心特性也就是依赖注入dependency injectionDI和面向切面编程aspect-oriented programmingAOP。
为了降低Java开发的复杂性Spring采取了以下4种关键策略
基于POJO的轻量级和最小侵入性编程 通过依赖注入和面向接口实现松耦合 基于切面和惯例进行声明式编程 通过切面和模板减少样板式代码。 Spring框架的设计目标设计理念和核心是什么 Spring设计目标Spring为开发者提供一个一站式轻量级应用开发平台
Spring设计理念在JavaEE开发中支持POJO和JavaBean开发方式使应用面向接口开发充分支持OO面向对象设计方法Spring通过IoC容器实现对象耦合关系的管理并实现依赖反转将对象之间的依赖关系交给IoC容器实现解耦
Spring框架的核心IoC容器和AOP模块。通过IoC容器管理POJO对象以及他们之间的耦合关系通过AOP以动态非侵入的方式增强服务。
IoC让相互协作的组件保持松散的耦合而AOP编程允许你把遍布于应用各层的功能分离出来形成可重用的功能组件。
spring常用的注解
什么是基于Java的Spring注解配置? 给一些注解的例子
基于Java的配置允许你在少量的Java注解的帮助下进行你的大部分Spring配置而非通过XML文件。
以Configuration 注解为例它用来标记类可以当做一个bean的定义被Spring IOC容器使用。
另一个例子是Bean注解它表示此方法将要返回一个对象作为一个bean注册进Spring应用上下文。
Configuration
public class StudentConfig {Beanpublic StudentBean myStudent() {return new StudentBean();}
}怎样开启注解装配
注解装配在默认情况下是不开启的为了使用注解装配我们必须在Spring配置文件中配置 context:annotation-config/元素。
Component, Controller, Repository, Service 有何区别 Component这将 java 类标记为 bean。它是任何 Spring 管理组件的通用构造型。spring 的组件扫描机制现在可以将其拾取并将其拉入应用程序环境中。
Controller这将一个类标记为 Spring Web MVC 控制器。标有它的 Bean 会自动导入到 IoC 容器中。
Service此注解是组件注解的特化。它不会对 Component 注解提供任何其他行为。您可以在服务层类中使用 Service 而不是 Component因为它以更好的方式指定了意图。
Repository这个注解是具有类似用途和功能的 Component 注解的特化。它为 DAO 提供了额外的好处。它将 DAO 导入 IoC 容器并使未经检查的异常有资格转换为 Spring DataAccessException。
Required 注解有什么作用 这个注解表明bean的属性必须在配置的时候设置通过一个bean定义的显式的属性值或通过自动装配若Required注解的bean属性未被设置容器将抛出BeanInitializationException。示例
public class Employee {private String name;Requiredpublic void setName(String name){this.namename;}public string getName(){return name;}
}Autowired 注解有什么作用 Autowired默认是按照类型装配注入的默认情况下它要求依赖对象必须存在可以设置它required属性为false。Autowired 注解提供了更细粒度的控制包括在何处以及如何完成自动装配。它的用法和Required一样修饰setter方法、构造器、属性或者具有任意名称和/或多个参数的PN方法。
public class Employee {private String name;Autowiredpublic void setName(String name) {this.namename;}public string getName(){return name;}
}Autowired和Resource之间的区别 Autowired可用于构造函数、成员变量、Setter方法
Autowired和Resource之间的区别
Autowired默认是按照类型装配注入的默认情况下它要求依赖对象必须存在可以设置它required属性为false。
Resource默认是按照名称来装配注入的只有当找不到与名称匹配的bean才会按照类型来装配注入。
Qualifier 注解有什么作用 当您创建多个相同类型的 bean 并希望仅使用属性装配其中一个 bean 时您可以使用Qualifier 注解和 Autowired 通过指定应该装配哪个确切的 bean 来消除歧义。
RequestMapping 注解有什么用 RequestMapping 注解用于将特定 HTTP 请求方法映射到将处理相应请求的控制器中的特定类/方法。此注释可应用于两个级别
类级别映射请求的 URL 方法级别映射 URL 以及 HTTP 请求方法
Spring的优缺点是什么
优点
方便解耦简化开发
Spring就是一个大工厂可以将所有对象的创建和依赖关系的维护交给Spring管理。
AOP编程的支持
Spring提供面向切面编程可以方便的实现对程序进行权限拦截、运行监控等功能。
声明式事务的支持
只需要通过配置就可以完成对事务的管理而无需手动编程。
方便程序的测试
Spring对Junit4支持可以通过注解方便的测试Spring程序。
方便集成各种优秀框架
Spring不排斥各种优秀的开源框架其内部提供了对各种优秀框架的直接支持如Struts、Hibernate、MyBatis等。
降低JavaEE API的使用难度
Spring对JavaEE开发中非常难用的一些APIJDBC、JavaMail、远程调用等都提供了封装使这些API应用难度大大降低。
缺点
Spring明明一个很轻量级的框架却给人感觉大而全 Spring依赖反射反射影响性能 使用门槛升高入门Spring需要较长时间 Spring有哪些应用场景 应用场景JavaEE企业应用开发包括SSH、SSM等
Spring价值
Spring是非侵入式的框架目标是使应用程序代码对框架依赖最小化 Spring提供一个一致的编程模型使应用直接使用POJO开发与运行环境隔离开来 Spring推动应用设计风格向面向对象和面向接口开发转变提高了代码的重用性和可测试性 Spring由哪些模块组成 Spring 总共大约有 20 个模块 由 1300 多个不同的文件构成。 而这些组件被分别整合在核心容器Core Container 、 AOPAspect Oriented Programming和设备支持Instrmentation 、数据访问与集成Data Access/Integeration 、 Web、 消息Messaging 、 Test等 6 个模块中。 以下是 Spring 5 的模块结构图 spring core提供了框架的基本组成部分包括控制反转Inversion of ControlIOC和依赖注入Dependency InjectionDI功能。 spring beans提供了BeanFactory是工厂模式的一个经典实现Spring将管理对象称为Bean。 spring context构建于 core 封装包基础上的 context 封装包提供了一种框架式的对象访问方法。 spring jdbc提供了一个JDBC的抽象层消除了烦琐的JDBC编码和数据库厂商特有的错误代码解析 用于简化JDBC。 spring aop提供了面向切面的编程实现让你可以自定义拦截器、切点等。 spring Web提供了针对 Web 开发的集成特性例如文件上传利用 servlet listeners 进行 ioc 容器初始化和针对 Web 的 ApplicationContext。 spring test主要为测试提供支持的支持使用JUnit或TestNG对Spring组件进行单元测试和集成测试。
Spring 框架中都用到了哪些设计模式
工厂模式BeanFactory就是简单工厂模式的体现用来创建对象的实例 **单例模式**Bean默认为单例模式。 代理模式Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术 模板方法用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。 观察者模式定义对象键一种一对多的依赖关系当一个对象的状态发生改变时所有依赖于它的对象都会得到通知被制动更新如Spring中listener的实现–ApplicationListener。 详细讲解一下核心容器spring context应用上下文) 模块 这是基本的Spring模块提供spring 框架的基础功能BeanFactory 是 任何以spring为基础的应用的核心。Spring 框架建立在此模块之上它使Spring成为一个容器。
Bean 工厂是工厂模式的一个实现提供了控制反转功能用来把应用的配置和依赖从真正的应用代码中分离。最常用的就是org.springframework.beans.factory.xml.XmlBeanFactory 它根据XML文件中的定义加载beans。该容器从XML 文件读取配置元数据并用它去创建一个完全配置的系统或应用。
Spring框架中有哪些不同类型的事件 Spring 提供了以下5种标准的事件
上下文更新事件ContextRefreshedEvent在调用ConfigurableApplicationContext 接口中的refresh()方法时被触发。
上下文开始事件ContextStartedEvent当容器调用ConfigurableApplicationContext的Start()方法开始/重新开始容器时触发该事件。
上下文停止事件ContextStoppedEvent当容器调用ConfigurableApplicationContext的Stop()方法停止容器时触发该事件。
上下文关闭事件ContextClosedEvent当ApplicationContext被关闭时触发该事件。容器被关闭时其管理的所有单例Bean都被销毁。
请求处理事件RequestHandledEvent在Web应用中当一个http请求request结束触发该事件。如果一个bean实现了ApplicationListener接口当一个ApplicationEvent 被发布以后bean会自动被通知。
Spring 应用程序有哪些不同组件
Spring 应用一般有以下组件
接口 - 定义功能。 Bean 类 - 它包含属性setter 和 getter 方法函数等。 Bean 配置文件 - 包含类的信息以及如何配置它们。 Spring 面向切面编程AOP - 提供面向切面编程的功能。 用户程序 - 它使用接口。
使用 Spring 有以下方式
作为一个成熟的 Spring Web 应用程序。 作为第三方 Web 框架使用 Spring Frameworks 中间层。 作为企业级 Java Bean它可以包装现有的 POJOPlain Old Java Objects。 用于远程使用。
Spring面向切面编程(AOP)
什么是AOP
OOP(Object-Oriented Programming)面向对象编程允许开发者定义纵向的关系但并适用于定义横向的关系导致了大量代码的重复而不利于各个模块的重用。
AOP(Aspect-Oriented Programming)一般称为面向切面编程作为面向对象的一种补充用于将那些与业务无关但却对多个对象产生影响的公共行为和逻辑抽取并封装为一个可重用的模块这个模块被命名为“切面”Aspect减少系统中的重复代码降低了模块间的耦合度同时提高了系统的可维护性。可用于权限认证、日志、事务处理等。
Spring AOP and AspectJ AOP 的区别
AOP 有哪些实现方式 AOP实现的关键在于 代理模式AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ动态代理则以Spring AOP为代表。
1AspectJ是静态代理的增强所谓静态代理就是AOP框架会在编译阶段生成AOP代理类因此也称为编译时增强他会在编译阶段将AspectJ(切面)织入到Java字节码中运行的时候就是增强之后的AOP对象。
2Spring AOP使用的动态代理所谓的动态代理就是说AOP框架不会去修改字节码而是每次运行时在内存中临时为方法生成一个AOP对象这个AOP对象包含了目标对象的全部方法并且在特定的切点做了增强处理并回调原对象的方法。
Spring AOP中的动态代理
JDK动态代理和CGLIB动态代理的区别
Spring AOP中的动态代理主要有两种方式JDK动态代理和CGLIB动态代理
JDK动态代理只提供接口的代理不支持类的代理。核心InvocationHandler接口和Proxy类InvocationHandler 通过invoke()方法反射来调用目标类中的代码动态地将横切逻辑和业务编织在一起接着Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。
如果代理类没有实现 InvocationHandler 接口那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIBCode Generation Library是一个代码生成的类库可以在运行时动态的生成指定类的一个子类对象并覆盖其中特定方法并添加增强代码从而实现AOP。CGLIB是通过继承的方式做的动态代理因此如果某个类被标记为final那么它是无法使用CGLIB做动态代理的。
静态代理与动态代理区别在于生成AOP代理对象的时机不同相对来说AspectJ的静态代理方式具有更好的性能但是AspectJ需要特定的编译器进行处理而Spring AOP则无需特定的编译器处理。
InvocationHandler 的 invoke(Object proxy,Method method,Object[] args)proxy是最终生成的代理实例; method 是被代理目标实例的某个具体方法; args 是被代理目标实例某个方法的具体入参, 在方法反射调用时使用。
如何理解 Spring 中的代理
将 Advice 应用于目标对象后创建的对象称为代理。在客户端对象的情况下目标对象和代理对象是相同的。
Advice Target Object Proxy
解释一下Spring AOP里面的几个名词
1切面Aspect切面是通知和切点的结合。通知和切点共同定义了切面的全部内容。 在Spring AOP中切面可以使用通用类基于模式的风格 或者在普通类中以 AspectJ 注解来实现。
2连接点Join point指方法在Spring AOP中一个连接点 总是 代表一个方法的执行。 应用可能有数以千计的时机应用通知。这些时机被称为连接点。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中并添加新的行为。
3通知Advice在AOP术语中切面的工作被称为通知。
4切入点Pointcut切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
5引入Introduction引入允许我们向现有类添加新方法或属性。
6目标对象Target Object 被一个或者多个切面aspect所通知advise的对象。它通常是一个代理对象。也有人把它叫做 被通知adviced 对象。 既然Spring AOP是通过运行时代理实现的这个对象永远是一个 被代理proxied 对象。
7织入Weaving织入是把切面应用到目标对象并创建新的代理对象的过程。在目标对象的生命周期里有多少个点可以进行织入
编译期切面在目标类编译时被织入。AspectJ的织入编译器是以这种方式织入切面的。 类加载期切面在目标类加载到JVM时被织入。需要特殊的类加载器它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入就支持以这种方式织入切面。 运行期切面在应用运行的某个时刻被织入。一般情况下在织入切面时AOP容器会为目标对象动态地创建一个代理对象。SpringAOP就是以这种方式织入切面。
Spring在运行时通知对象
通过在代理类中包裹切面Spring在运行期把切面织入到Spring管理的bean中。代理封装了目标类并拦截被通知方法的调用再把调用转发给真正的目标bean。当代理拦截到方法调用时在调用目标bean方法之前会执行切面逻辑。
直到应用需要被代理的bean时Spring才创建代理对象。如果使用的是ApplicationContext的话在ApplicationContext从BeanFactory中加载所有bean的时候Spring才会创建被代理的对象。因为Spring运行时才创建代理对象所以我们不需要特殊的编译器来织入SpringAOP的切面。
Spring只支持方法级别的连接点 因为Spring基于动态代理所以Spring只支持方法连接点。Spring缺少对字段连接点的支持而且它不支持构造器连接点。方法之外的连接点拦截功能我们可以利用Aspect来补充。
在Spring AOP 中关注点和横切关注的区别是什么在 spring aop 中 concern 和 cross-cutting concern 的不同之处 关注点concern是应用中一个模块的行为一个关注点可能会被定义成一个我们想实现的一个功能。
横切关注点cross-cutting concern是一个关注点此关注点是整个应用都会使用的功能并影响整个应用比如日志安全和数据传输几乎应用的每个模块都需要的功能。因此这些都属于横切关注点。
Spring通知有哪些类型 在AOP术语中切面的工作被称为通知实际上是程序执行时要通过SpringAOP框架触发的代码段。
Spring切面可以应用5种类型的通知
前置通知Before在目标方法被调用之前调用通知功能 后置通知After在目标方法完成之后调用通知此时不会关心方法的输出是什么 返回通知After-returning 在目标方法成功执行之后调用通知 异常通知After-throwing在目标方法抛出异常后调用通知 环绕通知Around通知包裹了被通知的方法在被通知的方法调用之前和调用之后执行自定义的行为。同一个aspect不同advice的执行顺序
①没有异常情况下的执行顺序
around before advice before advice target method 执行 around after advice after advice afterReturning
②有异常情况下的执行顺序
around before advice before advice target method 执行 around after advice after advice afterThrowing:异常发生 java.lang.RuntimeException: 异常发生
什么是切面 Aspect
aspect 由 pointcount 和 advice 组成切面是通知和切点的结合。 它既包含了横切逻辑的定义, 也包括了连接点的定义. Spring AOP 就是负责实施切面的框架, 它将切面所定义的横切逻辑编织到切面所指定的连接点中. AOP 的工作重心在于如何将增强编织目标对象的连接点上, 这里包含两个工作:
如何通过 pointcut 和 advice 定位到特定的 joinpoint 上 如何在 advice 中编写切面代码. 可以简单地认为, 使用 Aspect 注解的类就是切面.
Spring控制反转(IOC)
什么是Spring IOC 容器
控制反转即IoC (Inversion of Control)它把传统上由程序代码直接操控的对象的调用权交给容器通过容器来实现对象组件的装配和管理。所谓的“控制反转”概念就是对组件对象控制权的转移从程序代码本身转移到了外部容器。
Spring IOC 负责创建对象管理对象通过依赖注入DI装配对象配置对象并且管理这些对象的整个生命周期。
控制反转(IoC)有什么作用
管理对象的创建和依赖关系的维护。对象的创建并不是一件简单的事在对象关系比较复杂时如果依赖关系需要程序猿来维护的话那是相当头疼的
解耦由容器去维护具体的对象
托管了类的产生过程比如我们需要在类的产生过程中做一些处理最直接的例子就是代理如果有容器程序可以把这部分处理交给容器应用程序则无需去关心类是如何完成代理的
IOC的优点是什么
IOC 或 依赖注入把应用的代码量降到最低。 它使应用容易测试单元测试不再需要单例和JNDI查找机制。 最小的代价和最小的侵入性使松散耦合得以实现。 IOC容器支持加载服务时的饿汉式初始化和懒加载。 Spring IoC 的实现机制 Spring 中的 IoC 的实现原理就是工厂模式加反射机制。
interface Fruit {public abstract void eat();}
class Apple implements Fruit {public void eat(){System.out.println(Apple);}
}
class Orange implements Fruit {public void eat(){System.out.println(Orange);}
}
class Factory {public static Fruit getInstance(String ClassName) {Fruit fnull;try {f(Fruit)Class.forName(ClassName).newInstance();} catch (Exception e) {e.printStackTrace();}return f;}
}
class Client {public static void main(String[] a) {Fruit fFactory.getInstance(io.github.dunwu.spring.Apple);if(f!null){f.eat();}}
}Spring 的 IoC支持哪些功能
Spring 的 IoC 设计支持以下功能
依赖注入 依赖检查 自动装配 支持集合 指定初始化方法和销毁方法 支持回调某些方法但是需要实现 Spring 接口略有侵入 其中最重要的就是依赖注入从 XML 的配置上说即 ref 标签。对应 Spring RuntimeBeanReference 对象。
对于 IoC 来说最重要的就是容器。容器管理着 Bean 的生命周期控制着 Bean 的依赖注入。
BeanFactory 和 ApplicationContext有什么区别
BeanFactory和ApplicationContext是Spring的两大核心接口都可以当做Spring的容器。其中ApplicationContext是BeanFactory的子接口。
依赖关系
BeanFactory是Spring里面最底层的接口包含了各种Bean的定义读取bean配置文档管理bean的加载、实例化控制bean的生命周期维护bean之间的依赖关系。
ApplicationContext接口作为BeanFactory的派生除了提供BeanFactory所具有的功能外还提供了更完整的框架功能
继承MessageSource因此支持国际化。
统一的资源文件访问方式。
提供在监听器中注册bean的事件。
同时加载多个配置文件。
载入多个有继承关系上下文 使得每一个上下文都专注于一个特定的层次比如应用的web层。
加载方式
BeanFactroy采用的是延迟加载形式来注入Bean的即只有在使用到某个Bean时(调用getBean())才对该Bean进行加载实例化。这样我们就不能发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入BeanFacotry加载后直至第一次使用调用getBean方法才会抛出异常。
ApplicationContext它是在容器启动时一次性创建了所有的Bean。这样在容器启动时我们就可以发现Spring中存在的配置错误这样有利于检查所依赖属性是否注入。 ApplicationContext启动后预载入所有的单实例Bean通过预载入单实例bean ,确保当你需要的时候你就不用等待因为它们已经创建好了。
相对于基本的BeanFactoryApplicationContext 唯一的不足是占用内存空间。当应用程序配置Bean较多时程序启动较慢。
创建方式
BeanFactory通常以编程的方式被创建ApplicationContext还能以声明的方式创建如使用ContextLoader。
注册方式
BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用但两者之间的区别是BeanFactory需要手动注册而ApplicationContext则是自动注册。
Spring 如何设计容器的BeanFactory和ApplicationContext的关系详解 Spring 作者 Rod Johnson 设计了两个接口用以表示容器。
BeanFactory ApplicationContext BeanFactory 简单粗暴可以理解为就是个 HashMapKey 是 BeanNameValue 是 Bean 实例。通常只提供注册put获取get这两个功能。我们可以称之为 “低级容器”。
ApplicationContext 可以称之为 “高级容器”。因为他比 BeanFactory 多了更多的功能。他继承了多个接口。因此具备了更多的功能。例如资源的获取支持多种消息例如 JSP tag 的支持对 BeanFactory 多了工具级别的支持等待。所以你看他的名字已经不是 BeanFactory 之类的工厂了而是 “应用上下文” 代表着整个大容器的所有功能。该接口定义了一个 refresh 方法此方法是所有阅读 Spring 源码的人的最熟悉的方法用于刷新整个容器即重新加载/刷新所有的 bean。
当然除了这两个大接口还有其他的辅助接口这里就不介绍他们了。
BeanFactory和ApplicationContext的关系
为了更直观的展示 “低级容器” 和 “高级容器” 的关系这里通过常用的 ClassPathXmlApplicationContext 类来展示整个容器的层级 UML 关系。 有点复杂 先不要慌我来解释一下。
最上面的是 BeanFactory下面的 3 个绿色的都是功能扩展接口这里就不展开讲。
看下面的隶属 ApplicationContext 粉红色的 “高级容器”依赖着 “低级容器”这里说的是依赖不是继承哦。他依赖着 “低级容器” 的 getBean 功能。而高级容器有更多的功能支持不同的信息源头可以访问文件资源支持应用事件Observer 模式。
通常用户看到的就是 “高级容器”。 但 BeanFactory 也非常够用啦
左边灰色区域的是 “低级容器” 只负载加载 Bean获取 Bean。容器其他的高级功能是没有的。例如上图画的 refresh 刷新 Bean 工厂所有配置生命周期事件回调等。
小结
说了这么多不知道你有没有理解Spring IoC 这里小结一下IoC 在 Spring 里只需要低级容器就可以实现2 个步骤
加载配置文件解析成 BeanDefinition 放在 Map 里。
调用 getBean 的时候从 BeanDefinition 所属的 Map 里拿出 Class 对象进行实例化同时如果有依赖关系将递归调用 getBean 方法 —— 完成依赖注入。
上面就是 Spring 低级容器BeanFactory的 IoC。
至于高级容器 ApplicationContext他包含了低级容器的功能当他执行 refresh 模板方法的时候将刷新整个容器的 Bean。同时其作为高级容器包含了太多的功能。一句话他不仅仅是 IoC。他支持不同信息源头支持 BeanFactory 工具类支持层级容器支持访问文件资源支持事件发布通知支持接口回调等等。
ApplicationContext通常的实现是什么 FileSystemXmlApplicationContext 此容器从一个XML文件中加载beans的定义XML Bean 配置文件的全路径名必须提供给它的构造函数。
ClassPathXmlApplicationContext此容器也从一个XML文件中加载beans的定义这里你需要正确设置classpath因为这个容器将在classpath里找bean配置。
WebXmlApplicationContext此容器加载一个XML文件此文件定义了一个WEB应用的所有bean。
什么是Spring的依赖注入
控制反转IoC是一个很大的概念可以用不同的方式来实现。其主要实现方式有两种依赖注入和依赖查找
依赖注入相对于IoC而言依赖注入(DI)更加准确地描述了IoC的设计理念。所谓依赖注入Dependency Injection即组件之间的依赖关系由容器在应用系统运行期来决定也就是由容器动态地将某种依赖关系的目标对象实例注入到应用系统中的各个关联的组件之中。组件不做定位查询只提供普通的Java方法让容器去决定依赖关系。
依赖注入的基本原则
依赖注入的基本原则是应用组件不应该负责查找资源或者其他依赖的协作对象。配置对象的工作应该由IoC容器负责“查找资源”的逻辑应该从应用组件的代码中抽取出来交给IoC容器负责。容器全权负责组件的装配它会把符合依赖关系的对象通过属性JavaBean中的setter或者是构造器传递给需要的对象。
依赖注入有什么优势
依赖注入之所以更流行是因为它是一种更可取的方式让容器全权负责依赖查询受管组件只需要暴露JavaBean的setter方法或者带参数的构造器或者接口使容器可以在初始化时组装对象的依赖关系。其与依赖查找方式相比主要优势为
查找定位操作与应用代码完全无关。
不依赖于容器的API可以很容易地在任何容器以外使用应用对象。 不需要特殊的接口绝大多数对象可以做到完全不必依赖容器。
有哪些不同类型的依赖注入实现方式
依赖注入是时下最流行的IoC实现方式依赖注入分为接口注入Interface InjectionSetter方法注入Setter Injection和构造器注入Constructor Injection三种方式。其中接口注入由于在灵活性和易用性比较差现在从Spring4开始已被废弃。
构造器依赖注入构造器依赖注入通过容器触发一个类的构造器来实现的该类有一系列参数每个参数代表一个对其他类的依赖。
Setter方法注入Setter方法注入是容器通过调用无参构造器或无参static工厂 方法实例化bean之后调用该bean的setter方法即实现了基于setter的依赖注入。
构造器依赖注入和 Setter方法注入的区别 两种依赖方式都可以使用构造器注入和Setter方法注入。最好的解决方案是用构造器参数实现强制依赖setter方法实现可选依赖。
Spring事务的实现方式和实现原理
Spring事务的本质其实就是数据库对事务的支持没有数据库的事务支持spring是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
什么是事务 数据库事务是指作为单个逻辑工作单元执行的一系列操作这些操作要么一起成功要么一起失败是一个不可分割的工作单元。
在我们日常工作中涉及到事务的场景非常多一个 service 中往往需要调用不同的 dao 层方法这些方法要么同时成功要么同时失败我们需要在 service 层确保这一点
事务的四大特性:A原子性 C一致性 I隔离性 D持久性
Spring支持的事务管理类型 spring 事务实现方式有哪些
Spring支持两种类型的事务管理
编程式事务管理这意味你通过编程的方式管理事务给你带来极大的灵活性但是难维护。
声明式事务管理这意味着你可以将业务代码和事务管理分离你只需用注解和XML配置来管理事务。
Spring中事务的实现方式
1、编程式—实现事务
在applicationContext.xml中配置好数据源和事务管理器: 以上这种方式 不推荐使用代码入侵太多。大量的处理事务的代码穿插到业务代码中 2、声明式—实现事务
(1)、声明式事务:xml形式 提前配置好数据源
配置事务管理器配置通知添加事务的切面Aop的织入将切面和切入点绑定起来 2、configration配置类的形式配置声明式事务
1、配置好数据源信息 2、配置事务管理器 3、开启事务的注解支持 将该配置类添加到包扫描路径下接来下就可以直接在service的方法或者类上使用Transactional注解给方法添加事务
3、xml注解方式配置声明式事务 配置完成后只需要在想要开启注解的方法上加上Transactional注解就可以了
说一下Spring的事务传播行为
spring事务的传播行为说的是当多个事务同时存在的时候spring如何处理这些事务的行为。
① PROPAGATION_REQUIRED默认的事务传播如果当前没有事务就创建一个新事务如果当前存在事务就加入该事务该设置是最常用的设置。
② PROPAGATION_SUPPORTS支持当前事务如果当前存在事务就加入该事务如果当前不存在事务就以非事务执行。
③ PROPAGATION_MANDATORY支持当前事务如果当前存在事务就加入该事务如果当前不存在事务就抛出异常。
④ PROPAGATION_REQUIRES_NEW创建新事务无论当前存不存在事务都创建新事务。
⑤ PROPAGATION_NOT_SUPPORTED以非事务方式执行操作如果当前存在事务就把当前事务挂起。
⑥ PROPAGATION_NEVER以非事务方式执行如果当前存在事务则抛出异常。
⑦ PROPAGATION_NESTED如果当前存在事务则在嵌套事务内执行。如果当前没有事务则按REQUIRED属性执行。
在一个事务执行的过程中调用另一个事务时候(比如一个service方法调用另一个service方法)这个事务将以何种状态存在是两个事务共存呢还是一个事务是另一个事务的子事务还是一个事务加入另一个事务的子事务呢……利用事务的传播性来解决这个问题。
1、REQUIRED: spring默认的事务的传播性 REQUIRED 表示如果当前存在事务则加入该事务如果当前没有事务则创建一个新的事务。
Service
public class AccountService {AutowiredJdbcTemplate jdbcTemplate;Transactionalpublic void handle1() {jdbcTemplate.update(update user set money ? where id?;, 1, 2);}
}
Service
public class AccountService2 {AutowiredJdbcTemplate jdbcTemplate;AutowiredAccountService accountService;public void handle2() {jdbcTemplate.update(update user set money ? where username?;, 1, zhangsan);accountService.handle1();}
}如果 handle2 方法本身是有事务的则 handle1 方法就会加入到 handle2 方法所在的事务中这样两个方法将处于同一个事务中一起成功或者一起失败不管是 handle2 还是 handle1 谁抛异常都会导致整体回滚。
如果 handle2 方法本身是没有事务的则 handle1 方法就会自己开启一个新的事务。
2、REQUIRES_NEW REQUIRES_NEW 表示创建一个新的事务如果当前存在事务则把当前事务挂起。换言之不管外部方法是否有事务REQUIRES_NEW 都会开启自己的事务。
3、NESTED NESTED 表示如果当前存在事务则创建一个事务作为当前事务的嵌套事务来运行如果当前没有事务则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED。
4、MANDATORY MANDATORY 表示如果当前存在事务则加入该事务如果当前没有事务则抛出异常。
5、SUPPORTS NOT_SUPPORTED 表示以非事务方式运行如果当前存在事务则把当前事务挂起。
6、NOT_SUPPORTED NOT_SUPPORTED 表示以非事务方式运行如果当前存在事务则把当前事务挂起。
7、NEVER NEVER 表示以非事务方式运行如果当前存在事务则抛出异常。
spring事务的实现原理
底层是通过aop进行实现Transactional注解使用环绕通知在进入方法前开启事务 。使用try catch包含目标方法执行目标方法执行完成后如果没有抛出异常就提交事务。如果抛出异常就进行回滚。
代码实现:
定义注解
Target({ElementType.TYPE, ElementType.METHOD})
Retention(RetentionPolicy.RUNTIME)
Inherited
Documented
public interface rkTransactional {
}切面
Aspect
Component
Slf4j
public class ExtrkThreadAop {Autowiredprivate RkTransaction rkTransaction;/*** 只要方法上有加上rkTransactional 走around* 异常通知* param joinPoint* throws Throwable*/Around(value annotation(com.rk.aop.rkTransactional))public Object around(ProceedingJoinPoint joinPoint) {// 在目标方法之前开启事务 底层实现:将事务状态保存在当前线程里面TransactionStatus transactionStatus rkTransaction.begin();try {Object result joinPoint.proceed();//目标方法log.info(目标方法之后执行);//提交事务rkTransaction.commit(transactionStatus);return result;} catch (Throwable throwable) {// 目标方法执行向外抛出异常之后 手动回滚rkTransaction.rollback(transactionStatus);return fail;}}
}注解类
Component
public class RkTransaction {Autowiredprivate DataSourceTransactionManager dataSourceTransactionManager;// 开启事务public TransactionStatus begin() {TransactionStatus transaction dataSourceTransactionManager.getTransaction(new DefaultTransactionAttribute());return transaction;}// 提交事务public void commit(TransactionStatus transactionStatus) {dataSourceTransactionManager.commit(transactionStatus);}// 回滚事务public void rollback(TransactionStatus transactionStatus) {dataSourceTransactionManager.rollback(transactionStatus);}
}test: 测试 /*** 使用事务注解 事务到底在什么时候提交呢该方法没有抛出异常的情况下就会自动提交事务* aop* param name* return*/GetMapping(/insertUser)rkTransactionalpublic String insertUser(String name) {int result userMapper.insertUser(name);if (rk.equals(name)) {int j 1 / 0;}return result 0 ? ok : fail;}
}Spring 事务失效的7种场景
未启用spring事务管理功能方法不是public类型的数据源未配置事务管理器自身调用问题异常类型错误异常被吞了业务和spring事务代码必须在一个线程中
1、数据库不支持事务
2、没有配置事务管理器
3、事务所在的方法没有被public修饰
4、异常被catch没有抛出事务会失效
5、异常类型错误,默认是runtimeException才会回滚的
解决方案:加上Transactional(rollbackFor Exception.class)注解;这样Exception也会回滚
6、用final或者static关键字修饰的方法事务会失效
7、事务需要从外部调用Spring 自调事务用会失效。即相同类里边A 方法没有事务B 方法有事务A 方法调用 B 方法则 B 方法的事务会失效这点尤其要注意因为代理模式只拦截通过代理传入的外部方法调用所以自调用事务是不生效的。
1.1、未启用spring事务管理功能
EnableTransactionManagement 注解用来启用spring事务自动管理事务的功能这个注解千万不要忘记写了。
1.2、方法不是public类型的
Transaction 可以用在类上、接口上、public方法上如果将Trasaction用在了非public方法上事务将无效。
1.3、数据源未配置事务管理器
spring是通过事务管理器了来管理事务的一定不要忘记配置事务管理器了要注意为每个数据源配置一个事务管理器
Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {return new DataSourceTransactionManager(dataSource);
}1.4、自身调用问题
spring是通过aop的方式对需要spring管理事务的bean生成了代理对象然后通过代理对象拦截了目标方法的执行在方法前后添加了事务的功能所以必须通过代理对象调用目标方法的时候事务才会起效。
看下面代码大家思考一个问题当外部直接调用m1的时候m2方法的事务会生效么
Component
public class UserService {public void m1(){this.m2();}Transactionalpublic void m2(){//执行db操作}
}显然不会生效因为m1中通过this的方式调用了m2方法而this并不是代理对象this.m2()不会被事务拦截器所以事务是无效的如果外部直接调用通过UserService这个bean来调用m2方法事务是有效的上面代码可以做一下调整如下1在UserService中注入了自己此时m1中的m2事务是生效的.
Component
public class UserService {Autowired //1private UserService userService;public void m1() {this.userService.m2();}Transactionalpublic void m2() {//执行db操作}
}重点必须通过代理对象访问方法事务才会生效。
1.5、异常类型错误
spring事务回滚的机制对业务方法进行try catch当捕获到有指定的异常时spring自动对事务进行回滚那么问题来了哪些异常spring会回滚事务呢
并不是任何异常情况下spring都会回滚事务默认情况下RuntimeException和Error的情况下spring事务才会回滚。
也可以自定义回滚的异常类型
Transactional(rollbackFor {异常类型列表})1.6、异常被吞了
当业务方法抛出异常spring感知到异常的时候才会做事务回滚的操作若方法内部将异常给吞了那么事务无法感知到异常了事务就不会回滚了。
如下代码事务操作2发生了异常但是被捕获了此时事务并不会被回滚
Transactional
public void m1(){//事务操作1try{//事务操作2内部抛出了异常}catch(Exception e){}
}1.7、业务和spring事务代码必须在一个线程中 spring事务实现中使用了ThreadLocalThreadLocal大家应该知道吧可以实现同一个线程中数据共享必须是同一个线程的时候数据才可以共享这就要求业务代码必须和spring事务的源码执行过程必须在一个线程中才会受spring事务的控制比如下面代码方法内部的子线程内部执行的事务操作将不受m1方法上spring事务的控制这个大家一定要注意
Transactional
public void m1() {new Thread() {//一系列事务操作}.start();
}2、如何快速定位事务相关bug
2种方式
方式1看日志
如果你使用了logback或者log4j来输出日志可以修改一下日志级别为debug模式可以看到事务的详细执行日志帮助你定位错误
方式2调试代码
如果你对源码比较了解那么你会知道被spring管理事务的业务方法执行的时候都会被TransactionInterceptor拦截器拦截会进入到它的invoke方法中咱们可以在invoke方法中设置一些断点可以看到详细的执行过程排错也就比较容易了。
整体上来说还是需要你深入理解原理原理了解了写代码的时候本身就会避免很多坑。
说一下 spring 的事务隔离
spring 有五大隔离级别默认值为 ISOLATION_DEFAULT使用数据库的设置其他四个隔离级别和数据库的隔离级别一致
ISOLATION_DEFAULT用底层数据库的设置隔离级别数据库设置的是什么我就用什么
ISOLATION_READ_UNCOMMITTED读未提交最低隔离级别、事务未提交前就可被其他事务读取会出现幻读、脏读、不可重复读
ISOLATION_READ_COMMITTED读已提交一个事务提交后才能被其他事务读取到会造成幻读、不可重复读SQL server 的默认级别
ISOLATION_REPEATABLE_READ可重复读保证多次读取同一个数据时其值都和事务开始时候的内容是一致禁止读取到别的事务未提交的数据会造成幻读MySQL 的默认级别
ISOLATION_SERIALIZABLE序列化代价最高最可靠的隔离级别该隔离级别能防止脏读、不可重复读、幻读。
脏读 表示一个事务能够读取另一个事务中还未提交的数据。比如某个事务尝试插入记录 A此时该事务还未提交然后另一个事务尝试读取到了记录 A。
不可重复读 是指在一个事务内多次读同一数据。
幻读 指同一个事务内多次查询返回的结果集不一样。比如同一个事务 A 第一次查询时候有 n 条记录但是第二次同等条件下查询却有 n1 条记录这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据同一个记录的数据内容被修改了所有数据行的记录就变多或者变少了。
注意事务的隔离级别和数据库并发性是成反比的隔离级别越高并发性越低。
Spring框架的事务管理有哪些优点
为不同的事务API 如 JTAJDBCHibernateJPA 和JDO提供一个不变的编程模式。 为编程式事务管理提供了一套简单的API而不是一些复杂的事务API 支持声明式事务管理。 和Spring各种数据访问抽象层很好得集成。
你更倾向用哪种事务管理类型
大多数Spring框架的用户选择声明式事务管理因为它对应用代码的影响最小因此更符合一个无侵入的轻量级容器的思想。声明式事务管理要优于编程式事务管理虽然比编程式事务管理这种方式允许你通过代码控制事务少了一点灵活性。唯一不足地方是最细粒度只能作用到方法级别无法做到像编程式事务那样可以作用到代码块级别。
Spring Beans
什么是Spring beans Spring beans 是那些形成Spring应用的主干的java对象。它们被Spring IOC容器初始化装配和管理。这些beans通过容器中配置的元数据创建。比如以XML文件中 的形式定义。
一个 Spring Bean 定义 包含什么 一个Spring Bean 的定义包含容器必知的所有配置元数据包括如何创建一个bean它的生命周期详情及它的依赖。
如何给Spring 容器提供配置元数据Spring有几种配置方式 这里有三种重要的方法给Spring 容器提供配置元数据。
XML配置文件。 基于注解的配置。 基于java的配置。 Spring配置文件包含了哪些信息 Spring配置文件是个XML 文件这个文件包含了类信息描述了如何配置它们以及如何相互调用。
Spring基于xml注入bean的几种方式 Set方法注入
构造器注入①通过index设置参数的位置②通过type设置参数类型
静态工厂注入
实例工厂
你怎样定义类的作用域 当定义一个 在Spring里我们还能给这个bean声明一个作用域。它可以通过bean 定义中的scope属性来定义。如当Spring要在需要的时候每次生产一个新的bean实例bean的scope属性被指定为prototype。另一方面一个bean每次使用的时候必须返回同一个实例这个bean的scope 属性 必须设为 singleton。
解释Spring支持的几种bean的作用域 Spring框架支持以下五种bean的作用域
singleton : bean在每个Spring ioc 容器中只有一个实例。 prototype一个bean的定义可以有多个实例。 request每次http请求都会创建一个bean该作用域仅在基于web的Spring ApplicationContext情形下有效。 session在一个HTTP Session中一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。 global-session在一个全局的HTTP Session中一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。 注意 缺省的Spring bean 的作用域是Singleton。使用 prototype 作用域需要慎重的思考因为频繁创建和销毁 bean 会带来很大的性能开销。
Spring框架中的单例bean是线程安全的吗 不是Spring框架中的单例bean不是线程安全的。
spring 中的 bean 默认是单例模式spring 框架并没有对单例 bean 进行多线程的封装处理。
实际上大部分时候 spring bean 无状态的比如 dao 类所有某种程度上来说 bean 也是安全的但如果 bean 有状态的话比如 view model 对象那就要开发者自己去保证线程安全了最简单的就是改变 bean 的作用域把“singleton”变更为“prototype”这样请求 bean 相当于 new Bean()了所以就可以保证线程安全了。
有状态就是有数据存储功能。 无状态就是不会保存数据。 Spring如何处理线程并发问题 在一般情况下只有无状态的Bean才可以在多线程环境下共享在Spring中绝大部分Bean都可以声明为singleton作用域因为Spring对一些Bean中非线程安全状态采用ThreadLocal进行处理解决线程安全问题。
ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式仅提供一份变量不同的线程在访问前需要获取锁没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。
ThreadLocal会为每一个线程提供一个独立的变量副本从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象在编写多线程代码时可以把不安全的变量封装进ThreadLocal。
请解释Spring Bean的生命周期 首先说一下Servlet的生命周期实例化初始init接收请求service销毁destroy
Spring上下文中的Bean生命周期也类似如下
1实例化Bean
对于BeanFactory容器当客户向容器请求一个尚未初始化的bean时或初始化bean的时候需要注入另一个尚未初始化的依赖时容器就会调用createBean进行实例化。对于ApplicationContext容器当容器启动结束后通过获取BeanDefinition对象中的信息实例化所有的bean。
2设置对象属性依赖注入
实例化后的对象被封装在BeanWrapper对象中紧接着Spring根据BeanDefinition中的信息 以及 通过BeanWrapper提供的设置属性的接口完成依赖注入。
3处理Aware接口
接着Spring会检测该对象是否实现了xxxAware接口并将相关的xxxAware实例注入给Bean
①如果这个Bean已经实现了BeanNameAware接口会调用它实现的setBeanName(String beanId)方法此处传递的就是Spring配置文件中Bean的id值
②如果这个Bean已经实现了BeanFactoryAware接口会调用它实现的setBeanFactory()方法传递的是Spring工厂自身。
③如果这个Bean已经实现了ApplicationContextAware接口会调用setApplicationContext(ApplicationContext)方法传入Spring上下文
4BeanPostProcessor
如果想对Bean进行一些自定义的处理那么可以让Bean实现了BeanPostProcessor接口那将会调用postProcessBeforeInitialization(Object obj, String s)方法。
5InitializingBean 与 init-method
如果Bean在Spring配置文件中配置了 init-method 属性则会自动调用其配置的初始化方法。
6如果这个Bean实现了BeanPostProcessor接口将会调用postProcessAfterInitialization(Object obj, String s)方法由于这个方法是在Bean初始化结束时调用的所以可以被应用于内存或缓存技术
以上几个步骤完成后Bean就已经被正确创建了之后就可以使用这个Bean了。
7DisposableBean
当Bean不再需要时会经过清理阶段如果Bean实现了DisposableBean这个接口会调用其实现的destroy()方法
8destroy-method
最后如果这个Bean的Spring配置中配置了destroy-method属性会自动调用其配置的销毁方法。
解释Spring支持的几种bean的作用域
Spring容器中的bean可以分为5个范围
1singleton默认每个容器中只有一个bean的实例单例的模式由BeanFactory自身来维护。
2prototype为每一个bean请求提供一个实例。
3request为每一个网络请求创建一个实例在请求完成以后bean会失效并被垃圾回收器回收。
4session与request范围类似确保每个session中有一个bean的实例在session过期后bean会随之失效。
5global-session全局作用域global-session和Portlet应用相关。当你的应用部署在Portlet容器中工作时它包含很多portlet。如果你想要声明让所有的portlet共用全局的存储变量的话那么这全局变量需要存储在global-session中。全局作用域与Servlet中的session作用域效果相同。
8、Spring框架中的单例Beans是线程安全的么 Spring框架并没有对单例bean进行任何多线程的封装处理。关于单例bean的线程安全和并发问题需要开发者自行去搞定。但实际上大部分的Spring bean并没有可变的状态(比如Serview类和DAO类)所以在某种程度上说Spring的单例bean是线程安全的。如果你的bean有多种状态的话比如 View Model 对象就需要自行保证线程安全。最浅显的解决办法就是将多态bean的作用域由“singleton”变更为“prototype”。Spring如何处理线程并发问题
在一般情况下只有无状态的Bean才可以在多线程环境下共享在Spring中绝大部分Bean都可以声明为singleton作用域因为Spring对一些Bean中非线程安全状态采用ThreadLocal进行处理解决线程安全问题。
ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式仅提供一份变量不同的线程在访问前需要获取锁没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。
ThreadLocal会为每一个线程提供一个独立的变量副本从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象在编写多线程代码时可以把不安全的变量封装进ThreadLocal。
Spring基于xml注入bean的几种方式
1Set方法注入
2构造器注入①通过index设置参数的位置②通过type设置参数类型
3静态工厂注入
4实例工厂
详细内容可以阅读https://www.iteye.com/blog/blessht-1162131
Spring的自动装配
在spring中对象无需自己查找或创建与其关联的其他对象由容器负责把需要相互协作的对象引用赋予各个对象使用autowire来配置自动装载模式。
在Spring框架xml配置中共有5种自动装配
1no默认的方式是不进行自动装配的通过手工设置ref属性来进行装配bean。
2byName通过bean的名称进行自动装配如果一个bean的 property 与另一bean 的name 相同就进行自动装配。
3byType通过参数的数据类型进行自动装配。
4constructor利用构造函数进行装配并且构造函数的参数通过byType进行装配。
5autodetect自动探测如果有构造方法通过 construct的方式自动装配否则使用 byType的方式自动装配。
基于注解的方式
使用Autowired注解来自动装配指定的bean。在使用Autowired注解之前需要在Spring配置文件进行配置context:annotation-config /。在启动spring IoC时容器自动装载了一个AutowiredAnnotationBeanPostProcessor后置处理器当容器扫描到Autowied、Resource或Inject时就会在IoC容器自动查找需要的bean并装配给该对象的属性。在使用Autowired时首先在容器中查询对应类型的bean
如果查询结果刚好为一个就将该bean装配给Autowired指定的数据
如果查询的结果不止一个那么Autowired会根据名称来查找
如果上述查找的结果为空那么会抛出异常。解决方法时使用requiredfalse。
Autowired可用于构造函数、成员变量、Setter方法
注Autowired和Resource之间的区别
(1) Autowired默认是按照类型装配注入的默认情况下它要求依赖对象必须存在可以设置它required属性为false。
(2) Resource默认是按照名称来装配注入的只有当找不到与名称匹配的bean才会按照类型来装配注入。
Spring 框架中都用到了哪些设计模式
1工厂模式BeanFactory就是简单工厂模式的体现用来创建对象的实例
2单例模式Bean默认为单例模式。
3代理模式Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术
4模板方法用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。
5观察者模式定义对象键一种一对多的依赖关系当一个对象的状态发生改变时所有依赖于它的对象都会得到通知被制动更新如Spring中listener的实现–ApplicationListener。
spring 是如何开启事务的
核心原理
Spring事务管理的实现有许多细节如果对整个接口框架有个大体了解会非常有利于我们理解事务下面通过讲解Spring的事务接口来了解Spring实现事务的具体策略。
Spring事务管理涉及的接口的联系如下 spring开启事务有两种
1 基于注解开启事务
只需要在方法头上加一个注解Transactional即可 2 基于代码来开启事务
在需要开启事务的方法中输入以下代码即可
Autowired
private PlatformTransactionManager txManager;
// 开启事务管理DefaultTransactionDefinition def new DefaultTransactionDefinition();def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);TransactionStatus status txManager.getTransaction(def);try {//操作都放在这里}catch(Exception e){//这里是异常处理}txManager.commit(status);//事务提交spring的事务在什么场景下会失效
1如果数据库不支持事务则失效
因为事务是作用于数据库。例如使用MySQL且引擎是MyISAM则事务会不起作用因为MyISAM引擎本身不支持事务如果改成InnoDB则可以。
2Service类没有被Spring管理
因为Spring的事务是基于AOP所以如果Service类没有被Spring管理变成一个Spring Bean即使添加了**Transactional**注解事务也是无效的。
**3**内部调用
不带事务的方法调用该类中带事务的方法不会回滚。因为Spring的回滚是用过代理模式生成的如果是一个不带事务的方法调用该类的带事务的方法直接通过this.xxx()调用而不生成代理事务所以事务不起作用。常见解决方法“拆类”。
4使用默认的事务处理方式
Spring的事务默认是对RuntimeException进行回滚而不继承RuntimeException的不回滚。因为在java的设计中它认为不继承RuntimeException的异常是CheckException或普通异常如IOException这些异常在java语法中是要求强制处理的。对于这些普通异常Spring默认它们都已经处理所以默认不回滚。可以添加rollbackforException.class来表示所有的Exception都回滚。
5事务只能应用于 public 方法
Transactional注解只能应用于public方法如果你在protected、private或者默认可见性的方法上使用 Transactional 注解这将被忽略也不会抛出任何异常。
6数据源没有配置事务管理器
spring 对事务如何进行管理
参考文章spring对事务的管理
Spring 支持两种方式事务管理
一编程式的事务管理 通过TransactionTemplate手动管理事务
在实际应用中很少使用原因是要修改原来的代码加入事务管理代码 侵入性
二声明式事务管理XML配置文件方式或注解方式
Spring的声明式事务是通过AOP实现的环绕通知
开发中经常使用代码侵入性最小–推荐使用
注后面会演示声明式事务管理的两种方式xml配置文件方式和注解方式不演示编程式方式
Spring的事务机制包括声明式事务和编程式事务。
编程式事务管理Spring推荐使用TransactionTemplate实际开发中使用声明式事务较多。
声明式事务管理将我们从复杂的事务处理中解脱出来获取连接关闭连接、事务提交、回滚、异常处理等这些操作都不用我们处理了Spring都会帮我们处理。
声明式事务管理使用了AOP面向切面编程实现的本质就是在目标方法执行前后进行拦截。在目标方法执行前加入或创建一个事务在执行方法执行后根据实际情况选择提交或是回滚事务。
Spring JDBC-Spring对事务管理的支持
参考 Spring对事务管理的支持 此文章。
spring如何对bean进行自动的事务管理
1、Spring事务管理概述
Spring的事务管理简化了传统的事务管理流程并且在一定程度上减少了开发者的工作量。
1.1 事务管理的核心接口 在Spring的所有JAR包中包含一个名为Spring-tx-4.3.6.RELEASE的JAR包该包就是Spring提供的用于事务管理的依赖包。在该JAR包的org.Springframework.transaction包中有3个接口文件PlatformTransactionManager、TransactionDefinition和TransactionStatus。
PlatformTransactionManager PlatformTransactionManager接口是Spring提供的平台事务管理器主要用于管理事务。该接口中提供了3个事务操作的方法具体如下。
TransactionStatus getTransaction(TransactionDefinition definition)用于获取事务状态信息。该方法会根据TransactionDefinition参数返回一个TransactionStatus对象。TransactionStatus对象表示一个事务被关联在当前执行的线程上。
void commit(TransactionStatus status)用于提交事务。
void rollback(TransactionStatus status)用于回滚事务。
PlatformTransactionManager接口只是代表事务管理的接口并不知道底层是如何管理事务的它只需要事务管理提供上面的3个方法但具体如何管理事务则由它的实现类来完成。
PlatformTransactionManager接口有许多不同的实现类常见的几个实现类如下。
org.springframework.jdbc.datasource.DataSourceTransactionManager用于配置JDBC数据源的事务管理器。 org.springframework.orm.Hibernate4.HibernateTransactionManager用于配置Hibernate的事务管理器。 org.springframework.transaction.jta.JtaTransactionManager用于配置全局事务管理器。 当底层采用不同的持久层技术时系统只需使用不同的PlatformTransactionManager实现类即可。
2、TransactionDefinition
TransactionDefinition接口是事务定义描述的对象该对象中定义了事务规则并提供了获取事务相关信息的方法具体如下
string getName()获取事务对象名称。 int getlsolationLeve()获取事务的隔离级别。 int getPropagationBehavior()获取事务的传播行为。 int setTimeout()获取事务的超时时间。 boolean isReadOnly()获取事务是否只读。
上述方法中事务的传播行为是指在同一个方法中不同操作前后所使用的事务。传播行为有很多种具体如表所示。 在事务管理过程中传播行为可以控制是否需要创建事务以及如何创建事务。通常情况下数据的查询不会影响原数据的改变所以不需要进行事务管理而对于数据的插入、更新和删除操作必须进行事务管理。如果没有指定事务的传播行为Spring默认传播行为是REQUIRED。
3、TransactionStatus
TransactionStatus接口是事务的状态描述了某一时间点上事务的状态信息。该接口中包含6个方法具体如下
void flush()刷新事务。 boolean hasSavepoint()获取是否存在保存点。 boolean isCompleted()获取事务是否完成。 boolean isNewTransaction()获取是否是新事务。 boolean isRollbackOnly()获取是否回滚。 void setRollbackOnly()设置事务回滚。 1.2 事务管理的方式 Spring中的事务管理分为两种方式一种是传统的编程序事务管理另一种是声明式事务管理。
编程序事务管理通过编写代码实现的事务管理包括定义事务的开始、正常执行后的事务提交和异常时的事务回滚。 声明式事务管理通过AOP技术实现的事务管理其主要思想是将事务管理作为一个“切面”代码单独编写然后通过AOP技术将事务管理的“切面”代码植入业务目标类中。 声明式事务管理最大的优点在于开发者无须通过编程的方式来管理事务只需在配置文件中进行相关的事务规则声明就可以将事务规则应用到业务逻辑中。这使得开发人员可以更加专注于核心业务逻辑代码的编写在一定程度上减少了工作量提高了开发效率。所以在实际开发中通常都推荐使用声明式事务管理。
4、声明式事务管理
Spring的声明式事务管理可以通过两种方式来实现一种是基于XML的方式另一种是基于Annotation的方式。
2.1 基于XML方式的声明式事务 基于XML方式的声明式事务管理是通过在配置文件中配置事务规则的相关声明来实现的。
Spring 2.0以后提供了tx命名空间来配置事务tx命名空间下提供了tx:advice元素来配置事务的通知增强处理。当使用tx:advice元素配置了事务的增强处理后就可以通过编写的AOP配置让Spring自动对目标生成代理。配置tx:advice元素时通常需要指定id和transaction-manager属性其中id属性是配置文件中的唯一标识transaction-manager属性用于指定事务管理器。除此之外还需要配置一个tx:attributes子元素该子元素可通过配置多个tx:method子元素来配置执行事务的细节。
关于tx:method元素的属性描述如表所示。 Spring事务的实现方式和实现原理
Spring事务的本质其实就是数据库对事务的支持没有数据库的事务支持spring是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
1Spring事务的种类
spring支持编程式事务管理和声明式事务管理两种方式
①编程式事务管理使用TransactionTemplate。
②声明式事务管理建立在AOP之上的。其本质是通过AOP功能对方法前后进行拦截将事务处理的功能编织到拦截的方法中也就是在目标方法开始之前加入一个事务在执行完目标方法之后根据执行情况提交或者回滚事务。
声明式事务最大的优点就是不需要在业务逻辑代码中掺杂事务管理的代码只需在配置文件中做相关的事务规则声明或通过Transactional注解的方式便可以将事务规则应用到业务逻辑中。
声明式事务管理要优于编程式事务管理这正是spring倡导的非侵入式的开发方式使业务代码不受污染只要加上注解就可以获得完全的事务支持。唯一不足地方是最细粒度只能作用到方法级别无法做到像编程式事务那样可以作用到代码块级别。
2spring的事务传播行为
spring事务的传播行为说的是当多个事务同时存在的时候spring是如何处理这些事务的行为。
① PROPAGATION_REQUIRED如果当前没有事务就创建一个新事务如果当前存在事务就加入该事务该设置是最常用的设置。
② PROPAGATION_SUPPORTS支持当前事务如果当前存在事务就加入该事务如果当前不存在事务就以非事务执行。‘
③ PROPAGATION_MANDATORY支持当前事务如果当前存在事务就加入该事务如果当前不存在事务就抛出异常。
④ PROPAGATION_REQUIRES_NEW创建新事务无论当前存不存在事务都创建新事务。
⑤ PROPAGATION_NOT_SUPPORTED以非事务方式执行操作如果当前存在事务就把当前事务挂起。
⑥ PROPAGATION_NEVER以非事务方式执行如果当前存在事务则抛出异常。
⑦ PROPAGATION_NESTED如果当前存在事务则在嵌套事务内执行。如果当前没有事务则按REQUIRED属性执行。
3Spring中的隔离级别
① ISOLATION_DEFAULT这是个 PlatfromTransactionManager 默认的隔离级别使用数据库默认的事务隔离级别。
② ISOLATION_READ_UNCOMMITTED读未提交允许另外一个事务可以看到这个事务未提交的数据。
③ ISOLATION_READ_COMMITTED读已提交保证一个事务修改的数据提交后才能被另一事务读取而且能看到该事务对已有记录的更新。
④ ISOLATION_REPEATABLE_READ可重复读保证一个事务修改的数据提交后才能被另一事务读取但是不能看到该事务对已有记录的更新。
⑤ ISOLATION_SERIALIZABLE可串行化一个事务在执行的过程中完全看不到其他事务对数据库所做的更新。
4spring中事务的配置方式 配置文件配置方式 tx:advice idtxAdvice transaction-managertxManager tx:attributes !--设置所有匹配的方法然后设置传播级别和事务隔离--tx:method namesave* propagationREQUIRED / tx:method nameadd* propagationREQUIRED / tx:method namecreate* propagationREQUIRED / tx:method nameinsert* propagationREQUIRED / tx:method nameupdate* propagationREQUIRED / tx:method namemerge* propagationREQUIRED / tx:method namedel* propagationREQUIRED / tx:method nameremove* propagationREQUIRED / tx:method nameput* propagationREQUIRED / tx:method nameget* propagationSUPPORTS read-onlytrue / tx:method namecount* propagationSUPPORTS read-onlytrue / tx:method namefind* propagationSUPPORTS read-onlytrue / tx:method namelist* propagationSUPPORTS read-onlytrue / tx:method name* propagationSUPPORTS read-onlytrue / /tx:attributes
/tx:advice 注解的方式 !--开启注解的方式--
tx:annotation-driven transaction-managertransactioManager /Transactional(propagationPropagation.REQUIRED) 如果有事务, 那么加入事务, 没有的话新建一个(默认情况下) Transactional(propagationPropagation.NOT_SUPPORTED) 容器不为这个方法开启事务 Transactional(propagationPropagation.REQUIRES_NEW) 不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务 Transactional(propagationPropagation.MANDATORY) 必须在一个已有的事务中执行,否则抛出异常 Transactional(propagationPropagation.NEVER) 必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反) Transactional(propagationPropagation.SUPPORTS) 如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务.
(5) Spring框架中有哪些不同类型的事件
Spring 提供了以下5种标准的事件
1上下文更新事件ContextRefreshedEvent在调用ConfigurableApplicationContext 接口中的refresh()方法时被触发。
2上下文开始事件ContextStartedEvent当容器调用ConfigurableApplicationContext的Start()方法开始/重新开始容器时触发该事件。
3上下文停止事件ContextStoppedEvent当容器调用ConfigurableApplicationContext的Stop()方法停止容器时触发该事件。
4上下文关闭事件ContextClosedEvent当ApplicationContext被关闭时触发该事件。容器被关闭时其管理的所有单例Bean都被销毁。
5请求处理事件RequestHandledEvent在Web应用中当一个http请求request结束触发该事件。
如果一个bean实现了ApplicationListener接口当一个ApplicationEvent 被发布以后bean会自动被通知。
五、springBoot
springBoot的实现原理
参考的url
什么是 Spring Boot
Spring Boot 是 Spring 开源组织下的子项目是 Spring 组件一站式解决方案主要是简化了使用 Spring 的难度简省了繁重的配置提供了各种启动器开发者能快速上手。
SpringBoot是什么
springboot是依赖于spring的比起spring除了拥有spring的全部功能以外springboot无需繁琐的xml配置这取决于它自身强大的自动装配功能并且自身已嵌入Tomcat、Jetty等web容器集成了springmvc使得springboot可以直接运行不需要额外的容器提供了一些大型项目中常见的非功能性特性如嵌入式服务器、安全、指标健康检测、外部配置等
其实spring大家都知道boot是启动的意思。所以spring boot其实就是一个启动spring项目的一个工具而已总而言之springboot 是一个服务于框架的框架也可以说springboot是一个工具这个工具简化了spring的配置
Spring Boot的核心功能
1、 可独立运行的Spring项目Spring Boot可以以jar包的形式独立运行。
2、 内嵌的Servlet容器Spring Boot可以选择内嵌Tomcat、Jetty或者Undertow无须以war包形式部署项目。
3、 简化的Maven配置Spring提供推荐的基础 POM 文件来简化Maven 配置。
4、 自动配置SpringSpring Boot会根据项目依赖来自动配置Spring 框架极大地减少项目要使用的配置。
5、 提供生产就绪型功能提供可以直接在生产环境中使用的功能如性能指标、应用信息和应用健康检查。
6、 无代码生成和xml配置Spring Boot不生成代码。完全不需要任何xml配置即可实现Spring的所有配置。
Spring Boot 主要有如下优点
容易上手提升开发效率为 Spring 开发提供一个更快、更广泛的入门体验。 开箱即用远离繁琐的配置。 提供了一系列大型项目通用的非业务性功能例如内嵌服务器、安全管理、运行数据监控、运行状况检查和外部化配置等。 没有代码生成也不需要XML配置。 避免大量的 Maven 导入和各种版本冲突。
SpringBoot启动过程-流程
springboot的启动经过了一些一系列的处理我们先看看整体过程的流程图 一、SpringBoot启动的时候会构造一个SpringApplication的实例构造SpringApplication的时候会进行初始化的工作初始化的时候会做以下几件事
1、把参数sources设置到SpringApplication属性中这个sources可以是任何类型的参数. 2、判断是否是web程序并设置到webEnvironment的boolean属性中. 3、创建并初始化ApplicationInitializer设置到initializers属性中 。 4、创建并初始化ApplicationListener设置到listeners属性中 。 5、初始化主类mainApplicatioClass。
二、SpringApplication构造完成之后调用run方法启动SpringApplicationrun方法执行的时候会做以下几件事
1、构造一个StopWatch计时器用来记录SpringBoot的启动时间 。 2、初始化监听器获取SpringApplicationRunListeners并启动监听用于监听run方法的执行。 3、创建并初始化ApplicationArguments,获取run方法传递的args参数。 4、创建并初始化ConfigurableEnvironment环境配置。封装main方法的参数初始化参数写入到 Environment中发布 ApplicationEnvironmentPreparedEvent环境事件做一些绑定后返回Environment。 5、打印banner和版本。 6、构造Spring容器(ApplicationContext)上下文。先填充Environment环境和设置的参数如果application有设置beanNameGeneratorbean、resourceLoader加载器就将其注入到上下文中。调用初始化的切面发布ApplicationContextInitializedEvent上下文初始化事件。 7、SpringApplicationRunListeners发布finish事件。 8、StopWatch计时器停止计时日志打印总共启动的时间。 9、发布SpringBoot程序已启动事件(started()) 10、调用ApplicationRunner和CommandLineRunner 11、最后发布就绪事件ApplicationReadyEvent标志着SpringBoot可以处理就收的请求了(running())
Spring Boot 的核心注解是哪个
由以下注解组成
启动类上面的注解是**SpringBootApplication**它也是 Spring Boot 的核心注解主要组合包含了以下 3 个注解
SpringBootConfiguration组合了 Configuration 注解实现配置文件的功能。
EnableAutoConfiguration打开自动配置的功能也可以关闭某个自动配置的选项如关闭数据源自动配置功能 SpringBootApplication(exclude { DataSourceAutoConfiguration.class })。
ComponentScanSpring组件扫描。
配置
什么是 JavaConfig
Spring JavaConfig 是 Spring 社区的产品它提供了配置 Spring IoC 容器的纯Java 方法。因此它有助于避免使用 XML 配置。使用 JavaConfig 的优点在于
1面向对象的配置。由于配置被定义为 JavaConfig 中的类因此用户可以充分利用 Java 中的面向对象功能。一个配置类可以继承另一个重写它的**Bean** 方法等。
2减少或消除 XML 配置。基于依赖注入原则的外化配置的好处已被证明。但是许多开发人员不希望在 XML 和 Java 之间来回切换。JavaConfig 为开发人员提供了一种纯 Java 方法来配置与 XML 配置概念相似的 Spring 容器。从技术角度来讲只使用 JavaConfig 配置类来配置容器是可行的但实际上很多人认为将JavaConfig 与 XML 混合匹配是理想的。
3类型安全和重构友好。JavaConfig 提供了一种类型安全的方法来配置 Spring容器。由于 Java 5.0 对泛型的支持现在可以按类型而不是按名称检索 bean不需要任何强制转换或基于字符串的查找。
Spring Boot 自动配置原理是什么
注解 EnableAutoConfiguration, Configuration, ConditionalOnClass 就是自动配置的核心
EnableAutoConfiguration 给容器导入META-INF/spring.factories 里定义的自动配置类。
筛选有效的自动配置类。
每一个自动配置类结合对应的 xxxProperties.java 读取配置文件进行自动配置功能
你如何理解 Spring Boot 配置加载顺序
在 Spring Boot 里面可以使用以下几种方式来加载配置。
1properties文件
2YAML文件
3系统环境变量
4命令行参数
等等……
什么是 YAML YAML 是一种人类可读的数据序列化语言。它通常用于配置文件。与属性文件相比如果我们想要在配置文件中添加复杂的属性YAML 文件就更加结构化而且更少混淆。可以看出 YAML 具有分层配置数据。
YAML 配置的优势在哪里 ? YAML 现在可以算是非常流行的一种配置文件格式了无论是前端还是后端都可以见到 YAML 配置。那么 YAML 配置和传统的 properties 配置相比到底有哪些优势呢
配置有序在一些特殊的场景下配置有序很关键 支持数组数组中的元素可以是基本数据类型也可以是对象 简洁 相比 properties 配置文件YAML 还有一个缺点就是不支持 PropertySource 注解导入自定义的 YAML 配置。
Spring Boot 是否可以使用 XML 配置 ? Spring Boot 推荐使用 Java 配置而非 XML 配置但是 Spring Boot 中也可以使用 XML 配置通过 ImportResource 注解可以引入一个 XML 配置。
**spring boot 核心配置文件是什么**bootstrap.properties 和 application.properties 有何区别 ? 单纯做 Spring Boot 开发可能不太容易遇到 bootstrap.properties 配置文件但是在结合 Spring Cloud 时这个配置就会经常遇到了特别是在需要加载一些远程配置文件的时侯。
spring boot 核心的两个配置文件
bootstrap (. yml 或者 . properties)boostrap 由父 ApplicationContext 加载的比 applicaton 优先加载配置在应用程序上下文的引导阶段生效。一般来说我们在 Spring Cloud Config 或者 Nacos 中会用到它。且 boostrap 里面的属性不能被覆盖 application (. yml 或者 . properties) 由ApplicatonContext 加载用于 spring boot 项目的自动化配置。
application.yml的优先级高于application.properties 所以优先加载yml的配置文件
什么是 Spring Profiles
Spring Profiles 允许用户根据配置文件devtestprod 等来注册 bean。因此当应用程序在开发中运行时只有某些 bean 可以加载而在 PRODUCTION中某些其他 bean 可以加载。假设我们的要求是 Swagger 文档仅适用于 QA 环境并且禁用所有其他文档。这可以使用配置文件来完成。Spring Boot 使得使用配置文件非常简单。
如何在自定义端口上运行 Spring Boot 应用程序 为了在自定义端口上运行 Spring Boot 应用程序您可以在application.properties 中指定端口。server.port 8090
安全
如何实现 Spring Boot 应用程序的安全性
为了实现 Spring Boot 的安全性我们使用 spring-boot-starter-security 依赖项并且必须添加安全配置。它只需要很少的代码。配置类将必须扩展WebSecurityConfigurerAdapter 并覆盖其方法。
比较一下 Spring Security 和 Shiro 各自的优缺点 ?
由于 Spring Boot 官方提供了大量的非常方便的开箱即用的 Starter 包括 Spring Security 的 Starter 使得在 Spring Boot 中使用 Spring Security 变得更加容易甚至只需要添加一个依赖就可以保护所有的接口所以如果是 Spring Boot 项目一般选择 Spring Security 。当然这只是一个建议的组合单纯从技术上来说无论怎么组合都是没有问题的。Shiro 和 Spring Security 相比主要有如下一些特点
Spring Security 是一个重量级的安全管理框架Shiro 则是一个轻量级的安全管理框架 Spring Security 概念复杂配置繁琐Shiro 概念简单、配置简单 Spring Security 功能强大Shiro 功能简单
Spring Boot 中如何解决跨域问题 ?
跨域可以在前端通过 JSONP 来解决但是 JSONP 只可以发送 GET 请求无法发送其他类型的请求在 RESTful 风格的应用中就显得非常鸡肋因此我们推荐在后端通过 CORSCross-origin resource sharing 来解决跨域问题。这种解决方案并非 Spring Boot 特有的在传统的 SSM 框架中就可以通过 CORS 来解决跨域问题只不过之前我们是在 XML 文件中配置 CORS 现在可以通过实现WebMvcConfigurer接口然后重写addCorsMappings方法解决跨域问题。
Configuration
public class CorsConfig implements WebMvcConfigurer {Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping(/**).allowedOrigins(*).allowCredentials(true).allowedMethods(GET, POST, PUT, DELETE, OPTIONS).maxAge(3600);}
}Spring Boot 中的监视器是什么
Spring boot actuator 是 spring 启动框架中的重要功能之一。Spring boot 监视器可帮助您访问生产环境中正在运行的应用程序的当前状态。有几个指标必须在生产环境中进行检查和监控。即使一些外部应用程序可能正在使用这些服务来向相关人员触发警报消息。监视器模块公开了一组可直接作为 HTTP URL 访问的REST 端点来检查状态。
如何在 Spring Boot 中禁用 Actuator 端点安全性
默认情况下所有敏感的 HTTP 端点都是安全的只有具有 ACTUATOR 角色的用户才能访问它们。安全性是使用标准的 HttpServletRequest.isUserInRole 方法实施的。 我们可以使用来禁用安全性。只有在执行机构端点在防火墙后访问时才建议禁用安全性。
我们如何监视所有 Spring Boot 微服务
Spring Boot 提供监视器端点以监控各个微服务的度量。这些端点对于获取有关应用程序的信息如它们是否已启动以及它们的组件如数据库等是否正常运行很有帮助。但是使用监视器的一个主要缺点或困难是我们必须单独打开应用程序的知识点以了解其状态或健康状况。想象一下涉及 50 个应用程序的微服务管理员将不得不击中所有 50 个应用程序的执行终端。为了帮助我们处理这种情况我们将使用位于的开源项目。 它建立在 Spring Boot Actuator 之上它提供了一个 Web UI使我们能够可视化多个应用程序的度量。
什么是 WebSockets
WebSocket 是一种计算机通信协议通过单个 TCP 连接提供全双工通信信道。
1、WebSocket 是双向的 -使用 WebSocket 客户端或服务器可以发起消息发送。
2、WebSocket 是全双工的 -客户端和服务器通信是相互独立的。
3、单个 TCP 连接 -初始连接使用 HTTP然后将此连接升级到基于套接字的连接。然后这个单一连接用于所有未来的通信
4、Light -与 http 相比WebSocket 消息数据交换要轻得多。
什么是 Spring Data ? Spring Data 是 Spring 的一个子项目。用于简化数据库访问支持NoSQL 和 关系数据存储。其主要目标是使数据库的访问变得方便快捷。Spring Data 具有如下特点
SpringData 项目支持 NoSQL 存储
MongoDB 文档数据库 Neo4j图形数据库 Redis键/值存储 Hbase列族数据库 SpringData 项目所支持的关系数据存储技术
JDBC JPA Spring Data Jpa 致力于减少数据访问层 (DAO) 的开发量. 开发者唯一要做的就是声明持久层的接口其他都交给 Spring Data JPA 来帮你完成Spring Data JPA 通过规范方法的名字根据符合规范的名字来确定方法需要实现什么样的逻辑。
什么是 Spring Batch Spring Boot Batch 提供可重用的函数这些函数在处理大量记录时非常重要包括日志/跟踪事务管理作业处理统计信息作业重新启动跳过和资源管理。它还提供了更先进的技术服务和功能通过优化和分区技术可以实现极高批量和高性能批处理作业。简单以及复杂的大批量批处理作业可以高度可扩展的方式利用框架处理重要大量的信息。
什么是 FreeMarker 模板 FreeMarker 是一个基于 Java 的模板引擎最初专注于使用 MVC 软件架构进行动态网页生成。使用 Freemarker 的主要优点是表示层和业务层的完全分离。程序员可以处理应用程序代码而设计人员可以处理 html 页面设计。最后使用freemarker 可以将这些结合起来给出最终的输出页面。
如何集成 Spring Boot 和 ActiveMQ 对于集成 Spring Boot 和 ActiveMQ我们使用依赖关系。 它只需要很少的配置并且不需要样板代码。
什么是 Apache Kafka Apache Kafka 是一个分布式发布 - 订阅消息系统。它是一个可扩展的容错的发布 - 订阅消息系统它使我们能够构建分布式应用程序。这是一个 Apache 顶级项目。Kafka 适合离线和在线消息消费。
什么是 Swagger你用 Spring Boot 实现了它吗 Swagger 广泛用于可视化 API使用 Swagger UI 为前端开发人员提供在线沙箱。Swagger 是用于生成 RESTful Web 服务的可视化表示的工具规范和完整框架实现。它使文档能够以与服务器相同的速度更新。当通过 Swagger 正确定义时消费者可以使用最少量的实现逻辑来理解远程服务并与其进行交互。因此Swagger消除了调用服务时的猜测。
前后端分离如何维护接口文档 ? 前后端分离开发日益流行大部分情况下我们都是通过 Spring Boot 做前后端分离开发前后端分离一定会有接口文档不然会前后端会深深陷入到扯皮中。一个比较笨的方法就是使用 word 或者 md 来维护接口文档但是效率太低接口一变所有人手上的文档都得变。在 Spring Boot 中这个问题常见的解决方案是 Swagger 使用 Swagger 我们可以快速生成一个接口文档网站接口一旦发生变化文档就会自动更新所有开发工程师访问这一个在线网站就可以获取到最新的接口文档非常方便。
如何重新加载 Spring Boot 上的更改而无需重新启动服务器Spring Boot项目如何热部署
可以使用 DEV 工具来实现。通过这种依赖关系您可以节省任何更改嵌入式tomcat 将重新启动。Spring Boot 有一个开发工具DevTools模块它有助于提高开发人员的生产力。Java 开发人员面临的一个主要挑战是将文件更改自动部署到服务器并自动重启服务器。开发人员可以重新加载 Spring Boot 上的更改而无需重新启动服务器。这将消除每次手动部署更改的需要。Spring Boot 在发布它的第一个版本时没有这个功能。这是开发人员最需要的功能。DevTools 模块完全满足开发人员的需求。该模块将在生产环境中被禁用。它还提供 H2 数据库控制台以更好地测试应用程序。
dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-devtools/artifactId
/dependencySpring Boot 打成的 jar 和普通的 jar 有什么区别 ?
Spring Boot 项目最终打包成的 jar 是可执行 jar 这种 jar 可以直接通过 java -jar xxx.jar 命令来运行这种 jar 不可以作为普通的 jar 被其他项目依赖即使依赖了也无法使用其中的类。
Spring Boot 的 jar 无法被其他项目依赖主要还是他和普通 jar 的结构不同。普通的 jar 包解压后直接就是包名包里就是我们的代码而 Spring Boot 打包成的可执行 jar 解压后在 \BOOT-INF\classes 目录下才是我们的代码因此无法被直接引用。如果非要引用可以在 pom.xml 文件中增加配置将 Spring Boot 项目打包成两个 jar 一个可执行一个可引用。
运行 Spring Boot 有哪几种方式
1打包用命令或者放到容器中运行
2用 Maven/ Gradle 插件运行
3直接执行 main 方法运行
Spring Boot 需要独立的容器运行吗
可以不需要内置了 Tomcat/ Jetty 等容器。
开启 Spring Boot 特性有哪几种方式
1继承spring-boot-starter-parent项目
2导入spring-boot-dependencies项目依赖
如何使用 Spring Boot 实现异常处理
Spring 提供了一种使用 ControllerAdvice 处理异常的非常有用的方法。 我们通过实现一个 ControlerAdvice 类来处理控制器类抛出的所有异常。
如何使用 Spring Boot 实现分页和排序
使用 Spring Boot 实现分页非常简单。使用 Spring Data-JPA 可以实现将可分页的传递给存储库方法。
微服务中如何实现 session 共享 ?
在微服务中一个完整的项目被拆分成多个不相同的独立的服务各个服务独立部署在不同的服务器上各自的 session 被从物理空间上隔离开了但是经常我们需要在不同微服务之间共享 session 常见的方案就是 Spring Session Redis 来实现 session 共享。将所有微服务的 session 统一保存在 Redis 上当各个微服务对 session 有相关的读写操作时都去操作 Redis 上的 session 。这样就实现了 session 共享Spring Session 基于 Spring 中的代理过滤器实现使得 session 的同步操作对开发人员而言是透明的非常简便。
Spring Boot 中如何实现定时任务 ?
定时任务也是一个常见的需求Spring Boot 中对于定时任务的支持主要还是来自 Spring 框架。
在 Spring Boot 中使用定时任务主要有两种不同的方式一个就是使用 Spring 中的 Scheduled 注解另一个则是使用第三方框架 Quartz。
使用 Spring 中的 Scheduled 的方式主要通过 Scheduled 注解来实现。
使用 Quartz 则按照 Quartz 的方式定义 Job 和 Trigger 即可。
六、 springMVC
什么是Spring MVC
Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架通过把模型-视图-控制器分离将web层进行职责解耦把复杂的web应用分成逻辑清晰的几部分简化开发减少出错方便组内开发人员之间的配合。
Spring MVC的优点 1可以支持各种视图技术,而不仅仅局限于JSP
2与Spring框架集成如IoC容器、AOP等
3清晰的角色分配前端控制器(dispatcherServlet) , 请求到处理器映射handlerMapping), 处理器适配器HandlerAdapter), 视图解析器ViewResolver。
4 支持各种请求资源的映射策略。
核心组件
Spring MVC的主要组件
1前端控制器 DispatcherServlet不需要程序员开发
作用接收请求、响应结果相当于转发器有了DispatcherServlet 就减少了其它组件之间的耦合度。
2处理器映射器HandlerMapping不需要程序员开发
作用根据请求的URL来查找Handler
3处理器适配器HandlerAdapter
注意在编写Handler的时候要按照HandlerAdapter要求的规则去编写这样适配器HandlerAdapter才可以正确的去执行Handler。
4处理器Handler需要程序员开发
5视图解析器 ViewResolver不需要程序员开发
作用进行视图的解析根据视图逻辑名解析成真正的视图view
6视图View需要程序员开发jsp
View是一个接口 它的实现类支持不同的视图类型jspfreemarkerpdf等等
什么是DispatcherServlet Spring的MVC框架是围绕DispatcherServlet来设计的它用来处理所有的HTTP请求和响应。
什么是Spring MVC框架的控制器 控制器提供一个访问应用程序的行为此行为通常通过服务接口实现。控制器解析用户输入并将其转换为一个由视图呈现给用户的模型。Spring用一个非常抽象的方式实现了一个控制层允许用户创建多种用途的控制器。
Spring MVC的控制器是不是单例模式,如果是,有什么问题,怎么解决 答是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性能的,解决方案是在控制器里面不能写字段。
springMVC工作原理/流程
请描述Spring MVC的工作流程描述一下 DispatcherServlet 的工作流程 1用户发送请求至前端控制器DispatcherServlet 2 DispatcherServlet收到请求后调用HandlerMapping处理器映射器请求获取Handle 3处理器映射器根据请求url找到具体的处理器生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet 4DispatcherServlet 调用 HandlerAdapter处理器适配器 5HandlerAdapter 经过适配调用 具体处理器(Handler也叫后端控制器) 6Handler执行完成返回ModelAndView 7HandlerAdapter将Handler执行结果ModelAndView返回给DispatcherServlet 8DispatcherServlet将ModelAndView传给ViewResolver视图解析器进行解析 9ViewResolver解析后返回具体View 10DispatcherServlet对View进行渲染视图即将模型数据填充至视图中 11DispatcherServlet响应用户。
MVC框架
MVC是什么MVC设计模式的好处有哪些 mvc是一种设计模式设计模式就是日常开发中编写代码的一种好的方法和经验的总结。模型model-视图view-控制器controller三层架构的设计模式。用于实现前端页面的展现与后端业务数据处理的分离。
mvc设计模式的好处
1.分层设计实现了业务系统各个组件之间的解耦有利于业务系统的可扩展性可维护性。
2.有利于系统的并行开发提升开发效率。
注解原理是什么
注解本质是一个继承了Annotation的特殊接口其具体实现类是Java运行时生成的动态代理类。我们通过反射获取注解时返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池。
Spring MVC常用的注解有哪些
RequestMapping用于处理请求 url 映射的注解可用于类或方法上。用于类上则表示类中的所有响应请求的方法都是以该地址作为父路径。
RequestBody注解实现接收http请求的json数据将json转换为java对象。
ResponseBody注解实现将conreoller方法返回对象转化为json对象响应给客户。
SpingMvc中的控制器的注解一般用哪个,有没有别的注解可以替代 答一般用Controller注解,也可以使用RestController,RestController注解相当于ResponseBody Controller,表示是表现层,除此之外一般不用别的注解代替。
Controller注解的作用 在Spring MVC 中控制器Controller 负责处理由DispatcherServlet 分发的请求它把用户请求的数据经过业务处理层处理之后封装成一个Model 然后再把该Model 返回给对应的View 进行展示。在Spring MVC 中提供了一个非常简便的定义Controller 的方法我们无需继承特定的类或实现特定的接口只需使用**Controller 标记一个类是Controller 然后使用RequestMapping 和RequestParam** 等一些注解用以定义URL 请求和Controller 方法之间的映射这样的Controller 就能被外界访问到。此外Controller 不会直接依赖于HttpServletRequest 和HttpServletResponse 等HttpServlet 对象它们可以通过Controller 的方法参数灵活的获取到。
Controller 用于标记在一个类上使用它标记的类就是一个Spring MVC Controller 对象。分发处理器将会扫描使用了该注解的类的方法并检测该方法是否使用了RequestMapping 注解。Controller 只是定义了一个控制器类而使用RequestMapping 注解的方法才是真正处理请求的处理器。单单使用Controller 标记在一个类上还不能真正意义上的说它就是Spring MVC 的一个控制器类因为这个时候Spring 还不认识它。那么要如何做Spring 才能认识它呢这个时候就需要我们把这个控制器类交给Spring 来管理。有两种方式
在Spring MVC 的配置文件中定义MyController 的bean 对象。 在Spring MVC 的配置文件中告诉Spring 该到哪里去找标记为Controller 的Controller 控制器。 RequestMapping注解的作用 RequestMapping是一个用来处理请求地址映射的注解可用于类或方法上。用于类上表示类中的所有响应请求的方法都是以该地址作为父路径。
RequestMapping注解有六个属性下面我们把她分成三类进行说明下面有相应示例。
value method
value 指定请求的实际地址指定的地址可以是URI Template 模式后面将会说明
method 指定请求的method类型 GET、POST、PUT、DELETE等
consumesproduces
consumes 指定处理请求的提交内容类型Content-Type例如application/json, text/html;
produces: 指定返回的内容类型仅当request请求头中的(Accept)类型中包含该指定类型才返回
paramsheaders
params 指定request中必须包含某些参数值是才让该方法处理。
headers 指定request中必须包含某些指定的header值才能让该方法处理请求。
ResponseBody注解的作用 作用 该注解用于将Controller的方法返回的对象通过适当的HttpMessageConverter转换为指定格式后写入到Response对象的body数据区。
使用时机返回的数据不是html标签的页面而是其他某种格式的数据时如json、xml等使用
PathVariable和RequestParam的区别 请求路径上有个id的变量值可以通过PathVariable来获取 RequestMapping(value “/page/{id}”, method RequestMethod.GET)
RequestParam用来获得静态的URL请求入参 spring注解时action里用到。
Spring Cloud注册中心配置中心原理详解
谈谈你对 spring Cloud 的理解
spring Cloud 是一套分布式微服务的技术解决方案它提供了快速构建分布式系统常用的一些组件比如说配置管理、服务的注册与发现、服务调用的负载均衡、资源隔离、熔断降级等等。
不过 Spring Cloud 只是 Spring 官方提供的一套微服务标准定义
而真正的实现目前有两套体系用的比较多一个是 Spring Cloud Netflix 另一个是 Spring Cloud Alibaba
Spring Cloud Netflix 是基于 Netflix 这个公司的开源组件集成的一套微服务解决方案其中的组件有:
**1. Eureka——服务注册与发现 ** 2. Feign——服务调用
**3. Ribbon——负载均衡 **
4. Hystrix——服务熔断
5. Zuul——网关;
Spring Cloud Alibaba 是基于阿里巴巴开源组件集成的一套微服务解决方案其中包括:
Dubbo——消息通讯Nacos——服务注册与发现Seata——事务隔离Sentinel——熔断降级
有了 Spring Cloud 这样的技术生态使得我们在落地微服务架构时不用去考虑第三方技术集成带来额外成本只要通过配置组件来完成架构下的技术问题即可从而可以让我们更加侧重性能方面 以上这些就是我个人对 Spring Cloud 的理解
注册中心
参考地址 微服务二——注册中心Eureka、Nacos_微服务注册中心_364.99°的博客-CSDN博客
需求当一个服务提供者 Service 部署了多个实例交给 User 远程调用时 服务消费者 User 应该调用哪个实例如何获取其对应地址和端口User 如何获知实例是否健康
注册中心作用
帮助管理服务并帮助服务调用者选择并调用服务实时监测服务实例是否健康
Eureka
构成 eureka-server服务端注册中心 记录服务信息心跳监控
eureka-client客户端 服务提供者注册到服务端定期向服务端发送心跳、服务消费者从服务端拉取服务列表基于负载均衡选择服务 作用 服务注册 Service实例启动后会将自己的信息注册到 eureka服务端 服务拉取 User 根据 实例名 获取 Service 地址列表
服务搭建
配置文件:
我们需要将 eureka 注册到spring容器中所以需要在配置文件中做相关配置。
server:port: 8099
spring:application:name: eureka_server
eureka:client:# 配置eureka服务地址service-url:defaultZone: http://127.0.0.1:8099/eureka
项目启动
为了让项目能启动 eureka需要在启动类上加一个注解EnableEurekaServer
SpringBootApplication
EnableEurekaServer
public class ServerApplication {public static void main(String[] args) {SpringApplication.run(ServerApplication.class, args);}
}启动项目访问地址http://127.0.0.1:8099 注册的服务名称就是配置文件中的名称的大写。
服务注册
在上一步已经将 eureka-server eureka服务中心搭建完毕现在就开始注册服务实例了。
注意
无论是 服务提供者 还是 服务消费者他们的身份都是 eureka-client
记得添加 spring-boot-starter-web 的依赖不然会报错Field optionalArgs in org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration$RefreshableEurekaClientConfiguration required a bean of type ‘com.netflix.discovery.AbstractDiscoveryClientOptionalArgs’ that could not be found.
服务发现、服务注册统一都封装在eureka-client依赖。
需要在配置文件中配置 eureka-server 的地址。
spring:application:name: service_provider
eureka:client:service-url:defaultZone: http://127.0.0.1:8099/eureka
启动多个实例
为了让项目能启动 eureka需要在启动类上加一个注解EnableEurekaClient。
SpringBootApplication
EnableEurekaClient
public class ProviderApplication {public static void main(String[] args) {SpringApplication.run(ProviderApplication.class, args);}
}我们可以通过 IDEA 自带功能模仿启动多个服务实例。
打开 Service 面板
****
复制原来的 provider 启动配置 查看 eureka注册中心 服务发现
依赖导入
dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-netflix-eureka-client/artifactId
/dependency配置文件
spring:application:name: service_user
server:port: 8084eureka:client:service-url:defaultZone: http://127.0.0.1:8099/eureka服务拉取和负载均衡
服务拉取 修改 controller 代码将 url 路径的 ip、端口 修改为 服务名 RestController
RequestMapping(user)
public class UserController {Autowiredprivate RestTemplate restTemplate;GetMapping(/{id})public Book getBookById(PathVariable(id) Integer id) {// String url http://127.0.0.1:8081/provider id;String url http://service_provider/provider;if (id ! null) {url url id;}Book book restTemplate.getForObject(url, Book.class);return book;}
}注册 RestTemplate 的时候加上注解 LoadBalanced LoadBalancedBeanpublic RestTemplate restTemplate() {return new RestTemplate();}接口调用
RestController
RequestMapping(user)
public class UserController {Autowiredprivate RestTemplate restTemplate;GetMapping(/{id})public Book getBookById(PathVariable(id) Integer id) {// String url http://127.0.0.1:8081/provider id;String url http://PROVIDER/pro/;if (id ! null) {url url id;}Book book restTemplate.getForObject(url, Book.class);return book;}
}访问接口 访问报错 解决方案 多测试几下接口可以发现user一会儿调用的是 provider:8081 一会儿调用的是 provider:8082。这就是负载均衡算法选择的。
小结
eureka-server 搭建
引入依赖
dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-netflix-eureka-server/artifactId
/dependency
启动类添加 EnableEurekaServer配置文件配置 eureka 地址
server:port: 8099
spring:application:name: server
eureka:client:# 配置eureka服务地址service-url:defaultZone: http://127.0.0.1:8099/eureka服务注册
引入依赖
dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-netflix-eureka-client/artifactId
/dependency启动类添加 EnableEurekaClient配置文件配置 eureka 地址
server:port: 8081
spring:application:name: provider
eureka:client:# 配置eureka服务地址service-url:defaultZone: http://127.0.0.1:8099/eureka服务发现
引入依赖
dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-netflix-eureka-client/artifactId
/dependency启动类添加 EnableEurekaClient配置文件配置 eureka 地址
server:port: 8089
spring:application:name: user
eureka:client:# 配置eureka服务地址service-url:defaultZone: http://127.0.0.1:8099/eurekaRibbo - 负载均衡
1. 负载均衡流程
Ribbon负载均衡流程图 http://PROVIDER/pro/4 并非真实的地址这个需要Ribbon负载均衡去拦截然后选择具体的服务地址。而Ribbon就是通过 LoadBalancerInterceptor 的 intercept 方法来实现拦截请求并解析选择地址。 public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {URI originalUri request.getURI();String serviceName originalUri.getHost();Assert.state(serviceName ! null, Request URI does not contain a valid hostname: originalUri);return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));}负载均衡流程 2. 负载均衡策略
Ribbon的负载均衡策略是由 IRule 接口来定义的。 自定义负载均衡策略
方式一 在user中Bean 注入自定义 IRule
Bean
public IRule randomRule(){return new RandomRule();
}方式二 在user配置文件修改 IRule
provider: # 给某个微服务配置负载均衡规则这里是userservice服务ribbon:NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则注意
方式一的作用范围是在访问任何微服务的时候都是使用 RandomRule 负载均衡策略
方式二的作用范围是在访问 provider 微服务的时候才是采用 RandomRule 策略其他的还是使用默认策略。
加载策略
Ribbon默认采用懒加载即会在第一次访问时才会去创建 LoadBalanceClient 所以会在第一次请求的时候花上较长的等待时间。
可以通过配置文件更改加载策略为饿加载策略即初始化时就创建 LoadBalanceClient 降低第一次访问的耗时。
ribbon:eager-load:enabled: true # 开启饥饿加载clients: provider # 指定饥饿加载的服务名称nacos
概念 服务注册中心 作用 心跳检查 不同于 eureka 只能 Service实例 主动发起心跳nacos 对于非临时实例可以主动发起心跳检查 临时心跳检查异常的会被剔除出 服务地址列表
非临时心跳检查异常的不会被剔除定时推送变更 nacos 支持服务列表变更的消息推送模式服务列表更新更及时
注册中心
引入依赖、修改配置
父工程添加 SpringCloudAlibaba 的管理依赖。
!--spring-cloud-alibaba--
dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-alibaba-dependencies/artifactIdversion2.2.6.RELEASE/versiontypepom/typescopeimport/scope
/dependency子工程中注释掉 eureka 依赖引入 nacos 依赖。
dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId
/dependency配置文件配置nacos地址
spring:cloud:nacos:server-addr: localhost:8848 # nacos服务地址启动 user 和 provider 注意 eureka 注册名会变成大写nacos 不会所以改成 nacos 之后需要把访问地址改成小写。
请求测试 服务分级存储模型 一个服务可以拥有多个实例如provider 的 8081 和 8082如果这些实例分布于全国不同的机房如provider:8081 在成都机房、provider:8082 在重庆机房Nacos就将同一机房内的实例 划分为一个集群。
即一个服务可以拥有多个集群一个集群中可以拥有多个实例。 微服务相互之间访问时访问本地的速度更快所以应该尽可能访问相同集群的实例只有当本集群内存不够时才去访问其他集群。
1. 配置集群
配置服务集群
spring:cloud:nacos:server-addr: localhost:8848 # nacos服务地址discovery:cluster-name: CDVM Options
-Dserver.port8082 -Dspring.cloud.nacos.discovery.cluster-nameCQ2. 同集群优先的负载均衡
默认的 ZoneAvoidanceRule 负载均衡策略并不能实现据同集群优先来实现负载均衡因此Nacos中提供了一个NacosRule的实现可以优先从同集群中挑选实例。
修改负载均衡策略
方式一
Bean
public IRule nacosRule(){return new NacosRule();
}方式二
provider: # 访问的服务名ribbon:NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则3. 权重配置
服务器的性能之间存在差异为了让性能能好的服务器承担更多的用户请求Nacos提供了权重配置来控制访问率权重越大、访问率越高。 注意 权重为0的服务永远不会被访问。
4. 环境隔离
Nacos提供了namespace来实现环境隔离功能。
nacos中可以有多个namespacenamespace下可以有group、service等不同namespace之间相互隔离不同namespace的服务互相不可见 1. 创建 namespace
Nacos默认的namespace是public
命名空间 Nacos创建命名空间流程 2. 配置命名空间
spring:cloud:nacos:discovery:namespace: e11eb3bc-8eed-4cdd-93f0-7a6c01b85eb4 # 命名空间填IDspring:cloud:nacos:discovery:ephemeral: false # 设置为永久实例3. 配置管理
Nacos不仅可以担任微服务的注册中心还可以担任配置管理。
统一配置管理
可以使用统一配置管理来处理因为部署的微服务数量过多配置繁杂等问题。 Nacos一方面可以将配置集中管理另一方可以在配置变更时及时通知微服务实现配置的热更新。
注意 只把那些需要热更新的配置文件交给Nacos
1. nacos添加配置文件 Data ID 配置文件id服务名称-profile.后缀名 。
2. 从nacos拉取配置
spring引入了一种新的配置文件bootstrap.yaml文件会在application.yml之前被读取流程如下 引入nacos-config依赖
!--nacos配置管理依赖--
dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-config/artifactId
/dependency添加bootstrap.yml
spring:application:name: user # 服务名称profiles:active: dev #开发环境这里是devcloud:nacos:server-addr: localhost:8848 # Nacos地址config:file-extension: yaml # 文件后缀名这里会根据spring.cloud.nacos.server-addr获取nacos地址再根据 s p r i n g . a p p l i c a t i o n . n a m e − {spring.application.name}- spring.application.name−{spring.profiles.active}.${spring.cloud.nacos.config.file-extension}作为文件id来读取配置。 读取nacos配置 Value(${pattern.dateformat})private String dateformat;GetMapping(now)public String now() {return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));}启动报错 org.springframework.beans.factory.BeanCreationException: Error creating bean with name xxx
解决方案
检查 namespace配置文件与服务要在同一个 namespace 中 名称是否拼错 升级 nacos 版本 Value 切换为 NacosValue
配置热更新 配置热更新 修改nacos中的配置后微服务中无需重启即可让配置生效。
实现方式
方式一 在Value注入的变量所在类上添加注解 RefreshScope 方式二 使用 ConfigurationProperties 注解代替Value注解 在 user 服务中添加一个类读取patterrn.dateformat属性
Component
Data
ConfigurationProperties(prefix pattern)
public class PatternProperties {private String dateformat;
}在UserController中使用这个类代替Value:
Autowired
private PatternProperties patternProperties;GetMapping(now)
public String now() {return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat));
}配置共享
其实微服务启动时会去nacos读取多个配置文件因为nacos管理的配置文件不包含环境信息可以被多个环境共享。
只需要取名的时候不加上 profile 就能被共享如下 配置共享时的优先级 配置中心
链接【精选】微服务-配置中心_火恐龙的博客-CSDN博客
Nacos
微服务-配置中心
微服务架构中存在很多服务每个服务都有自己的配置文件这些配置文件如果集成在服务中也是难以维护的于是便可以使用配置中心来管理这些配置文件。
nacos既可以作为 注册中心 也可以作为 配置中心 使用nacos配置中心你可以将服务里的配置文件写在nacos上通过nacos面板来修改管理服务的配置而无需停止服务。如果使用nacos作为注册中心而不作为配置中心添加配置spring.cloud.nacos.config.enabled false 即可。可以以请求方式发布、获取配置发布配置posthttp://127.0.0.1:8848/nacos/v1/cs/configs?dataIdnacos.cfg.dataIdgrouptestcontentHelloWorld
获取配置gethttp://127.0.0.1:8848/nacos/v1/cs/configs?dataIdnacos.cfg.dataIdgrouptest
1.搭建服务端
nacos服务端搭建是十分容易的只需下载配置启动就行参考注册中心这篇点击前往
建议使用mysql作为持久化。
2.搭建客户端
客户端即每个配置要被管理的服务服务的搭建还如往常一样搭建只不过配置文件发生一些变化以下就认为你已经创建了一个服务模块/项目并以此起步。
1.客户端引入nacos config依赖
dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-config/artifactId
/dependency2.创建一个bootstrap.yml(或properties)配置nacos config的配置 :
spring:cloud:nacos:config: #nacos的信息添加后可以省略注册中心的nacos此类信息server-addr: 127.0.0.1:8848username: nacos password: nacoscontextPath: /nacos#--------------------------file-extension: yaml #显示的声明dataId文件扩展名3.application.yml(或properties)除特殊配置外只留下下面2个配置
server:port: 41150
spring:application:name: server-producer到这里你会发现你有2个配置文件 bootstrap.yml、application.yml 。这时你可能就有疑问了不是说好的将配置交给配置中心管理了吗怎么服务里还变多了呢。
那么我来给你解释一下吧
bootsteap.yml这里只配置配置中心的信息服务通过它去获取服务所需配置而且这里的内容基本不变。 application.yml这里只配置端口服务名因为首次启动服务时需要通过它注册达到nacos然后才能获取配置。 可以看出上面的配置基本上就是固定的而且基本上不会被修改实际项目中配置文件远远不止这些其他配置都将被放在配置中心统一管理。
4.在nacos配置中心添加配置文件 点击添加后页面如下配置名要和服务名一致,后缀带不带都行。 第一次发布配置后重启项目即可。之后修改配置都不需要启动服务。
服务与配置文件的匹配
文件名服务名建议显示指定配置文件后缀。 多同名配置会互相覆盖比如server、server.yml在指定yml格式后两个文件会覆盖。 更多特性自己摸索使用服务名.yml 并 在bootstrap.yml指定格式为yaml 一定能用。
测试一下效果按照阿里的示例进行举例。
在客户端创建一个测试类并在nacos上配置中文件添加属性然后注入到属性中启动后控制台就会打印。
Configuration
public class TestConfig {Value(${user.names})public String test1;Value(${user.pwds})public String test2;PostConstructpublic void getUser(){System.out.println(name test1 |pwd test2);}
}启动后会发现打印的数据就是配置中心上的数据了。
3.动态刷新 此节代码来自于阿里云-知行动手实验室 配置发生变化后服务中绑定的属性值将刷新会刷新的情况有3种
类上加注解RefreshScope、类种非静态属性加Value注解
RestController
RefreshScope
public class NacosConfigDemo {Value(${user.name})private String userName;Value(${user.age})private int userAge;PostConstructpublic void init() {System.out.printf([init] user name : %s , age : %d%n, userName, userAge);}RequestMapping(/user)public String user() {return String.format([HTTP] user name : %s , age : %d, userName, userAge);}
}类上加注解RefreshScope、ConfigurationProperties
RefreshScope
ConfigurationProperties(prefix user)
public class User {private String name;private int age;public String getName() {return name;}public void setName(String name) {this.name name;}public int getAge() {return age;}public void setAge(int age) {this.age age;}Overridepublic String toString() {return User{ name name \ , age age };}
}使用Nacos Config监听来对Bean属性实施监听。
RestController
RefreshScope
EnableConfigurationProperties(User.class)
public class NacosConfigDemo {Value(${user.name})private String userName;Value(${user.age})private int userAge;PostConstructpublic void init() {System.out.printf([init] user name : %s , age : %d%n, userName, userAge);}PreDestroypublic void destroy() {System.out.printf([destroy] user name : %s , age : %d%n, userName, userAge);}RequestMapping(/user)public String user() {return String.format([HTTP] user name : %s , age : %d, userName, userAge);}
}4.nacos config高可用
nacos是配置中心也是注册中心实现nacos的高可用和 注册中心 高可用一样。
5.nacos config高级配置
namespace命名空间 nacos可以实现配置文件之间隔离不同的命名空间下可以存在相同的 Group 或 Data ID 的配置可以实现不同开发环境开发、测试、生产等配置文件的隔离。
创建命名空间也很容易 切换命名空间 创建后在客户端bootstrap.yml添加如下配置进行绑定
spring:cloud:nacos:config:namespace: c70a9f91-a687-427f-97bb-5a6e54289a6eGroup分组
在创建配置时可以指定分组自定义分组在bootstrap.yml添加如下
spring:cloud:nacos:config:group: DEVELOP_GROUP自定义配置名
三种情况完整配置案例
spring:application:name: myservercloud:nacos:config:server-addr: 127.0.0.1:8848extension-configs[0]: # 1.在默认组,不支持动态刷新data-id: myconfig-1.yamlextension-configs[1]: # 2.不在默认组不支持动态刷新data-id: myconfig-2.yamlgroup: MY_GROUPextension-configs[2]: # 3.不在默认的组支持动态刷新data-id: myconfig-3.yamlgroup: MY_GROUPrefresh: true总结默认配置名默认组时才会自动刷新否则需指定refresh: true才自动刷新。 注意
多个相同配置文件存在优先级extension-configs[a]里a越大优先级越高。data-id结尾必须有properties/yaml/yml等扩展名。
Config
Apollo
Spring Cloud是目前微服务架构领域的翘楚无数的书籍博客都在讲解这个技术。不过大多数讲解还停留在对Spring Cloud功能使用的层面其底层的很多原理很多人可能并不知晓。因此本文将通过大量的手绘图给大家谈谈Spring Cloud微服务架构的底层原理。
实际上Spring Cloud 是一个全家桶式的技术栈包含了很多组件。本文先从其最核心的几个组件入手来剖析一下其底层的工作原理。也就是 Eureka、Feign、Ribbon、Hystrix、Zuul 这几个组件。 EFRHZ
一、业务场景介绍
先来给大家说一个业务场景假设咱们现在开发一个电商网站要实现支付订单的功能流程如下
创建一个订单后如果用户立刻支付了这个订单我们需要将订单状态更新为“已支付”扣减相应的商品库存通知仓储中心进行发货给用户的这次购物增加相应的积分
针对上述流程我们需要有: 订单服务、库存服务、仓储服务、积分服务。整个流程的大体思路如下
用户针对一个订单完成支付之后就会去找订单服务更新订单状态订单服务调用库存服务完成相应功能订单服务调用仓储服务完成相应功能订单服务调用积分服务完成相应功能
至此整个支付订单的业务流程结束
下图这张图清晰表明了各服务间的调用过程 好有了业务场景之后咱们就一起来看看Spring Cloud微服务架构中这几个组件如何相互协作各自发挥的作用以及其背后的原理。
二、Spring Cloud核心组件Eureka
咱们来考虑第一个问题订单服务想要调用库存服务、仓储服务或者积分服务怎么调用
订单服务压根儿就不知道人家库存服务在哪台机器上啊他就算想要发起一个请求都不知道发送给谁有心无力这时候就轮到Spring Cloud Eureka出场了。Eureka是微服务架构中的注册中心专门负责服务的注册与发现。
咱们来看看下面的这张图结合图来仔细剖析一下整个流程 如上图所示库存服务、仓储服务、积分服务中都有一个Eureka Client组件这个组件专门负责将这个服务的信息注册到Eureka Server中。说白了就是告诉Eureka Server自己在哪台机器上监听着哪个端口。而Eureka Server是一个注册中心里面有一个注册表保存了各服务所在的机器和端口号
订单服务里也有一个Eureka Client组件这个Eureka Client组件会找Eureka Server问一下库存服务在哪台机器啊监听着哪个端口啊仓储服务呢积分服务呢然后就可以把这些相关信息从Eureka Server的注册表中拉取到自己本地缓存起来。
这时如果订单服务想要调用库存服务不就可以找自己本地的Eureka Client问一下库存服务在哪台机器监听哪个端口吗收到响应后紧接着就可以发送一个请求过去调用库存服务扣减库存的那个接口同理如果订单服务要调用仓储服务、积分服务也是如法炮制。
总结一下
Eureka Client负责将这个服务的信息注册到Eureka Server中Eureka Server注册中心里面有一个注册表保存了各个服务所在的机器和端口号
三、Spring Cloud核心组件Feign
现在订单服务确实知道库存服务、积分服务、仓库服务在哪里了同时也监听着哪些端口号了。但是新问题又来了难道订单服务要自己写一大堆代码跟其他服务建立网络连接然后构造一个复杂的请求接着发送请求过去最后对返回的响应结果再写一大堆代码来处理吗
这是上述流程翻译的代码片段咱们一起来看看体会一下这种绝望而无助的感受
友情提示前方高能 看完上面那一大段代码有没有感到后背发凉、一身冷汗实际上你进行服务间调用时如果每次都手写代码代码量比上面那段要多至少几倍所以这个事压根儿就不是地球人能干的。
既然如此那怎么办呢别急Feign早已为我们提供好了优雅的解决方案。来看看如果用Feign的话你的订单服务调用库存服务的代码会变成啥样 看完上面的代码什么感觉是不是感觉整个世界都干净了又找到了活下去的勇气没有底层的建立连接、构造请求、解析响应的代码直接就是用注解定义一个 FeignClient 接口然后调用那个接口就可以了。人家Feign Client会在底层根据你的注解跟你指定的服务建立连接、构造请求、发起靕求、获取响应、解析响应等等。这一系列脏活累活人家Feign全给你干了。
那么问题来了Feign是如何做到这么神奇的呢很简单Feign的一个关键机制就是使用了动态代理。咱们一起来看看下面的图结合图来分析
首先如果你对某个接口定义了**FeignClient**注解Feign就会针对这个接口创建一个动态代理接着你要是调用那个接口本质就是会调用 Feign创建的动态代理这是核心中的核心Feign的动态代理会根据你在接口上的**RequestMapping**等注解来动态构造出你要请求的服务的地址最后针对这个地址发起请求、解析响应 四、Spring Cloud核心组件Ribbon
说完了Feign还没完。现在新的问题又来了如果人家库存服务部署在了5台机器上如下所示
192.168.169:9000192.168.170:9000192.168.171:9000192.168.172:9000192.168.173:9000
这下麻烦了人家Feign怎么知道该请求哪台机器呢
这时Spring Cloud Ribbon就派上用场了。Ribbon就是专门解决这个问题的。它的作用是负载均衡会帮你在每次请求时选择一台机器均匀的把请求分发到各个机器上Ribbon 的负载均衡默认使用的最经典的Round Robin轮询算法。这是啥简单来说就是如果订单服务对库存服务发起10次请求那就先让你请求第1台机器、然后是第2台机器、第3台机器、第4台机器、第5台机器接着再来—个循环第1台机器、第2台机器。。。以此类推。
此外Ribbon是和Feign以及Eureka紧密协作完成工作的具体如下
首先Ribbon会从 Eureka Client里获取到对应的服务注册表也就知道了所有的服务都部署在了哪些机器上在监听哪些端口号。然后Ribbon就可以使用默认的Round Robin算法从中选择一台机器Feign就会针对这台机器构造并发起请求。
对上述整个过程再来一张图帮助大家更深刻的理解 五、Spring Cloud核心组件Hystrix
在微服务架构里一个系统会有很多的服务。以本文的业务场景为例订单服务在一个业务流程里需要调用三个服务。现在假设订单服务自己最多只有100个线程可以处理请求然后呢积分服务不幸的挂了每次订单服务调用积分服务的时候都会卡住几秒钟然后抛出—个超时异常。
咱们一起来分析一下这样会导致什么问题
如果系统处于高并发的场景下大量请求涌过来的时候订单服务的100个线程都会卡在请求积分服务这块。导致订单服务没有一个线程可以处理请求然后就会导致别人请求订单服务的时候发现订单服务也挂了不响应任何请求了
上面这个就是微服务架构中恐怖的服务雪崩问题如下图所示 如上图这么多服务互相调用要是不做任何保护的话某一个服务挂了就会引起连锁反应导致别的服务也挂。比如积分服务挂了会导致订单服务的线程全部卡在请求积分服务这里没有一个线程可以工作瞬间导致订单服务也挂了别人请求订单服务全部会卡住无法响应。
但是我们思考一下就算积分服务挂了订单服务也可以不用挂啊为什么
我们结合业务来看支付订单的时候只要把库存扣减了然后通知仓库发货就OK了如果积分服务挂了大不了等他恢复之后慢慢人肉手工恢复数据为啥一定要因为一个积分服务挂了就直接导致订单服务也挂了呢不可以接受
现在问题分析完了如何解决
这时就轮到Hystrix闪亮登场了。Hystrix是隔离、熔断以及降级的一个框架。啥意思呢说白了Hystrix会搞很多个小小的线程池比如订单服务请求库存服务是一个线程池请求仓储服务是一个线程池请求积分服务是一个线程池。每个线程池里的线程就仅仅用于请求那个服务。
打个比方现在很不幸积分服务挂了会咋样
当然会导致订单服务里那个用来调用积分服务的线程都卡死不能工作了啊但由于订单服务调用库存服务、仓储服务的这两个线程池都是正常工作的所以这两个服务不会受到任何影响。
这个时候如果别人请求订单服务订单服务还是可以正常调用库存服务扣减库存调用仓储服务通知发货。只不过调用积分服务的时候每次都会报错。**但是如果积分服务都挂了每次调用都要去卡住几秒钟干啥呢有意义吗当然没有**所以我们直接对积分服务熔断不就得了比如在5分钟内请求积分服务直接就返回了不要去走网络请求卡住几秒钟这个过程就是所谓的熔断
**那人家又说兄弟积分服务挂了你就熔断好歹你干点儿什么啊别啥都不干就直接返回啊**没问题咱们就来个降级每次调用积分服务你就在数据库里记录一条消息说给某某用户增加了多少积分因为积分服务挂了导致没增加成功这样等积分服务恢复了你可以根据这些记录手工加一下积分。这个过程就是所谓的降级。
为帮助大家更直观的理解接下来用一张图梳理一下Hystrix隔离、熔断和降级的全流程 六、Spring Cloud核心组件Zuul
说完了Hystrix接着给大家说说最后一个组件Zuul也就是微服务网关。**这个组件是负责网络路由的。**不懂网络路由行那我给你说说如果没有Zuul的日常工作会怎样
假设你后台部署了几百个服务现在有个前端兄弟人家请求是直接从浏览器那儿发过来的。打个比方人家要请求一下库存服务你难道还让人家记着这服务的名字叫做inventory-service部署在5台机器上就算人家肯记住这一个你后台可有几百个服务的名称和地址呢难不成人家请求一个就得记住一个你要这样玩儿那真是友谊的小船说翻就翻
上面这种情况压根儿是不现实的。所以一般微服务架构中都必然会设计一个网关在里面像android、ios、pc前端、微信小程序、H5等等不用去关心后端有几百个服务就知道有一个网关所有请求都往网关走网关会根据请求中的一些特征将请求转发给后端的各个服务。
而且有一个网关之后还有很多好处比如可以做统一的降级、限流、认证授权、安全等等。
七、总结使用及配置
最后再来总结一下上述几个Spring Cloud核心组件在微服务架构中分别扮演的角色 Eureka各个服务启动时Eureka Client 都会将服务注册到 Eureka Server并且 Eureka Client 还可以反过来从 Eureka Server 拉取注册表从而知道其他服务在哪里 Feign基于Feign的动态代理机制根据注解和选择的机器拼接请求URL地址发起请求 Ribbon服务间发起请求的时候基于Ribbon做负载均衡从一个服务的多台机器中选择一台 Hystrix发起请求是通过 Hystrix 的线程池来走的不同的服务走不同的线程池实现了不同服务调用的隔离避免了服务雪崩的问题 Zuul如果前端、移动端要调用后端系统统一从 Zuul 网关进入由Zuul网关转发请求给对应的服务 以上就是我们通过一个电商业务场景阐述了 Spring Cloud 微服务架构几个核心组件的底层原理。 文字总结还不够直观没问题我们将Spring Cloud的5个核心组件通过一张图串联起来再来直观的感受一下其底层的架构原理
Eureka
一、erueka 客户端配置 1、Eureka 启禁用
eureka.client.enabledtrue
2、Eureka 连接超时时间
eureka.client.eureka-server-connect-timeout-seconds5
eureka.client.eureka-connection-idle-timeout-seconds30
3、Eureka Server等待超时时间
eureka.client.eureka-server-read-timeout-seconds8
4、拉取Eureka注册信息
eureka.client.fetch-registrytrue
5、注册自身到Eureka
eureka.client.register-with-eurekatrue
6、过滤状态存活的实例
eureka.client.filter-only-up-instancestrue
7、注册实例的名称
eureka.instance.appnameunknown
8、健康检查相对路径
eureka.instance.health-check-url-path/health
9、HOME页面相对路径
eureka.instance.home-page-url-path/
10、服务续约到期时间
eureka.instance.lease-expiration-duration-in-seconds90
11、服务续约间隔时间
eureka.instance.lease-renewal-interval-in-seconds30
12、注册元数据
eureka.instance.metadata-map
13、通过配置文件找到namespace忽略springcloud的配置
eureka.instance.namespace
14、注册是否显示IP地址第一个非回环地址
eureka.instance.prefer-ip-addressfalse
15、注册时使用的IP地址
eureka.instance.ip-address
16、压缩Eureka Server的数据
eureka.client.g-zip-contenttrue
17、心跳执行器的线程池初始值
eureka.client.heartbeat-executor-thread-pool-size2
18、最初复制实例信息到Eureka服务器所需的时间
eureka.client.initial-instance-info-replication-interval-seconds40
19、同步实例变更信息到Eureka服务到周期
eureka.client.instance-info-replication-interval-seconds30
20、从Eureka服务器拉取服务信息周期
eureka.client.registry-fetch-interval-seconds30
21、Eureka Server地址
eureka.client.serviceUrl.defaultZonexxx,xxx,xxx
二、eureka 服务端配置 1、注册实例
eureka.instance.registry.default-open-for-traffic-count1
eureka.instance.registry.expected-number-of-renews-per-min1
2、服务面板
eureka.dashborad.enabledtrue
eureka.dashboard.path/
3、自我保护
eureka.server.enable-self-preservation: true eureka.server.eviction-interval-timer-in-ms: 60000
eureka.server.renewal-percent-threshold: 0.85
4、限流
eureka.server.rate-limiter-enabled: false eureka.server.rate-limiter-throttle-standard-clients: false eureka.server.rate-limiter-burst-size: 10 eureka.server.rate-limiter-full-fetch-average-rate: 100
eureka.server.rate-limiter-registry-fetch-average-rate: 500
5、相应缓存
eureka.server.response-cache-auto-expiration-in-seconds: 180 eureka.server.response-cache-update-interval-ms: 30000
eureka.server.use-read-only-response-cache: true
6、安全校验
#启用安全校验
security.basic.enabledtrue #授权用户名 security.user.nameroot #授权密码
security.user.password123456
三、高可用配置 1、节点数据同步
eureka.server.batch-replication: false
eureka.enable-replicated-request-compression: false
2、同步线程池
eureka.server.min-threads-for-status-replication: 1 eureka.server.max-threads-for-status-replication: 1 eureka.server.max-idle-thread-in-minutes-age-for-status-replication: 10 eureka.server.min-threads-for-peer-replication: 5 eureka.server.max-threads-for-peer-replication: 20 eureka.server.max-idle-thread-age-in-minutes-for-peer-replication: 15
eureka.server.number-of-replication-retries: 5
3、定时任务
eureka.server.peer-eureka-status-refresh-time-interval-ms: 3000 eureka.server.peer-eureka-nodes-update-interval-ms: 600000
eureka.server.min-available-instances-for-peer-replication: -1
4、连接
eureka.server.peer-node-total-connections-per-host: 500 eureka.server.peer-node-total-connections: 1000 eureka.server.peer-node-read-timeout-ms: 200 eureka.server.peer-node-connect-timeout-ms: 200
eureka.server.peer-node-connection-idle-timeout-seconds: 30
5、重置注册
eureka.server.registry-sync-retries: 0 eureka.server.registry-sync-retry-wait-ms: 30000
Feign
使用及配置url: SpringCloudFeign的使用及配置_feignclient配置_Poetry-Distance的博客-CSDN博客 Ribbon
配置及使用的链接 Ribbon介绍 配置文件配置篇_配置文件中ribbon.listofservers的作用-CSDN博客
Hystrix
使用及配置的url: Hystrix使用及其配置详解_hystrix配置_李子捌的博客-CSDN博客 RestController
RequestMapping(/circuitBreaker)
public class CircuitBreakConfigController {GetMappingHystrixCommand(fallbackMethod fallback,commandProperties {HystrixProperty(name execution.isolation.thread.timeoutInMilliseconds,value 1000),HystrixProperty(name circuitBreaker.requestVolumeThreshold,value 4),HystrixProperty(name circuitBreaker.errorThresholdPercentage,value 50),HystrixProperty(name metrics.rollingStats.timeInMilliseconds,value 60000),HystrixProperty(name circuitBreaker.sleepWindowInMilliseconds,value 60000)})public ResponseEntityString demo() {try {// 模拟接口请求TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}return new ResponseEntity(Hello!, HttpStatus.OK);}private ResponseEntityString fallback() {return new ResponseEntity(Timeout!, HttpStatus.OK);}
}Zuul
七、REDIS 什么是Redis
Redis(Remote Dictionary Server) 是一个使用 C 语言编写的开源的BSD许可高性能非关系型NoSQL的键值对数据库。
Redis 可以存储键和五种不同类型的值之间的映射。键的类型只能为字符串值支持五种数据类型字符串、列表、集合、散列表、有序集合。
与传统数据库不同的是 Redis 的数据是存在内存中的所以读写速度非常快因此 redis 被广泛应用于缓存方向每秒可以处理超过 10万次读写操作是已知性能最快的Key-Value DB。另外Redis 也经常用来做分布式锁。除此之外Redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。
Redis有5中数据类型 SSHLZ
String (字符串)Set (集合)Hash (散列)List (列表)Zset 有序集合
redis 持久化 —— RDBRedis DataBase和 AOFAppend Only File
关于Redis目前都是使用Redis作为数据缓存缓存的目标主要是那些需要经常访问的数据或计算复杂而耗时的数据。缓存的效果就是减少了数据库读的次数减少了复杂数据的计算次数从而提高了服务器的性能。
一、redis持久化----两种方式
1、redis提供了两种持久化的方式分别是RDBRedis DataBase和AOFAppend Only File。
2、RDB简而言之就是在不同的时间点将redis存储的数据生成快照并存储到磁盘等介质上
3、AOF则是换了一个角度来实现持久化那就是将redis执行过的所有写指令记录下来在下次redis重新启动时只要把这些写指令从前到后再重复执行一遍就可以实现数据恢复了。
4、其实RDB和AOF两种方式也可以同时使用在这种情况下如果redis重启的话则会优先采用AOF方式来进行数据恢复这是因为AOF方式的数据恢复完整度更高。
5、如果你没有数据持久化的需求也完全可以关闭RDB和AOF方式这样的话redis将变成一个纯内存数据库就像memcache一样。
二、redis持久化----RDB
1、RDB方式是将redis某一时刻的数据持久化到磁盘中是一种快照式的持久化方法。
2、redis在进行数据持久化的过程中会先将数据写入到一个临时文件中待持久化过程都结束了才会用这个临时文件替换上次持久化好的文件。正是这种特性让我们可以随时来进行备份因为快照文件总是完整可用的。
3、对于RDB方式redis会单独创建fork一个子进程来进行持久化而主进程是不会进行任何IO操作的这样就确保了redis 极高的性能。
4、如果需要进行大规模数据的恢复且对于数据恢复的完整性不是非常敏感那RDB方式要比AOF方式更加的高效。
5、虽然RDB有不少优点但它的缺点也是不容忽视的。如果你对数据的完整性非常敏感那么RDB方式就不太适合你因为即使你每5分钟都持久化一次当redis故障时仍然会有近5分钟的数据丢失。所以redis还提供了另一种持久化方式那就是AOF。
三、redis持久化----AOF
1、AOF英文是Append Only File即只允许追加不允许改写的文件。
2、如前面介绍的AOF方式是将执行过的写指令记录下来在数据恢复时按照从前到后的顺序再将指令都执行一遍就这么简单。
3、我们通过配置redis.conf中的appendonly yes就可以打开AOF功能。如果有写操作如SET等redis就会被追加到AOF文件的末尾。
4、默认的AOF持久化策略是每秒钟fsync一次fsync是指把缓存中的写指令记录到磁盘中因为在这种情况下redis仍然可以保持很好的处理性能即使redis故障也只会丢失最近1秒钟的数据。
5、如果在追加日志时恰好遇到磁盘空间满、inode满或断电等情况导致日志写入不完整也没有关系redis提供了redis-check-aof工具可以用来进行日志修复。
6、因为采用了追加方式如果不做任何处理的话AOF文件会变得越来越大为此redis提供了AOF文件重写rewrite机制即当AOF文件的大小超过所设定的阈值时redis就会启动AOF文件的内容压缩只保留可以恢复数据的最小指令集。举个例子或许更形象假如我们调用了100次INCR指令在AOF文件中就要存储100条指令但这明显是很低效的完全可以把这100条指令合并成一条SET指令这就是重写机制的原理。
7、在进行AOF重写时仍然是采用先写临时文件全部完成后再替换的流程所以断电、磁盘满等问题都不会影响AOF文件的可用性这点大家可以放心。
8、AOF方式的另一个好处我们通过一个“场景再现”来说明。某同学在操作redis时不小心执行了FLUSHALL导致redis内存中的数据全部被清空了这是很悲剧的事情。不过这也不是世界末日只要redis配置了AOF持久化方式且AOF文件还没有被重写rewrite我们就可以用最快的速度暂停redis并编辑AOF文件将最后一行的FLUSHALL命令删除然后重启redis就可以恢复redis的所有数据到FLUSHALL之前的状态了。是不是很神奇这就是AOF持久化方式的好处之一。但是如果AOF文件已经被重写了那就无法通过这种方法来恢复数据了。
9、虽然优点多多但AOF方式也同样存在缺陷比如在同样数据规模的情况下AOF文件要比RDB文件的体积大。而且AOF方式的恢复速度也要慢于RDB方式。
如果你直接执行BGREWRITEAOF命令那么redis会生成一个全新的AOF文件其中便包括了可以恢复现有数据的最少的命令集。
10、如果运气比较差AOF文件出现了被写坏的情况也不必过分担忧redis并不会贸然加载这个有问题的AOF文件而是报错退出。这时可以通过以下步骤来修复出错的文件
1.备份被写坏的AOF文件 2.运行redis-check-aof –fix进行修复 3.用diff -u来看下两个文件的差异确认问题点 4.重启redis加载修复后的AOF文件
四、redis持久化----AOF重写
1、AOF重写的内部运行原理我们有必要了解一下。
2、在重写即将开始之际redis会创建fork一个“重写子进程”这个子进程会首先读取现有的AOF文件并将其包含的指令进行分析压缩并写入到一个临时文件中。
3、与此同时主工作进程会将新接收到的写指令一边累积到内存缓冲区中一边继续写入到原有的AOF文件中这样做是保证原有的AOF文件的可用性避免在重写过程中出现意外。
4、当“重写子进程”完成重写工作后它会给父进程发一个信号父进程收到信号后就会将内存中缓存的写指令追加到新AOF文件中。
5、当追加结束后redis就会用新AOF文件来代替旧AOF文件之后再有新的写指令就都会追加到新的AOF文件中了。
五、redis持久化----如何选择RDB和AOF
1、对于我们应该选择RDB还是AOF官方的建议是两个同时使用。这样可以提供更可靠的持久化方案。
2、redis的备份和还原可以借助第三方的工具redis-dump。
六、Redis的两种持久化方式也有明显的缺点
1、RDB需要定时持久化风险是可能会丢两次持久之间的数据量可能很大。
2、AOF每秒fsync一次指令硬盘如果硬盘IO慢会阻塞父进程风险是会丢失1秒多的数据在Rewrite过程中主进程把指令存到mem-buffer中最后写盘时会阻塞主进程。
redids事务 ACID
Redis 的事务需要先划分出三个阶段
事务开启使用 MULTI 可以标志着执行该命令的客户端从非事务状态切换至事务状态 命令入队MULTI开启事务之后非 WATCH、EXEC、DISCARD、MULTI 等特殊命令客户端的命令不会被立即执行而是放入一个事务队列 如果收到 EXEC 命令事务队列里的命令将会被执行 如果收到 DISCARD 命令则事务被丢弃。 命令入队过程如果出错(如使用了不存在的命令)则事务队列会被拒接执行
执行事务执行事务期间出现了异常(如命令和操作的数据类型不匹配)事务队列的里的命令还是继续执行下去直到全部命令执行完不会回滚。 WATCH 可用于监控 redis 变量值在命令 EXEC 之前redis 里的数据是有机会被其他客户端的命令修改的。使用 WATCH 监控的变量被修改后执行 EXEC 时则会返回执行失败的 nil 回复
从严格意义上来说Redis 是没有事务的。因为事务必须具备四个特点
原子性(Atomicity) 一致性(Consistency) 隔离性(Isolation) 持久性(Durability)
Redis 是做不到这四点只是具备其中一些特征redis的事务是个伪事务而且不支持回滚。
原子性
EXEC命令执行前
在命令入队时就报错如内存不足命令名称错误redis 就会报错并且记录下这个错误。此时客户还能继续提交命令操作等到执行EXEC时redis 就会拒绝执行所有提交的命令操作返回事务失败的结果 nil。
EXEC命令执行后
命令和操作的数据类型不匹配但 redis 实例没有检查出错误。在执行完 EXEC 命令以后redis 实际执行这些指令就会报错。此时事务是不会回滚的但事务队列的命令还是继续被执行。事务的原子性无法保证。
EXEC执行时发生故障
如果 redis 开启了 AOF 日志那么只会有部分的事务操作被记录到 AOF 日志中。需要使用 redis-check-aof 工具检查 AOF 日志文件这个工具可以把未完成的事务操作从 AOF 文件中去除。事务的原子性得到保证。
一致性
EXEC命令执行前
入队报错事务会被放弃执行具有一致性。
EXEC命令执行后
实际执行时报错错误的指令不会执行正确的指令可以正常执行一致性可以保证。
EXEC执行时发生故障
RDB 模式RDB 快照不会在事务执行时执行事务结果不会保存在RDB AOF 模式可以使用 redis-check-aof 工具检查 AOF 日志文件把未完成的事务操作从 AOF 文件中去除。可以保证一致性。
隔离性
EXEC 命令执行前
隔离性需要通过 WATCH 机制保证。因为 EXEC 命令执行前其他客户端命令可以被执行相关变量会被修改但可以使用 WATCH 机制监控相关变量。一旦相关变量被修改则 EXEC 后则事务失败返回具有隔离性。
EXEC 命令执行后
Redis 是单线程执行事务队列里的命令和其他客户端的命令只能二选一被顺序执行因此具有隔离性
持久性
如果 redis 没有使用 RDB 或 AOF事务的持久化是不存在的 RDB 模式那么在一个事务执行后而下一次的 RDB快照还未执行前如果发生了实例宕机数据丢失这种情况下事务修改的数据也是不能保证持久化 AOF 模式因为 AOF 模式的三种配置选项 no、everysec 和 always 都会存在数据丢失的情况。所以事务的持久性属性也还是得不到保证。 总结
Redis 的事务机制可以保证一致性和隔离性但是无法保证持久性具备了一定的原子性但不支持回滚。
redis部署方案
standaloan(单机模式)
standaloan 是redis单机模式及所有服务连接一台redis服务该模式不适用生产。如果发生宕机内存爆炸就可能导致所有连接改redis的服务发生缓存失效引起雪崩。
ssentinel哨兵模式
redis-Sentinel(哨兵模式)是Redis官方推荐的高可用性(HA)解决方案当用Redis做Master-slave的高可用方案时假如master宕机了Redis本身(包括它的很多客户端)都没有实现自动进行主备切换而Redis-sentinel本身也是一个独立运行的进程它能监控多个master-slave集群发现master宕机后能进行切换 redis集群模式同样可以实现redis高可用部署,Redis Sentinel集群模式中随着业务量和数据量增到性能达到redis单节点瓶颈垂直扩容受机器限制水平扩容涉及对应用的影响以及数据迁移中数据丢失风险。针对这些痛点 Redis3.0推出cluster分布式集群方案当遇到单节点内存并发流量瓶颈是采用cluster方案实现负载均衡cluster方案主要解决分片问题即把整个数据按照规则分成多个子集存储在多个不同几点上每个节点负责自己整个数据的一部分。
Redis Cluster 集群模式
redis cluster 采用哈希分区规则中的虚拟槽分区。虚拟槽分区巧妙地使用了哈希空间使用分散度良好的哈希函数把所有的数据映射到一个固定范围内的整数集合整数定义为槽slot。Redis Cluster槽的范围是0 16383。槽是集群内数据管理和迁移的基本单位。采用大范围的槽的主要目的是为了方便数据的拆分和集群的扩展每个节点负责一定数量的槽。Redis Cluster采用虚拟槽分区所有的键根据哈希函数映射到0 16383计算公式slot CRC16(key)16383。每一个实节点负责维护一部分槽以及槽所映射的键值数据。下图展现一个五个节点构成的集群每个节点平均大约负责3276个槽以及通过计算公式映射到对应节点的对应槽的过程。1.单机模式
优点
架构简单部署方便 高性价比缓存使用时无需备用节点单实例可用性可以用supervisor或crontab保证当然为了满足业务的高可用性也可以牺牲一个备用节点但同时刻只有一个实例对外提供服务 高性能。 缺点
不保证数据的可靠性 在缓存使用进程重启后数据丢失即使有备用的节点解决高可用性但是仍然不能解决缓存预热问题因此不适用于数据可靠性要求高的业务 高性能受限于单核CPU的处理能力Redis是单线程机制CPU为主要瓶颈所以适合操作命令简单排序、计算较少的场景。也可以考虑用Memcached替代。 2.主从模式
Redis多副本采用主从replication部署结构相较于单副本而言最大的特点就是主从实例间数据实时同步并且提供数据持久化和备份策略。主从实例部署在不同的物理服务器上根据公司的基础环境配置可以实现同时对外提供服务和读写分离策略。
优点
高可靠性一方面采用双机主备架构能够在主库出现故障时自动进行主备切换从库提升为主库提供服务保证服务平稳运行另一方面开启数据持久化功能和配置合理的备份策略能有效的解决数据误操作和数据异常丢失的问题 读写分离策略从节点可以扩展主库节点的读能力有效应对大并发量的读操作。 缺点
故障恢复复杂如果没有RedisHA系统需要开发当主库节点出现故障时需要手动将一个从节点晋升为主节点同时需要通知业务方变更配置并且需要让其它从库节点去复制新主库节点整个过程需要人为干预比较繁琐 主库的写能力受到单机的限制可以考虑分片 主库的存储能力受到单机的限制可以考虑Pika 原生复制的弊端在早期的版本中也会比较突出如Redis复制中断后Slave会发起psync此时如果同步不成功则会进行全量同步主库执行全量备份的同时可能会造成毫秒或秒级的卡顿又由于COW机制导致极端情况下的主库内存溢出程序异常退出或宕机主库节点生成备份文件导致服务器磁盘IO和CPU压缩资源消耗发送数GB大小的备份文件导致服务器出口带宽暴增阻塞请求建议升级到最新版本。 3.哨兵模式
Redis Sentinel是社区版本推出的原生高可用解决方案其部署架构主要包括两部分Redis Sentinel集群和Redis数据集群。
其中Redis Sentinel集群是由若干Sentinel节点组成的分布式集群可以实现故障发现、故障自动转移、配置中心和客户端通知。Redis Sentinel的节点数量要满足2n1n1的奇数个。
锁的
优点
Redis Sentinel集群部署简单 能够解决Redis主从模式下的高可用切换问题 很方便实现Redis数据节点的线形扩展轻松突破Redis自身单线程瓶颈可极大满足Redis大容量或高性能的业务需求 可以实现一套Sentinel监控一组Redis数据节点或多组数据节点。 缺点
部署相对Redis主从模式要复杂一些原理理解更繁琐 资源浪费Redis数据节点中slave节点作为备份节点不提供服务 Redis Sentinel主要是针对Redis数据节点中的主节点的高可用切换对Redis的数据节点做失败判定分为主观下线和客观下线两种对于Redis的从节点有对节点做主观下线操作并不执行故障转移。 不能解决读写分离问题实现起来相对复杂。 建议
如果监控同一业务可以选择一套Sentinel集群监控多组Redis数据节点的方案反之选择一套Sentinel监控一组Redis数据节点的方案。 sentinel monitor 配置中的建议设置成Sentinel节点的一半加1当Sentinel部署在多个IDC的时候单个IDC部署的Sentinel数量不建议超过Sentinel数量 – quorum。 合理设置参数防止误切控制切换灵敏度控制 a. quorum
b. down-after-milliseconds 30000
c. failover-timeout 180000
d. maxclient
e. timeout
部署的各个节点服务器时间尽量要同步否则日志的时序性会混乱。 Redis建议使用pipeline和multi-keys操作减少RTT次数提高请求效率。 自行搞定配置中心zookeeper方便客户端对实例的链接访问。 4.集群方式
Redis Cluster是社区版推出的Redis分布式集群解决方案主要解决Redis分布式方面的需求比如当遇到单机内存并发和流量等瓶颈的时候Redis Cluster能起到很好的负载均衡的目的。
Redis Cluster集群节点最小配置6个节点以上3主3从其中主节点提供读写操作从节点作为备用节点不提供请求只作为故障转移使用。
Redis Cluster采用虚拟槽分区所有的键根据哈希函数映射到016383个整数槽内每个节点负责维护一部分槽以及槽所印映射的键值数据。
优点
无中心架构 数据按照slot存储分布在多个节点节点间数据共享可动态调整数据分布 可扩展性可线性扩展到1000多个节点节点可动态添加或删除 高可用性部分节点不可用时集群仍可用。通过增加Slave做standby数据副本能够实现故障自动failover节点之间通过gossip协议交换状态信息用投票机制完成Slave到Master的角色提升 降低运维成本提高系统的扩展性和可用性。 缺点
Client实现复杂驱动要求实现Smart Client缓存slots mapping信息并及时更新提高了开发难度客户端的不成熟影响业务的稳定性。目前仅JedisCluster相对成熟异常处理部分还不完善比如常见的“max redirect exception”。 节点会因为某些原因发生阻塞阻塞时间大于clutser-node-timeout被判断下线这种failover是没有必要的。 数据通过异步复制不保证数据的强一致性。 多个业务使用同一套集群时无法根据统计区分冷热数据资源隔离性较差容易出现相互影响的情况。 Slave在集群中充当“冷备”不能缓解读压力当然可以通过SDK的合理设计来提高Slave资源的利用率。 Key批量操作限制如使用mset、mget目前只支持具有相同slot值的Key执行批量操作。对于映射为不同slot值的Key由于Keys不支持跨slot查询所以执行mset、mget、sunion等操作支持不友好。 Key事务操作支持有限只支持多key在同一节点上的事务操作当多个Key分布于不同的节点上时无法使用事务功能。 Key作为数据分区的最小粒度不能将一个很大的键值对象如hash、list等映射到不同的节点。 不支持多数据库空间单机下的redis可以支持到16个数据库集群模式下只能使用1个数据库空间即db 0。 复制结构只支持一层从节点只能复制主节点不支持嵌套树状复制结构。 避免产生hot-key导致主库节点成为系统的短板。 避免产生big-key导致网卡撑爆、慢查询等。 重试时间应该大于cluster-node-time时间。 Redis Cluster不建议使用pipeline和multi-keys操作减少max redirect产生的场景。 Redis持久化 由于Redis的数据都存放在内存中如果没有配置持久化redis重启后数据就全丢失了于是需要开启redis的持久化功能将数据保存到磁盘上当redis重启后可以从磁盘中恢复数据。redis提供两种方式进行持久化一种是RDB持久化原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化另外一种是AOFappend only file持久化原理是将Reids的操作日志以追加的方式写入文件。那么这两种持久化方式有什么区别呢改如何选择呢网上看了大多数都是介绍这两种方式怎么配置怎么使用就是没有介绍二者的区别在什么应用场景下使用。
2、二者的区别
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘实际操作过程是fork一个子进程先将数据集写入临时文件写入成功后再替换之前的文件用二进制压缩存储。
AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作查询操作不会记录以文本的方式记录可以打开文件看到详细的操作记录。
3、二者优缺点
RDB存在哪些优势呢
1). 一旦采用该方式那么你的整个Redis数据库将只包含一个文件这对于文件备份而言是非常完美的。比如你可能打算每个小时归档一次最近24小时的数据同时还要每天归档一次最近30天的数据。通过这样的备份策略一旦系统出现灾难性故障我们可以非常容易的进行恢复。
2). 对于灾难恢复而言RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。
3). 性能最大化。对于Redis的服务进程而言在开始持久化时它唯一需要做的只是fork出子进程之后再由子进程完成这些持久化的工作这样就可以极大的避免服务进程执行IO操作了。
4). 相比于AOF机制如果数据集很大RDB的启动效率会更高。
RDB又存在哪些劣势呢
1). 如果你想保证数据的高可用性即最大限度的避免数据丢失那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象此前没有来得及写入磁盘的数据都将丢失。
2). 由于RDB是通过fork子进程来协助完成数据持久化工作的因此如果当数据集较大时可能会导致整个服务器停止服务几百毫秒甚至是1秒钟。
AOF的优势有哪些呢
1). 该机制可以带来更高的数据安全性即数据持久性。Redis中提供了3中同步策略即每秒同步、每修改同步和不同步。事实上每秒同步也是异步完成的其效率也是非常高的所差的是一旦系统出现宕机现象那么这一秒钟之内修改的数据将会丢失。而每修改同步我们可以将其视为同步持久化即每次发生的数据变化都会被立即记录到磁盘中。可以预见这种方式在效率上是最低的。至于无同步无需多言我想大家都能正确的理解它。
2). 由于该机制对日志文件的写入操作采用的是append模式因此在写入过程中即使出现宕机现象也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题不用担心在Redis下一次启动之前我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。
3). 如果日志过大Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。
4). AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上我们也可以通过该文件完成数据的重建。
AOF的劣势有哪些呢
1). 对于相同数量的数据集而言AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
2). 根据同步策略的不同AOF在运行效率上往往会慢于RDB。总之每秒同步策略的效率是比较高的同步禁用策略的效率和RDB一样高效。
二者选择的标准就是看系统是愿意牺牲一些性能换取更高的缓存一致性aof还是愿意写操作频繁的时候不启用备份来换取更高的性能待手动运行save的时候再做备份rdb。rdb这个就更有些 eventually consistent的意思了。
4、常用配置
RDB持久化配置
Redis会将数据集的快照dump到dump.rdb文件中。此外我们也可以通过配置文件来修改Redis服务器dump快照的频率在打开6379.conf文件之后我们搜索save可以看到下面的配置信息
save 900 1 #在900秒(15分钟)之后如果至少有1个key发生变化则dump内存快照。
save 300 10 #在300秒(5分钟)之后如果至少有10个key发生变化则dump内存快照。
save 60 10000 #在60秒(1分钟)之后如果至少有10000个key发生变化则dump内存快照。
AOF持久化配置
在Redis的配置文件中存在三种同步方式它们分别是
appendfsync always #每次有数据修改发生时都会写入AOF文件。
appendfsync everysec #每秒钟同步一次该策略为AOF的缺省策略。
appendfsync no #从不同步。高效但是数据不会被持久化。
5、Redis自研
Redis自研的高可用解决方案主要体现在配置中心、故障探测和failover的处理机制上通常需要根据企业业务的实际线上环境来定制化。
优点
高可靠性、高可用性 自主可控性高 贴切业务实际需求可缩性好兼容性好。 缺点
实现复杂开发成本高 需要建立配套的周边设施如监控域名服务存储元数据信息的数据库等 维护成本高。
Redis有哪些优缺点
优点
读写性能优异 Redis能读的速度是110000次/s写的速度是81000次/s。 支持数据持久化支持AOF和RDB两种持久化方式。 支持事务Redis的所有操作都是原子性的同时Redis还支持对几个操作合并后的原子性执行。 数据结构丰富除了支持string类型的value外还支持hash、set、zset、list等数据结构。 支持主从复制主机会自动将数据同步到从机可以进行读写分离。
缺点
数据库容量受到物理内存的限制不能用作海量数据的高性能读写因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。 Redis 不具备自动容错和恢复功能主机从机的宕机都会导致前端部分读写请求失败需要等待机器重启或者手动切换前端的IP才能恢复。 主机宕机宕机前有部分数据未能及时同步到从机切换IP后还会引入数据不一致的问题降低了系统的可用性。 Redis 较难支持在线扩容在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题运维人员在系统上线时必须确保有足够的空间这对资源造成了很大的浪费。 为什么要用 Redis /为什么要用缓存 主要从“高性能”和“高并发”这两点来看待这个问题。
高性能
假如用户第一次访问数据库中的某些数据。这个过程会比较慢因为是从硬盘上读取的。将该用户访问的数据存在数缓存中这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存所以速度相当快。如果数据库中的对应数据改变的之后同步改变缓存中相应的数据即可
高并发:
直接操作缓存能够承受的请求是远远大于直接访问数据库的所以我们可以考虑把数据库中的部分数据转移到缓存中去这样用户的一部分请求会直接到缓存这里而不用经过数据库。 为什么要用 Redis 而不用 map/guava 做缓存?
缓存分为本地缓存和分布式缓存。以 Java 为例使用自带的 map 或者 guava 实现的是本地缓存最主要的特点是轻量以及快速生命周期随着 jvm 的销毁而结束并且在多实例的情况下每个实例都需要各自保存一份缓存缓存不具有一致性。
使用 redis 或 memcached 之类的称为分布式缓存在多实例的情况下各实例共用一份缓存数据缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用整个程序架构上较为复杂。
Redis为什么这么快
1、完全基于内存绝大部分请求是纯粹的内存操作非常快速。数据存在内存中类似于 HashMapHashMap 的优势就是查找和操作的时间复杂度都是O(1)
2、数据结构简单对数据操作也简单Redis 中的数据结构是专门进行设计的
3、采用单线程避免了不必要的上下文切换和竞争条件也不存在多进程或者多线程导致的切换而消耗 CPU不用去考虑各种锁的问题不存在加锁释放锁操作没有因为可能出现死锁而导致的性能消耗
4、使用多路 I/O 复用模型非阻塞 IO
5、使用底层模型不同它们之间底层实现方式以及与客户端之间通信的应用协议不一样Redis 直接自己构建了 VM 机制 因为一般的系统调用系统函数的话会浪费一定的时间去移动和请求
Redis有哪些数据类型
Redis主要有5种数据类型包括StringListSetZsetHash满足大部分的使用要求
Redis的应用场景
总结一
计数器
可以对 String 进行自增自减运算从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高很适合存储频繁读写的计数量。
缓存
将热点数据放到内存中设置内存的最大使用量以及淘汰策略来保证缓存的命中率。
会话缓存
可以使用 Redis 来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息也就不再具有状态一个用户可以请求任意一个应用服务器从而更容易实现高可用性以及可伸缩性。
全页缓存FPC
除基本的会话token之外Redis还提供很简便的FPC平台。以Magento为例Magento提供一个插件来使用Redis作为全页缓存后端。此外对WordPress的用户来说Pantheon有一个非常好的插件 wp-redis这个插件能帮助你以最快速度加载你曾浏览过的页面。
查找表
例如 DNS 记录就很适合使用 Redis 进行存储。查找表和缓存类似也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效而缓存的内容可以失效因为缓存不作为可靠的数据来源。
消息队列(发布/订阅功能)
List 是一个双向链表可以通过 lpush 和 rpop 写入和读取消息。不过最好使用 Kafka、RabbitMQ 等消息中间件。
分布式锁实现
在分布式场景下无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX 命令实现分布式锁除此之外还可以使用官方提供的 RedLock 分布式锁实现。
其它
Set 可以实现交集、并集等操作从而实现共同好友等功能。ZSet 可以实现有序性操作从而实现排行榜等功能。
总结二
Redis相比其他缓存有一个非常大的优势就是支持多种数据类型。
数据类型说明string字符串最简单的k-v存储hashhash格式value为field和value适合ID-Detail这样的场景。list简单的list顺序列表支持首位或者末尾插入数据set无序list查找速度快适合交集、并集、差集处理sorted set有序的set
其实通过上面的数据类型的特性基本就能想到合适的应用场景了。
string——适合最简单的k-v存储类似于memcached的存储结构短信验证码配置信息等就用这种类型来存储。
hash——一般key为ID或者唯一标示value对应的就是详情了。如商品详情个人信息详情新闻详情等。
list——因为list是有序的比较适合存储一些有序且数据相对固定的数据。如省市区表、字典表等。因为list是有序的适合根据写入的时间来排序如最新的***消息队列等。
set——可以简单的理解为ID-List的模式如微博中一个人有哪些好友set最牛的地方在于可以对两个set提供交集、并集、差集操作。例如查找两个人共同的好友等。
Sorted Set——是set的增强版本增加了一个score参数自动会根据score的值进行排序。比较适合类似于top 10等不根据插入的时间来排序的数据。
如上所述虽然Redis不像关系数据库那么复杂的数据结构但是也能适合很多场景比一般的缓存数据结构要多。了解每种数据结构适合的业务场景不仅有利于提升开发效率也能有效利用Redis的性能。
什么是Redis持久化
持久化就是把内存的数据写到磁盘中去防止服务宕机了内存数据丢失。
Redis 的持久化机制是什么各自的优缺点
Redis 提供两种持久化机制 RDB默认 和 AOF 机制:
RDB是 Redis DataBase 缩写快照
RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。 优点
1、只有一个文件 dump.rdb方便持久化。 2、容灾性好一个文件可以保存到安全的磁盘。 3、性能最大化fork 子进程来完成写操作让主进程继续处理命令所以是 IO 最大化。使用单独子进程来进行持久化主进程不会进行任何 IO 操作保证了 redis 的高性能 4.相对于数据集大时比 AOF 的启动效率更高。
缺点
1、数据安全性低。RDB 是间隔一段时间进行持久化如果持久化之间 redis 发生故障会发生数据丢失。所以这种方式更适合数据要求不严谨的时候) 2、AOFAppend-only file)持久化方式 是指所有的命令行记录以 redis 命令请 求协议的格式完全持久化存储)保存为 aof 文件。 AOF持久化
AOF持久化(即Append Only File持久化)则是将Redis执行的每次写命令记录到单独的日志文件中当重启Redis会重新将持久化的日志中文件恢复数据。
当两种方式同时开启时数据恢复Redis会优先选择AOF恢复。 优点
1、数据安全aof 持久化可以配置 appendfsync 属性有 always每进行一次 命令操作就记录到 aof 文件中一次。 2、通过 append 模式写文件即使中途服务器宕机可以通过 redis-check-aof 工具解决数据一致性问题。 3、AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前文件过大时会对命令 进行合并重写可以删除其中的某些命令比如误操作的 flushall) 缺点
1、AOF 文件比 RDB 文件大且恢复速度慢。 2、数据集大的时候比 rdb 启动效率低。 优缺点是什么
AOF文件比RDB更新频率高优先使用AOF还原数据。 AOF比RDB更安全也更大 RDB性能比AOF好 如果两个都配了优先加载AOF 如何选择合适的持久化方式 一般来说 如果想达到足以媲美PostgreSQL的数据安全性你应该同时使用两种持久化功能。在这种情况下当 Redis 重启的时候会优先载入AOF文件来恢复原始的数据因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
如果你非常关心你的数据 但仍然可以承受数分钟以内的数据丢失那么你可以只使用RDB持久化。
有很多用户都只使用AOF持久化但并不推荐这种方式因为定时生成RDB快照snapshot非常便于进行数据库备份 并且 RDB 恢复数据集的速度也要比AOF恢复的速度要快除此之外使用RDB还可以避免AOF程序的bug。
如果你只希望你的数据在服务器运行的时候存在你也可以不使用任何持久化方式。
Redis持久化数据和缓存怎么做扩容 如果Redis被当做缓存使用使用一致性哈希实现动态扩容缩容。
如果Redis被当做一个持久化存储使用必须使用固定的keys-to-nodes映射关系节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情况必须使用可以在运行时进行数据再平衡的一套系统而当前只有Redis集群可以做到这样。
过期键的删除策略 Redis的过期键的删除策略 我们都知道Redis是key-value数据库我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了Redis如何处理。
过期策略通常有以下三种
定时过期每个设置过期时间的key都需要创建一个定时器到过期时间就会立即清除。该策略可以立即清除过期的数据对内存很友好但是会占用大量的CPU资源去处理过期的数据从而影响缓存的响应时间和吞吐量。 惰性过期只有当访问一个key时才会判断该key是否已过期过期则清除。该策略可以最大化地节省CPU资源却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问从而不会被清除占用大量内存。 定期过期每隔一定的时间会扫描一定数量的数据库的expires字典中一定数量的key并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时可以在不同情况下使得CPU和内存资源达到最优的平衡效果。 (expires字典会保存所有设置了过期时间的key的过期时间数据其中key是指向键空间中的某个键的指针value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。) Redis中同时使用了惰性过期和定期过期两种过期策略。
Redis key的过期时间和永久有效分别怎么设置 EXPIRE和PERSIST命令。
我们知道通过expire来设置key 的过期时间那么对过期的数据怎么处理呢? 除了缓存服务器自带的缓存失效策略之外Redis默认的有6中策略可供选择我们还可以根据具体的业务需求进行自定义的缓存淘汰常见的策略有两种
定时去清理过期的缓存
当有用户请求过来时再判断这个请求所用到的缓存是否过期过期的话就去底层系统得到新数据并更新缓存。
两者各有优劣第一种的缺点是维护大量缓存的key是比较麻烦的第二种的缺点就是每次用户请求过来都要判断缓存失效逻辑相对比较复杂具体用哪种方案大家可以根据自己的应用场景来权衡。
内存相关 MySQL里有2000w数据redis中只存20w的数据如何保证redis中的数据都是热点数据 redis内存数据集大小上升到一定大小的时候就会施行数据淘汰策略。
Redis的内存淘汰策略有哪些 Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时怎么处理需要新写入且需要申请额外空间的数据。
全局的键空间选择性移除
noeviction当内存不足以容纳新写入数据时新写入操作会报错。 allkeys-lru当内存不足以容纳新写入数据时在键空间中移除最近最少使用的key。这个是最常用的 allkeys-random当内存不足以容纳新写入数据时在键空间中随机移除某个key。 设置过期时间的键空间选择性移除
volatile-lru当内存不足以容纳新写入数据时在设置了过期时间的键空间中移除最近最少使用的key。 volatile-random当内存不足以容纳新写入数据时在设置了过期时间的键空间中随机移除某个key。 volatile-ttl当内存不足以容纳新写入数据时在设置了过期时间的键空间中有更早过期时间的key优先移除。 总结
Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据过期策略用于处理过期的缓存数据。
Redis主要消耗什么物理资源 内存。
Redis的内存用完了会发生什么 如果达到设置的上限Redis的写命令会返回错误信息但是读命令还可以正常返回。或者你可以配置内存淘汰机制当Redis达到内存上限时会冲刷掉旧的内容。
Redis如何做内存优化 可以好好利用Hash,list,sorted set,set等集合类型数据因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。尽可能使用散列表hashes散列表是说散列表里面存储的数少使用的内存非常小所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象不要为这个用户的名称姓氏邮箱密码设置单独的key而是应该把这个用户的所有信息存储到一张散列表里面
线程模型 Redis线程模型 Redis基于Reactor模式开发了网络事件处理器这个处理器被称为文件事件处理器file event handler。它的组成结构为4部分多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的所以Redis才叫单线程模型。
文件事件处理器使用 I/O 多路复用multiplexing程序来同时监听多个套接字 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。 当被监听的套接字准备好执行连接应答accept、读取read、写入write、关闭close等操作时 与操作相对应的文件事件就会产生 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。 虽然文件事件处理器以单线程方式运行 但通过使用 I/O 多路复用程序来监听多个套接字 文件事件处理器既实现了高性能的网络通信模型 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接 这保持了 Redis 内部单线程设计的简单性。
参考https://www.cnblogs.com/barrywxx/p/8570821.html
事务 什么是事务 事务是一个单独的隔离操作事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中不会被其他客户端发送来的命令请求所打断。
事务是一个原子操作事务中的命令要么全部被执行要么全部都不执行。
Redis事务的概念 Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合。事务支持一次执行多个命令一个事务中所有命令都会被序列化。在事务执行过程会按照顺序串行化执行队列中的命令其他客户端提交的命令请求不会插入到事务执行命令序列中。
总结说redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
Redis事务的三个阶段 事务开始 MULTI 命令入队 事务执行 EXEC 事务执行过程中如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求将会把请求放入队列中排队
Redis事务相关命令 Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的
Redis会将一个事务中的所有命令序列化然后按顺序执行。
redis 不支持回滚“Redis 在事务失败时不进行回滚而是继续执行余下的命令” 所以 Redis 的内部可以保持简单且快速。 如果在一个事务中的命令出现错误那么所有的命令都不会执行 如果在一个事务中出现运行错误那么正确的命令会被执行。 WATCH 命令是一个乐观锁可以为 Redis 事务提供 check-and-set CAS行为。 可以监控一个或多个键一旦其中有一个键被修改或删除之后的事务就不会执行监控一直持续到EXEC命令。 MULTI命令用于开启一个事务它总是返回OK。 MULTI执行之后客户端可以继续向服务器发送任意多条命令这些命令不会立即被执行而是被放到一个队列中当EXEC命令被调用时所有队列中的命令才会被执行。 EXEC执行所有事务块内的命令。返回事务块内所有命令的返回值按命令执行的先后顺序排列。 当操作被打断时返回空值 nil 。 通过调用DISCARD客户端可以清空事务队列并放弃执行事务 并且客户端会从事务状态中退出。 UNWATCH命令可以取消watch对所有key的监控。 事务管理ACID概述 原子性Atomicity 原子性是指事务是一个不可分割的工作单位事务中的操作要么都发生要么都不发生。
一致性Consistency 事务前后数据的完整性必须保持一致。
隔离性Isolation 多个事务并发执行时一个事务的执行不应影响其他事务的执行
持久性Durability 持久性是指一个事务一旦被提交它对数据库中数据的改变就是永久性的接下来即使数据库发生故障也不应该对其有任何影响
Redis的事务总是具有ACID中的一致性和隔离性其他特性是不支持的。当服务器运行在AOF持久化模式下并且appendfsync选项的值为always时事务也具有耐久性。
Redis事务支持隔离性吗 Redis 是单进程程序并且它保证在执行事务时不会对事务进行中断事务可以运行直到执行完所有事务队列中的命令为止。因此Redis 的事务是总是带有隔离性的。
Redis事务保证原子性吗支持回滚吗 Redis中单条命令是原子性执行的但事务不保证原子性且没有回滚。事务中任意命令执行失败其余的命令仍会被执行。
Redis事务其他实现 基于Lua脚本Redis可以保证脚本内的命令一次性、按顺序地执行 其同时也不提供事务运行错误的回滚执行过程中如果部分命令运行错误剩下的命令还是会继续运行完 基于中间标记变量通过另外的标记变量来标识事务是否执行完成读取数据时先读取该标记变量判断是否事务执行完成。但这样会需要额外写代码实现比较繁琐
Redis 主从架构
单机的 redis能够承载的 QPS 大概就在上万到几万不等。对于缓存来说一般都是用来支撑读高并发的。因此架构做成主从(master-slave)架构一主多从主负责写并且将数据复制到其它的 slave 节点从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容支撑读高并发。 redis replication - 主从架构 - 读写分离 - 水平扩容支撑读高并发
redis replication 的核心机制
redis 采用异步方式复制数据到 slave 节点不过 redis2.8 开始slave node 会周期性地确认自己每次复制的数据量 一个 master node 是可以配置多个 slave node 的 slave node 也可以连接其他的 slave node slave node 做复制的时候不会 block master node 的正常工作 slave node 在做复制的时候也不会 block 对自己的查询操作它会用旧的数据集来提供服务但是复制完成的时候需要删除旧数据集加载新数据集这个时候就会暂停对外服务了 slave node 主要用来进行横向扩容做读写分离扩容的 slave node 可以提高读的吞吐量。 注意如果采用了主从架构那么建议必须开启 master node 的持久化不建议用 slave node 作为 master node 的数据热备因为那样的话如果你关掉 master 的持久化可能在 master 宕机重启的时候数据是空的然后可能一经过复制 slave node 的数据也丢了。
另外master 的各种备份方案也需要做。万一本地的所有文件丢失了从备份中挑选一份 rdb 去恢复 master这样才能确保启动的时候是有数据的即使采用了后续讲解的高可用机制slave node 可以自动接管 master node但也可能 sentinel 还没检测到 master failuremaster node 就自动重启了还是可能导致上面所有的 slave node 数据被清空。
redis 主从复制的核心原理
当启动一个 slave node 的时候它会发送一个 PSYNC 命令给 master node。
如果这是 slave node 初次连接到 master node那么会触发一次 full resynchronization 全量复制。此时 master 会启动一个后台线程开始生成一份 RDB 快照文件
同时还会将从客户端 client 新收到的所有写命令缓存在内存中。RDB 文件生成完毕后 master 会将这个 RDB 发送给 slaveslave 会先写入本地磁盘然后再从本地磁盘加载到内存中
接着 master 会将内存中缓存的写命令发送到 slaveslave 也会同步这些数据。
slave node 如果跟 master node 有网络故障断开了连接会自动重连连接之后 master node 仅会复制给 slave 部分缺少的数据。 过程原理
当从库和主库建立MS关系后会向主数据库发送SYNC命令 主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程)并将期间接收到的写命令缓存起来 当快照完成后主Redis会将快照文件和所有缓存的写命令发送给从Redis 从Redis接收到后会载入快照文件并且执行收到的缓存的命令 之后主Redis每当接收到写命令时就会将命令发送从Redis从而保证数据的一致 缺点
所有的slave节点数据的复制和同步都由master节点来处理会照成master节点压力太大使用主从从结构来解决
Redis集群的主从复制模型是怎样的 为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用所以集群使用了主从复制模型每个节点都会有N-1个复制品
生产环境中的 redis 是怎么部署的 redis cluster10 台机器5 台机器部署了 redis 主实例另外 5 台机器部署了 redis 的从实例每个主实例挂了一个从实例5 个节点对外提供读写服务每个节点的读写高峰qps可能可以达到每秒 5 万5 台机器最多是 25 万读写请求/s。
机器是什么配置32G 内存 8 核 CPU 1T 磁盘但是分配给 redis 进程的是10g内存一般线上生产环境redis 的内存尽量不要超过 10g超过 10g 可能会有问题。
5 台机器对外提供读写一共有 50g 内存。
因为每个主实例都挂了一个从实例所以是高可用的任何一个主实例宕机都会自动故障迁移redis 从实例会自动变成主实例继续提供读写服务。
你往内存里写的是什么数据每条数据的大小是多少商品数据每条数据是 10kb。100 条数据是 1mb10 万条数据是 1g。常驻内存的是 200 万条商品数据占用内存是 20g仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。
其实大型的公司会有基础架构的 team 负责缓存集群的运维。
说说Redis哈希槽的概念 Redis集群没有使用一致性hash,而是引入了哈希槽的概念Redis集群有16384个哈希槽每个key通过CRC16校验后对16384取模来决定放置哪个槽集群的每个节点负责一部分hash槽。
Redis集群会有写操作丢失吗为什么 Redis并不能保证数据的强一致性这意味这在实际中集群在特定的条件下可能会丢失写操作。
Redis集群之间是如何复制的 异步复制
Redis集群最大节点个数是多少 16384个
Redis集群如何选择数据库 Redis集群目前无法做数据库选择默认在0数据库。
分区 Redis是单线程的如何提高多核CPU的利用率 可以在同一个服务器部署多个Redis的实例并把他们当作不同的服务器来使用在某些时候无论如何一个服务器是不够的 所以如果你想使用多个CPU你可以考虑一下分片shard。
为什么要做Redis分区 分区可以让Redis管理更大的内存Redis将可以使用所有机器的内存。如果没有分区你最多只能使用一台机器的内存。分区使Redis的计算能力通过简单地增加计算机得到成倍提升Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。
你知道有哪些Redis分区实现方案 客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取。大多数客户端已经实现了客户端分区。 代理分区 意味着客户端将请求发送给代理然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis实例然后根据Redis的响应结果返回给客户端。redis和memcached的一种代理实现就是Twemproxy 查询路由(Query routing) 的意思是客户端随机地请求任意一个redis实例然后由Redis将请求转发给正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由但并不是直接将请求从一个redis节点转发到另一个redis节点而是在客户端的帮助下直接redirected到正确的redis节点。 Redis分区有什么缺点 涉及多个key的操作通常不会被支持。例如你不能对两个集合求交集因为他们可能被存储到不同的Redis实例实际上这种情况也有办法但是不能直接使用交集指令。 同时操作多个key,则不能使用Redis事务. 分区使用的粒度是key不能使用一个非常长的排序key存储一个数据集The partitioning granularity is the key, so it is not possible to shard a dataset with a single huge key like a very big sorted set 当使用分区的时候数据处理会非常复杂例如为了备份你必须从不同的Redis实例和主机同时收集RDB / AOF文件。 分区时动态扩容或缩容可能非常复杂。Redis集群在运行时增加或者删除Redis节点能做到最大程度对用户透明地数据再平衡但其他一些客户端分区或者代理分区方法则不支持这种特性。然而有一种预分片的技术也可以较好的解决这个问题。 分布式问题 Redis实现分布式锁 Redis为单进程单线程模式采用队列模式将并发访问变成串行访问且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。
当且仅当 key 不存在将 key 的值设为 value。 若给定的 key 已经存在则 SETNX 不做任何动作
SETNX 是『SET if Not eXists』(如果不存在则 SET)的简写。
返回值设置成功返回 1 。设置失败返回 0 。
八、MyBatis
简介结构图 Mybatis缓存一级缓存、二级缓存
Mybatis的一级、二级缓存 1一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存其存储作用域为 Session当 Session flush 或 close 之后该 Session 中的所有 Cache 就将清空默认打开一级缓存。
2二级缓存与一级缓存其机制相同 二级缓存 默认也是采用 PerpetualCacheHashMap 存储不同在于其存储作用域为 Mapper(Namespace)并且可自定义存储源如 Ehcache。默认不打开二级缓存要开启二级缓存使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置
3对于缓存数据更新机制当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后默认该作用域下所有 select 中的缓存将被 clear。
MyBatis是什么
MyBatis 是一款优秀的持久层框架一个半 ORM对象关系映射框架它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJOPlain Old Java Objects普通老式 Java 对象为数据库中的记录。
ORM是什么 ORMObject Relational Mapping对象关系映射是一种为了解决关系型数据库数据与简单Java对象POJO的映射关系的技术。简单的说ORM是通过使用描述对象和数据库之间映射的元数据将程序中的对象自动持久化到关系型数据库中。
为什么说Mybatis是半自动ORM映射工具它与全自动的区别在哪里 Hibernate属于全自动ORM映射工具使用Hibernate查询关联对象或者关联集合对象时可以根据对象关系模型直接获取所以它是全自动的。
而Mybatis在查询关联对象或关联集合对象时需要手动编写sql来完成所以称之为半自动ORM映射工具。
传统JDBC开发存在的问题 频繁创建数据库连接对象、释放容易造成系统资源浪费影响系统性能。可以使用连接池解决这个问题。但是使用jdbc需要自己实现连接池。 sql语句定义、参数设置、结果集处理存在硬编码。实际项目中sql语句变化的可能性较大一旦发生变化需要修改java代码系统需要重新编译重新发布。不好维护。 使用preparedStatement向占有位符号传参数存在硬编码因为sql语句的where条件不一定可能多也可能少修改sql还要修改代码系统不易维护。 结果集处理存在重复代码处理麻烦。如果可以映射成Java对象会比较方便。
mybatis的实现原理
参考文章mybatis实现原理
JDBC编程有哪些不足之处MyBatis是如何解决这些问题的
1、数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能如果使用数据库连接池可解决此问题。
解决在mybatis-config.xml中配置数据链接池使用连接池管理数据库连接。
2、Sql语句写在代码中造成代码不易维护实际应用sql变化的可能较大sql变动需要改变java代码。
解决将Sql语句配置在XXXXmapper.xml文件中与java代码分离。
3、向sql语句传参数麻烦因为sql语句的where条件不一定可能多也可能少占位符需要和参数一一对应。
解决 Mybatis自动将java对象映射至sql语句。
4、对结果集解析麻烦sql变化导致解析代码变化且解析前需要遍历如果能将数据库记录封装成pojo对象解析比较方便。
解决Mybatis自动将sql执行结果映射至java对象。
Mybatis优缺点
优点
与传统的数据库访问技术相比ORM有以下优点
基于SQL语句编程相当灵活不会对应用程序或者数据库的现有设计造成任何影响SQL写在XML里解除sql与程序代码的耦合便于统一管理提供XML标签支持编写动态SQL语句并可重用与JDBC相比减少了50%以上的代码量消除了JDBC大量冗余的代码不需要手动开关连接很好的与各种数据库兼容因为MyBatis使用JDBC来连接数据库所以只要JDBC支持的数据库MyBatis都支持提供映射标签支持对象与数据库的ORM字段关系映射提供对象关系映射标签支持对象关系组件维护能够与Spring很好的集成
缺点
SQL语句的编写工作量较大尤其当字段多、关联表多时对开发人员编写SQL语句的功底有一定要求SQL语句依赖于数据库导致数据库移植性差不能随意更换数据库MyBatis框架适用场景 MyBatis专注于SQL本身是一个足够灵活的DAO层解决方案。 对性能的要求很高或者需求变化较多的项目如互联网项目MyBatis将是不错的选择。 Hibernate 和 MyBatis 的区别 相同点
都是对jdbc的封装都是持久层的框架都用于dao层的开发。
不同点
映射关系
MyBatis 是一个半自动映射的框架配置Java对象与sql语句执行结果的对应关系多表关联关系配置简单 Hibernate 是一个全表映射的框架配置Java对象与数据库表的对应关系多表关联关系配置复杂 SQL优化和移植性
Hibernate 对SQL语句封装提供了日志、缓存、级联级联比 MyBatis 强大等特性此外还提供 HQLHibernate Query Language操作数据库数据库无关性支持好但会多消耗性能。如果项目需要支持多种数据库代码开发量少但SQL语句优化困难。 MyBatis 需要手动编写 SQL支持动态 SQL、处理列表、动态生成表名、支持存储过程。开发工作量相对大些。直接使用SQL语句操作数据库不支持数据库无关性但sql语句优化容易。 开发难易程度和学习成本
Hibernate 是重量级框架学习使用门槛高适合于需求相对稳定中小型的项目比如办公自动化系统
MyBatis 是轻量级框架学习使用门槛低适合于需求变化频繁大型的项目比如互联网电子商务系统
总结
MyBatis 是一个小巧、方便、高效、简单、直接、半自动化的持久层框架
Hibernate 是一个强大、方便、高效、复杂、间接、全自动化的持久层框架。
MyBatis的解析和运行原理
MyBatis编程步骤是什么样的 1、 创建SqlSessionFactory
2、 通过SqlSessionFactory创建SqlSession
3、 通过sqlsession执行数据库操作
4、 调用session.commit()提交事务
5、 调用session.close()关闭会话
MyBatis的工作原理
在学习 MyBatis 程序之前需要了解一下 MyBatis 工作原理以便于理解程序。MyBatis 的工作原理如下图
1读取 MyBatis 配置文件mybatis-config.xml 为 MyBatis 的全局配置文件配置了 MyBatis 的运行环境等信息例如数据库连接信息。
2加载映射文件。映射文件即 SQL 映射文件该文件中配置了操作数据库的 SQL 语句需要在 MyBatis 配置文件 mybatis-config.xml 中加载。mybatis-config.xml 文件可以加载多个映射文件每个文件对应数据库中的一张表。
3构造会话工厂通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。
4创建会话对象由会话工厂创建 SqlSession 对象该对象中包含了执行 SQL 语句的所有方法。
5Executor 执行器MyBatis 底层定义了一个 Executor 接口来操作数据库它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句同时负责查询缓存的维护。
6MappedStatement 对象在 Executor 接口的执行方法中有一个 MappedStatement 类型的参数该参数是对映射信息的封装用于存储要映射的 SQL 语句的 id、参数等信息。
7输入参数映射输入参数类型可以是 Map、List 等集合类型也可以是基本数据类型和 POJO 类型。输入参数映射过程类似于 JDBC 对 preparedStatement 对象设置参数的过程。
8输出结果映射输出结果类型可以是 Map、 List 等集合类型也可以是基本数据类型和 POJO 类型。输出结果映射过程类似于 JDBC 对结果集的解析过程。
MyBatis的功能架构是怎样的 把Mybatis的功能架构分为三层
API接口层提供给外部使用的接口API开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。 数据处理层负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。 基础支撑层负责最基础的功能支撑包括连接管理、事务管理、配置加载和缓存处理这些都是共用的东西将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。
MyBatis的框架架构设计是怎么样的
架构图如下 这张图从上往下看。MyBatis的初始化会从mybatis-config.xml配置文件解析构造成Configuration这个类就是图中的红框。
(1)加载配置配置来源于两个地方一处是配置文件一处是Java代码的注解将SQL的配置信息加载成为一个个MappedStatement对象包括了传入参数映射配置、执行的SQL语句、结果映射配置存储在内存中。
(2)SQL解析当API接口层接收到调用请求时会接收到传入SQL的ID和传入对象可以是Map、JavaBean或者基本数据类型Mybatis会根据SQL的ID找到对应的MappedStatement然后根据传入参数对象对MappedStatement进行解析解析后可以得到最终要执行的SQL语句和参数。
(3)SQL执行将最终得到的SQL和参数拿到数据库进行执行得到操作数据库的结果。
(4)结果映射将操作数据库的结果按照映射的配置进行转换可以转换成HashMap、JavaBean或者基本数据类型并将最终结果返回。
为什么需要预编译 定义 SQL 预编译指的是数据库驱动在发送 SQL 语句和参数给 DBMS 之前对 SQL 语句进行编译这样 DBMS 执行 SQL 时就不需要重新编译。
为什么需要预编译 JDBC 中使用对象 PreparedStatement 来抽象预编译语句使用预编译。预编译阶段可以优化 SQL 的执行。预编译之后的 SQL 多数情况下可以直接执行DBMS 不需要再次编译越复杂的SQL编译的复杂度将越大预编译阶段可以合并多次操作为一个操作。同时预编译语句对象可以重复利用。把一个 SQL 预编译后产生的 PreparedStatement 对象缓存下来下次对于同一个SQL可以直接使用这个缓存的 PreparedState 对象。Mybatis默认情况下将对所有的 SQL 进行预编译。
Mybatis都有哪些Executor执行器它们之间的区别是什么
Mybatis有三种基本的Executor执行器SimpleExecutor、ReuseExecutor、BatchExecutor。
SimpleExecutor每执行一次update或select就开启一个Statement对象用完立刻关闭Statement对象。
ReuseExecutor执行update或select以sql作为key查找Statement对象存在就使用不存在就创建用完后不关闭Statement对象而是放置于MapString, Statement内供下一次使用。简言之就是重复使用Statement对象。
BatchExecutor执行update没有selectJDBC批处理不支持select将所有sql都添加到批处理中addBatch()等待统一执行executeBatch()它缓存了多个Statement对象每个Statement对象都是addBatch()完毕后等待逐一执行executeBatch()批处理。与JDBC批处理相同。
作用范围Executor的这些特点都严格限制在SqlSession生命周期范围内。
Mybatis中如何指定使用哪一种Executor执行器
在Mybatis配置文件中在设置settings可以指定默认的ExecutorType执行器类型也可以手动给DefaultSqlSessionFactory的创建SqlSession的方法传递ExecutorType类型参数如SqlSession openSession(ExecutorType execType)。
配置默认的执行器。SIMPLE 就是普通的执行器REUSE 执行器会重用预处理语句prepared statements BATCH 执行器将重用语句并执行批量更新。
Mybatis是否支持延迟加载如果支持它的实现原理是什么 Mybatis仅支持association关联对象和collection关联集合对象的延迟加载association指的就是一对一collection指的就是一对多查询。在Mybatis配置文件中可以配置是否启用延迟加载lazyLoadingEnabledtrue|false。
它的原理是使用CGLIB创建目标对象的代理对象当调用目标方法时进入拦截器方法比如调用a.getB().getName()拦截器invoke()方法发现a.getB()是null值那么就会单独发送事先保存好的查询关联B对象的sql把B查询上来然后调用a.setB(b)于是a的对象b属性就有值了接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。
当然了不光是Mybatis几乎所有的包括Hibernate支持延迟加载的原理都是一样的。
映射器
#{}和${}的区别
#{}是占位符预编译处理${}是拼接符字符串替换没有预编译处理。
Mybatis在处理**#{}时#{}传入参数是以字符串传入会将SQL中的#{}替换为?号调用PreparedStatement**的set方法来赋值。
Mybatis在处理时 是 原 值 传 入 就 是 把 {}时是原值传入就是把时是原值传入就是把{}替换成变量的值相当于JDBC中的Statement编译
变量替换后#{} 对应的变量自动加上单引号 ‘’变量替换后${} 对应的变量不会加上单引号 ‘’
#{} 可以有效的防止SQL注入提高系统安全性${} 不能防止SQL 注入
#{} 的变量替换是在DBMS 中${} 的变量替换是在 DBMS 外
模糊查询like语句该怎么写 1’%${question}%’ 可能引起SQL注入不推荐
2“%”#{question}“%” 注意因为#{…}解析成sql语句时候会在变量外侧自动加单引号’ 所以这里 % 需要使用双引号 不能使用单引号 ’ 不然会查不到任何结果。
3CONCAT(’%’,#{question},’%’) 使用CONCAT()函数推荐
4使用bind标签
select idlistUserLikeUsername resultTypecom.jourwon.pojo.Userbind namepattern value% username % /select id,sex,age,username,password from person where username LIKE #{pattern}
/select在mapper中如何传递多个参数
方法1顺序传参法
public User selectUser(String name, int deptId);select idselectUser resultMapUserResultMapselect * from userwhere user_name #{0} and dept_id #{1}
/select
#{}里面的数字代表传入参数的顺序。
这种方法不建议使用sql层表达不直观且一旦顺序调整容易出错。
方法2Param注解传参法
public User selectUser(Param(userName) String name, int Param(deptId) deptId);select idselectUser resultMapUserResultMapselect * from userwhere user_name #{userName} and dept_id #{deptId}
/select
#{}里面的名称对应的是注解Param括号里面修饰的名称。
这种方法在参数不多的情况还是比较直观的推荐使用。
方法3Map传参法
public User selectUser(MapString, Object params);select idselectUser parameterTypejava.util.Map resultMapUserResultMapselect * from userwhere user_name #{userName} and dept_id #{deptId}
/select
#{}里面的名称对应的是Map里面的key名称。
这种方法适合传递多个参数且参数易变能灵活传递的情况。
方法4Java Bean传参法
public User selectUser(User user);select idselectUser parameterTypecom.jourwon.pojo.User resultMapUserResultMapselect * from userwhere user_name #{userName} and dept_id #{deptId}
/select#{}里面的名称对应的是User类里面的成员属性。
这种方法直观需要建一个实体类扩展不容易需要加属性但代码可读性强业务逻辑处理方便推荐使用。
Mybatis如何执行批量操作
使用foreach标签
foreach的主要用在构建in条件中它可以在SQL语句中进行迭代一个集合。foreach标签的属性主要有itemindexcollectionopenseparatorclose。 item 表示集合中每一个元素进行迭代时的别名随便起的变量名 index 指定一个名字用于表示在迭代过程中每次迭代到的位置不常用 open 表示该语句以什么开始常用“(” separator表示在每次进行迭代之间以什么符号作为分隔符常用“,” close 表示以什么结束常用“)”。 在使用foreach的时候最关键的也是最容易出错的就是collection属性该属性是必须指定的但是在不同情况下该属性的值是不一样的主要有一下3种情况
如果传入的是单参数且参数类型是一个List的时候collection属性值为list如果传入的是单参数且参数类型是一个array数组的时候collection的属性值为array如果传入的参数是多个的时候我们就需要把它们封装成一个Map了当然单参数也可以封装成map实际上如果你在传入参数的时候在MyBatis里面也是会把它封装成一个Map的map的key就是参数名所以这个时候collection属性值就是传入的List或array对象在自己封装的map里面的key 具体用法如下
!-- 批量保存(foreach插入多条数据两种方法)int addEmpsBatch(Param(emps) ListEmployee emps); --
!-- MySQL下批量保存可以foreach遍历 mysql支持values(),(),()语法 -- //推荐使用
insert idaddEmpsBatchINSERT INTO emp(ename,gender,email,did)VALUESforeach collectionemps itememp separator,(#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id})/foreach
/insert
!-- 这种方式需要数据库连接属性allowMutiQueriestrue的支持如jdbc.urljdbc:mysql://localhost:3306/mybatis?allowMultiQueriestrue --
insert idaddEmpsBatchforeach collectionemps itememp separator; INSERT INTO emp(ename,gender,email,did)VALUES(#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id})/foreach
/insert
使用ExecutorType.BATCH
Mybatis内置的ExecutorType有3种默认为simple,该模式下它为每个语句的执行创建一个新的预处理语句单条提交sql而batch模式重复使用已经预处理的语句并且批量执行所有更新语句显然batch性能将更优 但batch模式也有自己的问题比如在Insert操作时在事务没有提交之前是没有办法获取到自增的id这在某型情形下是不符合业务要求的
具体用法如下
//批量保存方法测试
Test
public void testBatch() throws IOException{SqlSessionFactory sqlSessionFactory getSqlSessionFactory();//可以执行批量操作的sqlSessionSqlSession openSession sqlSessionFactory.openSession(ExecutorType.BATCH);//批量保存执行前时间long start System.currentTimeMillis();try {EmployeeMapper mapper openSession.getMapper(EmployeeMapper.class);for (int i 0; i 1000; i) {mapper.addEmp(new Employee(UUID.randomUUID().toString().substring(0, 5), b, 1));}openSession.commit();long end System.currentTimeMillis();//批量保存执行后的时间System.out.println(执行时长 (end - start));//批量 预编译sql一次》设置参数》10000次》执行1次 677//非批量 预编译设置参数执行 》10000次 1121} finally {openSession.close();}
}
mapper和mapper.xml如下
public interface EmployeeMapper { //批量保存员工Long addEmp(Employee employee);
}mapper namespacecom.jourwon.mapper.EmployeeMapper!--批量保存员工 --insert idaddEmpinsert into employee(lastName,email,gender)values(#{lastName},#{email},#{gender})/insert
/mapper如何获取生成的主键
对于支持主键自增的数据库MySQL
insert idinsertUser useGeneratedKeystrue keyPropertyuserId insert into user( user_name, user_password, create_time) values(#{userName}, #{userPassword} , #{createTime, jdbcType TIMESTAMP})
/insertparameterType 可以不写Mybatis可以推断出传入的数据类型。如果想要访问主键那么应当parameterType 应当是java实体或者Map。这样数据在插入之后 可以通过ava实体或者Map 来获取主键值。通过 getUserId获取主键
不支持主键自增的数据库Oracle
对于像Oracle这样的数据没有提供主键自增的功能而是使用序列的方式获取自增主键。 可以使用selectKey标签来获取主键的值这种方式不仅适用于不提供主键自增功能的数据库也适用于提供主键自增功能的数据库 selectKey一般的用法
selectKey keyColumnid resultTypelong keyPropertyid orderBEFORE
/selectKey insert idinsertUser selectKey keyColumnid resultTypelong keyPropertyuserId orderBEFORESELECT USER_ID.nextval as id from dual /selectKey insert into user( user_id,user_name, user_password, create_time) values(#{userId},#{userName}, #{userPassword} , #{createTime, jdbcType TIMESTAMP})
/insert此时会将Oracle生成的主键值赋予userId变量。这个userId 就是USER对象的属性这样就可以将生成的主键值返回了。如果仅仅是在insert语句中使用但是不返回此时keyProperty“任意自定义变量名”resultType 可以不写。 Oracle 数据库中的值要设置为 BEFORE 这是因为 Oracle中需要先从序列获取值然后将值作为主键插入到数据库中。
扩展 如果Mysql 使用selectKey的方式获取主键需要注意下面两点
order AFTER 获取递增主键值 SELECT LAST_INSERT_ID()
当实体类中的属性名和表中的字段名不一样 怎么办 第1种 通过在查询的SQL语句中定义字段名的别名让字段名的别名和实体类的属性名一致。
select idgetOrder parameterTypeint resultTypecom.jourwon.pojo.Orderselect order_id id, order_no orderno ,order_price price form orders where order_id#{id};
/select第2种 通过resultMap来映射字段名和实体类属性名的一一对应的关系。
select idgetOrder parameterTypeint resultMaporderResultMapselect * from orders where order_id#{id}
/selectresultMap typecom.jourwon.pojo.Order idorderResultMap!–用id属性来映射主键字段–id propertyid columnorder_id!–用result属性来映射非主键字段property为实体类属性名column为数据库表中的属性–result property orderno column order_no/result propertyprice columnorder_price /
/reslutMapMapper 编写有哪几种方式
第一种接口实现类继承 SqlSessionDaoSupport使用此种方法需要编写mapper 接口mapper 接口实现类、mapper.xml 文件。
1在 sqlMapConfig.xml 中配置 mapper.xml 的位置
mappersmapper resourcemapper.xml 文件的地址 /mapper resourcemapper.xml 文件的地址 /
/mappers2定义 mapper 接口
3实现类集成 SqlSessionDaoSupport
mapper 方法中可以 this.getSqlSession()进行数据增删改查。
4spring 配置
bean id classmapper 接口的实现property namesqlSessionFactoryrefsqlSessionFactory/property
/bean第二种使用 org.mybatis.spring.mapper.MapperFactoryBean
1在 sqlMapConfig.xml 中配置 mapper.xml 的位置如果 mapper.xml 和mappre 接口的名称相同且在同一个目录这里可以不用配置
mappersmapper resourcemapper.xml 文件的地址 /mapper resourcemapper.xml 文件的地址 /
/mappers2定义 mapper 接口
3mapper.xml 中的 namespace 为 mapper 接口的地址
4mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致
5Spring 中定义
bean id classorg.mybatis.spring.mapper.MapperFactoryBeanproperty namemapperInterface valuemapper 接口地址 /property namesqlSessionFactory refsqlSessionFactory /
/bean第三种使用 mapper 扫描器
1mapper.xml 文件编写
mapper.xml 中的 namespace 为 mapper 接口的地址
mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致
如果将 mapper.xml 和 mapper 接口的名称保持一致则不用在 sqlMapConfig.xml中进行配置。
2定义 mapper 接口
注意 mapper.xml 的文件名和 mapper 的接口名称保持一致且放在同一个目录
3配置 mapper 扫描器
bean classorg.mybatis.spring.mapper.MapperScannerConfigurerproperty namebasePackage valuemapper 接口包地址/propertyproperty namesqlSessionFactoryBeanNamevaluesqlSessionFactory/
/bean4使用扫描器后从 spring 容器中获取 mapper 的实现对象。
什么是MyBatis的接口绑定有哪些实现方式 接口绑定就是在MyBatis中任意定义接口然后把接口里面的方法和SQL语句绑定我们直接调用接口方法就可以这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置。
接口绑定有两种实现方式
通过注解绑定就是在接口的方法上面加上 Select、Update等注解里面包含Sql语句来绑定
通过xml里面写SQL来绑定 在这种情况下要指定xml映射文件里面的namespace必须为接口的全路径名。当Sql语句比较简单时候用注解绑定 当SQL语句比较复杂时候用xml绑定一般用xml绑定的比较多。
使用MyBatis的mapper接口调用时有哪些要求
1、Mapper接口方法名和mapper.xml中定义的每个sql的id相同。
2、Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同。
3、Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同。
4、Mapper.xml文件中的namespace即是mapper接口的类路径。
最佳实践中通常一个Xml映射文件都会写一个Dao接口与之对应请问这个Dao接口的工作原理是什么Dao接口里的方法参数不同时方法能重载吗 Dao接口就是人们常说的Mapper接口接口的全限名就是映射文件中的namespace的值接口的方法名就是映射文件中MappedStatement的id值接口方法内的参数就是传递给sql的参数。Mapper接口是没有实现类的当调用接口方法时接口全限名方法名拼接字符串作为key值可唯一定位一个MappedStatement举例com.mybatis3.mappers.StudentDao.findStudentById可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面id findStudentById的MappedStatement。在Mybatis中每一个、、、标签都会被解析为一个MappedStatement对象。
Dao接口里的方法是不能重载的因为是全限名方法名的保存和寻找策略。
Dao接口的工作原理是JDK动态代理Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象代理对象proxy会拦截接口方法转而执行MappedStatement所代表的sql然后将sql执行结果返回。
Mybatis的Xml映射文件中不同的Xml映射文件id是否可以重复
不同的Xml映射文件如果配置了namespace那么id可以重复如果没有配置namespace那么id不能重复毕竟namespace不是必须的只是最佳实践而已。
原因就是namespaceid是作为MapString, MappedStatement的key使用的如果没有namespace就剩下id那么id重复会导致数据互相覆盖。有了namespace自然id就可以重复namespace不同namespaceid自然也就不同。
简述Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系
答Mybatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。在Xml映射文件中标签会被解析为ParameterMap对象其每个子元素会被解析为ParameterMapping对象。标签会被解析为ResultMap对象其每个子元素会被解析为ResultMapping对象。每一个、、、标签均会被解析为MappedStatement对象标签内的sql会被解析为BoundSql对象。
Mybatis是如何将sql执行结果封装为目标对象并返回的都有哪些映射形式
第一种是使用标签逐一定义列名和对象属性名之间的映射关系。
第二种是使用sql列的别名功能将列别名书写为对象属性名比如T_NAME AS NAME对象属性名一般是name小写但是列名不区分大小写Mybatis会忽略列名大小写智能找到与之对应对象属性名你甚至可以写成T_NAME AS NaMeMybatis一样可以正常工作。
有了列名与属性名的映射关系后Mybatis通过反射创建对象同时使用反射给对象的属性逐一赋值并返回那些找不到映射关系的属性是无法完成赋值的。
Xml映射文件中除了常见的select|insert|updae|delete标签之外还有哪些标签
还有很多其他的标签、、、、加上动态sql的9个标签trim|where|set|foreach|if|choose|when|otherwise|bind等其中为sql片段标签通过标签引入sql片段为不支持自增的主键生成策略标签。
Mybatis映射文件中如果A标签通过include引用了B标签的内容请问B标签能否定义在A标签的后面还是说必须定义在A标签的前面
虽然Mybatis解析Xml映射文件是按照顺序解析的但是被引用的B标签依然可以定义在任何地方Mybatis都可以正确识别。
原理是Mybatis解析A标签发现A标签引用了B标签但是B标签尚未解析到尚不存在此时Mybatis会将A标签标记为未解析状态然后继续解析余下的标签包含B标签待所有标签解析完毕Mybatis会重新解析那些被标记为未解析的标签此时再解析A标签时B标签已经存在A标签也就可以正常解析完成了。
高级查询
MyBatis实现一对一一对多有几种方式怎么操作的
有联合查询和嵌套查询。联合查询是几个表联合查询只查询一次通过在resultMap里面的associationcollection节点配置一对一一对多的类就可以完成
嵌套查询是先查一个表根据这个表里面的结果的外键id去再另外一个表里面查询数据也是通过配置associationcollection但另外一个表的查询通过select节点配置。
Mybatis是否可以映射Enum枚举类
Mybatis可以映射枚举类不单可以映射枚举类Mybatis可以映射任何对象到表的一列上。映射方式为自定义一个TypeHandler实现TypeHandler的setParameter() 和 getResult() 接口方法。
TypeHandler有两个作用一是完成从javaType至jdbcType的转换二是完成jdbcType至javaType的转换体现为setParameter() 和 getResult()两个方法分别代表设置sql问号占位符参数和获取列查询结果。
动态SQL
Mybatis动态sql是做什么的都有哪些动态sql能简述一下动态sql的执行原理不
Mybatis动态sql可以让我们在Xml映射文件内以标签的形式编写动态sql完成逻辑判断和动态拼接sql的功能Mybatis提供了9种动态sql标签trim|where|set|foreach|if|choose|when|otherwise|bind。
其执行原理为使用OGNL从sql参数对象中计算表达式的值根据表达式的值动态拼接sql以此来完成动态sql的功能。
插件模块
Mybatis是如何进行分页的分页插件的原理是什么
Mybatis使用RowBounds对象进行分页它是针对ResultSet结果集执行的内存分页而非物理分页可以在sql内直接书写带有物理分页的参数来完成物理分页功能也可以使用分页插件来完成物理分页。
分页插件的基本原理是使用Mybatis提供的插件接口实现自定义插件在插件的拦截方法内拦截待执行的sql然后重写sql根据dialect方言添加对应的物理分页语句和物理分页参数。
举例select * from student拦截sql后重写为select t.* from (select * from student) t limit 0, 10
简述Mybatis的插件运行原理以及如何编写一个插件。
Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件Mybatis使用JDK的动态代理为需要拦截的接口生成代理对象以实现接口方法拦截功能每当执行这4种接口对象的方法时就会进入拦截方法具体就是InvocationHandler的**invoke()**方法当然只会拦截那些你指定需要拦截的方法。
实现Mybatis的Interceptor接口并复写**intercept()**方法然后在给插件编写注解指定要拦截哪一个接口的哪些方法即可记住别忘了在配置文件中配置你编写的插件。
九、数据库-MySQL
MySQL主从、集群模式简单介绍
主从模式、集群模式都是在一个项目中使用多个mysql节点进行存储和读取数据。
当单机模式部署不满足安全性、高可用、高并发等需求的时候就需要考虑主从模式或者集群模式部署。
1、主从模式 Replication
主从模式或者是叫主从架构、主从复制有以下几种常见方案一主一从、一主多从、多主一从、互为主备、级联复制等。 主数据库必须开启binary log二进制功能因为主从同步所有的操作都是基于二进制文件来完成的。 数据同步模式有
异步模式主库将事务binlog事件写入到binlog文件中此时主库只会通知一下dump线程发送这些新的binlog然后主库就会继续处理提交操作而此时不会保证这些binlog传到任何一个从库节点上。半同步模式主库只需要等待至少一个从库节点收到并且Flush binlog到relay-log文件即可主库不需要等待所有从库给主库反馈。同时这里只是一个收到的反馈而不是已经完全执行并且提交的反馈。全同步模式当主库提交事务之后所有的从库节点必须全部收到APPLY并且提交这些事务然后主库线程才能继续做后续操作。
1、主从复制指的是当主数据库中进行了update、insert、delete操作导致数据发生改变时变化会实时同步到一个或者多个从数据库slave中。
2、默认情况下异步复制、无需维持长连接。
3、通过配置可以选择想要同步的库和表。
2、集群模式
集群最大的优点就是数据实时同步高可用每个节点的数据都是同步一致的不像主从有时会出现数据不一致而高可用任何一个节点宕机都不会影响业务。
集群模式有以下集中常见部署方式
读写分离的集群模式集群中有的节点只进行写入操作有的节点只进行读取操作每个节点的数据都是完全一致的。分片分库分表集群模式集群中所有的节点表结构一致每个节点存储的数据不一样。分片算法主要有两种一种是范围法1-100条数据在节点A101-200条数据在节点B另一种是HASH法对每条数据按照一定的算法分配到不同的存储节点。读写分离和分片模式组合应用先进行分片模式部署然后对每个分片进行读写分离模式部署。 3、主从模式部署注意事项
常用命令执行命令之前stop服务执行完再start
查看主节点状态show master status\G;查看从节点状态show slave status\G;停止同步stop slave;开启同步start slave;修改Master_Log_FileCHANGE MASTER TO MASTER_LOG_FILE‘mysql-bin.000026’, MASTER_LOG_POS0;修改master节点信息CHANGE MASTER TO MASTER_HOST‘192.168.203.141’, MASTER_PORT33060,MASTER_USER‘root’, MASTER_PASSWORD‘123456’;删除当前节点的 binlog 文件PURGE BINARY LOGS TO ‘binlog.000001’;数据的操作日志位置SHOW BINARY LOGS;数据的真实具体位置SHOW GLOBAL VARIABLES LIKE “%datadir%”;
每个节点的slave_sql_running、Slave_IO_Running两个字段都是YES集群状态才正常 主服务器查看主节点状态显示的 File 字段和从服务器查看从节点状态显示的 Master_Log_File 字段必须保持一致。 slave_sql_running为No的话可能是主从库数据不同步可以同步一下数据。
数据导出命令在mysql服务器执行不需要登录数据库
mysqldump -u[用户名] -h[ip] -p[密码] -P[端口号] --databases 数据库名 --tables 表名 导出的文件名.sql
数据库导入命令导入的时候需要指定数据库保证指定的数据库存在
mysqldump -u[用户名] -h[ip] -p[密码] -P[端口号] 导入的文件名.sql
UNION 和 UNION ALL 区别
一、区别1取结果的交集
1、union: 对两个结果集进行并集操作, 不包括重复行,相当于distinct, 同时进行默认规则的排序;
2、union all: 对两个结果集进行并集操作, 包括重复行, 即所有的结果全部显示, 不管是不是重复;
二、区别2获取结果后的操作
1、union: 会对获取的结果进行排序操作
2、union all: 不会对获取的结果进行排序操作
三、总结
union all只是合并查询结果并不会进行去重和排序操作在没有去重的前提下使用union all的执行效率要比union高
分库分表
链接 MySQL-如何分库分表一看就懂_mysql分库分表怎么实现-CSDN博客
一、为什么要分库分表 如果一个网站业务快速发展那这个网站流量也会增加数据的压力也会随之而来比如电商系统来说双十一大促对订单数据压力很大Tps十几万并发量如果传统的架构一主多从主库容量肯定无法满足这么高的Tps业务越来越大单表数据超出了数据库支持的容量持久化磁盘IO,传统的数据库性能瓶颈产品经理业务·必须做改变程序数据库刀子切分优化。数据库连接数不够需要分库表的数据量大优化后查询性能还是很低需要分。
二、什么是分库分表 分库分表方案是对关系型数据库数据存储和访问机制的一种补充。 分库将一个库的数据拆分到多个相同的库中访问的时候访问一个库 分表把一个表的数据放到多个表中操作对应的某个表就行
三、分库分表的几种方式 1.垂直拆分
(1) 数据库垂直拆分 根据业务拆分如图电商系统拆分成订单库会员库商品库
(2)表垂直拆分 根据业务去拆分表如图把user表拆分成user_base表和user_info表use_base负责存储登录user_info负责存储基本用户信息
垂直拆分特点 1.每个库表的结构都不一样 2.每个库表的数据至少一列一样 3.每个库表的并集是全量数据
垂直拆分优缺点
优点 1.拆分后业务清晰专库专用按业务拆分 2.数据维护简单按业务不同业务放到不同机器上
缺点: 1.如果单表的数据量写读压力大 2.受某种业务决定或者被限制也就是说一个业务往往会影响到数据库的瓶颈性能问题如双十一抢购 3.部分业务无法关联join只能通过java程序接口去调用提高了开发复杂度
2、水平拆分
(1) 数据库水平拆分 如图按会员库拆分拆分成会员1库会员2库以userId拆分userId尾号0-5为1库 6-9为2库还有其他方式进行取模偶数放到1库奇数放到2库
(2) 表水平拆分 如图把users表拆分成users1表和users2表以userId拆分进行取模偶数放到users1表奇数放到users2表
水平拆分的其他方式
range来分,每个库一段连续的数据这个一般是按比如时间范围来的但是这种一般较少用因为很容易产生热点问题大量的流量都打在最新的数据上了,优点扩容的时候就很容易因为你只要预备好给每个月都准备一个库就可以了到了一个新的月份的时候自然而然就会写新的库了 缺点大部分的 请求都是访问最新的数据。实际生产用range要看场景你的用户不是仅仅访问最新的数据而是均匀的访问现在的数据以及历史的数据 hash分发,优点可以平均分配每个库的数据量和请求压力 缺点扩容起来比较麻烦会有一个数据迁移的这么一个过程 水平拆分特点 1.每个库表的结构都一样 2.每个库表的数据都不一样 3.每个库表的并集是全量数据
水平拆分优缺点
优点 1.单库/单表的数据保持在一定量减少有助于性能提高 2.提高了系统的稳定性和负载能力 3.拆分表的结构相同程序改造较少。
缺点: 1.数据的扩容很有难度维护量大 2.拆分规则很难抽象出来 3.分片事务的一致性问题部分业务无法关联join只能通过java程序接口去调用
四、分库分表带来的问题 分布式事务 跨库join查询 分布式全局唯一id 开发成本 对程序员要求高
五、分库分表技术如何选型 分库分表的开源框架 jdbc 直连层shardingsphere、tddl proxy 代理层mycatmysql-proxy360
jdbc直连层 jdbc直连层又叫jdbc应用层,是因为所有分片规则所有分片逻辑包括处理分布式事务 所有这些问题它都是在应用层所有项目都是由war包构成的所有分片都写成了jar包放到了war包里面java需要虚拟机去运行的虚拟机运行的时候就会把war包里面的字节文件进行classLoder加载到jvm内存中所有分片逻辑都是基于内存方进行操作的
proxy代理层 如图,proxy代理层所有分片规则所有分片逻辑包括处理分布式事务都在mycat写好了,所有分片逻辑都是基于mycat方进行操作
jdbc直连层和proxy代理层优缺点
jdbc直连层性能高只支持java语言支持跨数据库 proxy代理层开发成本低支持跨语言不支持跨数据库
MySQL有哪些数据类型
1、整数类型**
包括TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT分别表示1字节、2字节、3字节、4字节、8字节整数。任何整数类型都可以加上UNSIGNED属性表示数据是无符号的即非负整数。 长度整数类型可以被指定长度例如INT(11)表示长度为11的INT类型。长度在大多数场景是没有意义的它不会限制值的合法范围只会影响显示字符的个数而且需要和UNSIGNED ZEROFILL属性配合使用才有意义。 例子假定类型设定为INT(5)属性为UNSIGNED ZEROFILL如果用户插入的数据为12的话那么数据库实际存储数据为00012。
2、实数类型**
包括FLOAT、DOUBLE、DECIMAL。 DECIMAL可以用于存储比BIGINT还大的整型能存储精确的小数。 而FLOAT和DOUBLE是有取值范围的并支持使用标准的浮点进行近似计算。 计算时FLOAT和DOUBLE相比DECIMAL效率更高一些DECIMAL你可以理解成是用字符串进行处理。
3、字符串类型**
包括VARCHAR、CHAR、TEXT、BLOB VARCHAR用于存储可变长字符串它比定长类型更节省空间。 VARCHAR使用额外1或2个字节存储字符串长度。列长度小于255字节时使用1字节表示否则使用2字节表示。 VARCHAR存储的内容超出设置的长度时内容会被截断。 CHAR是定长的根据定义的字符串长度分配足够的空间。 CHAR会根据需要使用空格进行填充方便比较。 CHAR适合存储很短的字符串或者所有值都接近同一个长度。 CHAR存储的内容超出设置的长度时内容同样会被截断。
使用策略 对于经常变更的数据来说CHAR比VARCHAR更好因为CHAR不容易产生碎片。 对于非常短的列CHAR比VARCHAR在存储空间上更有效率。 使用时要注意只分配需要的空间更长的列排序时会消耗更多内存。 尽量避免使用TEXT/BLOB类型查询时会使用临时表导致严重的性能开销。
4、枚举类型ENUM**
把不重复的数据存储为一个预定义的集合。 有时可以使用ENUM代替常用的字符串类型。 ENUM存储非常紧凑会把列表值压缩到一个或两个字节。 ENUM在内部存储时其实存的是整数。 尽量避免使用数字作为ENUM枚举的常量因为容易混乱。 排序是按照内部存储的整数
5、日期和时间类型**
尽量使用timestamp空间效率高于datetime 用整数保存时间戳通常不方便处理。 如果需要存储微妙可以使用bigint存储。 看到这里这道真题是不是就比较容易回答了。
MySQL中varchar与char有哪些区别
1、固定长度 可变长度
VARCHAR VARCHAR类型用于存储可变长度字符串是最常见的字符串数据类型。它比固定长度类型更节省空间因为它仅使用必要的空间(根据实际字符串的长度改变存储空间)。 有一种情况例外如果MySQL表使用ROW_FORMATFIXED创建的话每一行都会使用定长存储。
CHAR CHAR类型用于存储固定长度字符串MySQL总是根据定义的字符串长度分配足够的空间。当存储CHAR值时MySQL会删除字符串中的末尾空格(在MySQL 4.1和更老版本中VARCHAR 也是这样实现的——也就是说这些版本中CHAR和VARCHAR在逻辑上是一样的区别只是在存储格式上)。 同时CHAR值会根据需要采用空格进行剩余空间填充以方便比较和检索。但正因为其长度固定所以会占据多余的空间也是一种空间换时间的策略
2、存储方式
VARCHAR VARCHAR需要使用1或2个额外字节记录字符串的长度如果列的最大长度小于或等于255字节则只使用1个字节表示否则使用2个字节。假设采用latinl字符集一个VARCHAR(10)的列需要11个字节的存储空间。VARCHAR(1000)的列则需要1002 个字节因为需要2个字节存储长度信息。
VARCHAR节省了存储空间所以对性能也有帮助。但是由于行是变长的在UPDATE时可能使行变得比原来更长这就导致需要做额外的工作。如果一个行占用的空间增长并且在页内没有更多的空间可以存储在这种情况下不同的存储引擎的处理方式是不一样的。例如MylSAM会将行拆成不同的片段存储InnoDB则需要分裂页来使行可以放进页内。
CHAR CHAR适合存储很短或长度近似的字符串。例如CHAR非常适合存储密码的MD5值因为这是一个定长的值。对于经常变更的数据CHAR也比VARCHAR更好因为定长的CHAR类型不容易产生碎片。对于非常短的列CHAR比VARCHAR在存储空间上也更有效率。例如用CHAR(1)来存储只有Y和N的值如果采用单字节字符集只需要一个字节但是VARCHAR(1)却需要两个字节因为还有一个记录长度的额外字节。
3、存储容量
CHAR 对于char类型来说最多只能存放的字符个数为255和编码无关任何编码最大容量都是255。
VARCHAR MySQL行默认最大65535字节是所有列共享相加的所以VARCHAR的最大值受此限制。
表中只有单列字段情况下varchar一般最多能存放(65535 - 3)个字节varchar的最大有效长度通过最大行数据长度和使用的字符集来确定通常的最大长度是65532个字符当字符串中的字符都只占1个字节时能达到65532个字符
为什么是65532个字符算法如下有余数时向下取整
最大长度(字符数) 行存储最大字节数 - NULL标识列占用字节数 - 长度标识字节数 / 字符集单字符最大字节数
NULL标识列占用字节数允许NULL时占一字节 长度标识字节数记录长度的标识长度小于等于25528时占1字节小于65535时216,占2字节 VARCHAR类型在4.1和5.0版本发生了很大的变化使得情况更加复杂。从MySQL 4.1开始每个字符串列可以定义自己的字符集和排序规则。这些东西会很大程度上影响性能。
4.0版本及以下MySQL中varchar长度是按字节展示如varchar(20)指的是20字节 5.0版本及以上MySQL中varchar长度是按字符展示。如varchar(20)指的是20字符。 当然行总长度还是65535字节而字符和字节的换算则与编码方式有关不同的字符所占的字节是不同的。编码划分如下
GBK编码 一个英文字符占一个字节中文2字节单字符最大可占用2个字节。
UTF-8编码 一个英文字符占一个字节中文3字节单字符最大可占用3个字节。
utf8mb4编码 一个英文字符占一个字节中文3字节单字符最大占4个字节如emoji表情4字节。
假设当前还有6字节可以存放字符按单字符占用最大字节数来算可以存放3个GBK、或2个utf8、或1个utf8mb4。
Mysql的索引和主键的区别
1、主键一定是唯一性的索引唯一性的索引不一定就是主键。
主键就是能够唯一标识表中某一行的属性或者是属性组一个表只能有一个主键但可以有多个候选索引。因为主键可以唯一标识一行记录所以可以确保执行数据更新、删除的时候不会出现错误的。主键还经常和外键构成参照完整性约束防止出现数据不一致。数据库管理系统对于主键自动生成唯一索引所以主键也是一个特殊的索引。
2、一个表中可以有多个唯一索引但是主键只能有一个。
3、主键列不允许为空值而唯一性索引列允许空值。
4、主键也可以由多个字段组成组成复合主键同时主键也是唯一索引。
5、唯一索引表示索引值唯一可以由一个或者几个字段组成一个表可以由多个唯一索引。
数据库基础知识
为什么要使用数据库 数据保存在内存
优点 存取速度快
缺点 数据不能永久保存
数据保存在文件
优点 数据永久保存
缺点1速度比内存操作慢频繁的IO操作。2查询数据不方便
数据保存在数据库
1数据永久保存
2使用SQL语句查询方便效率高。
3管理数据方便
什么是SQL 结构化查询语言(Structured Query Language)简称SQL是一种数据库查询语言。
作用用于存取数据、查询、更新和管理关系数据库系统。
什么是MySQL? MySQL是一个关系型数据库管理系统由瑞典MySQL AB 公司开发属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一在 WEB 应用方面MySQL是最好的 RDBMS (Relational Database Management System关系数据库管理系统) 应用软件之一。在Java企业级开发中非常常用因为 MySQL 是开源免费的并且方便扩展。
数据库的事务的基本特性
事务是并发控制的基本单位保证事务ACID的特性是事务处理的重要任务而并发操作有可能会破坏其ACID特性。
所以事务是针对并发而言的即 对 数据 在并发操作时保驾护航。
**原子性Atomicity **
**原子性**在我理解看来是事务中各项操作要么全部成功要么全部失败。很有江湖义气一说同生共死。
一致性Consistency
**一致性**我理解的是更侧重结果事务结束后系统状态是一致的。
隔离性Isolation
隔离性并发执行的事务彼此无法看到对方的中间状态。
持久性 Durability
持久性当事务完成后它对于数据的改变是永久性的即使出现致命的系统故障也将一直保持。
在实际生产应用中 针对 事务的隔离性 又划分出了几种隔离级别 并发事务处理带来的问题
更新丢失
当两个或多个事务选择同一行然后基于最初选定的值更新该行时由于每个事务都不知道其他事务的存在就会发生丢失更新问题–最后的更新覆盖了由其他事务所做的更新
脏读在一个事务处理过程中读取了另一个未提交事务中的数据。 解读两个事务 A 和 B首先 A 事务对 数据 a 执行加 500 的操作 a 1500此时 B 事务读取数据 a 的值 1500后 A 事务 又对数据 a 执行减500 的操作 a 1000 A 事务 commit 。
不可重复读事务 A 多次读取同一数据事务 B 在事务A多次读取的过程中对数据作了更新并提交导致事务A多次读取同一数据时结果 不一致。
解读两个事务 A 和 B首先 A 事务对 数据 a 进行查询 a 1000此时 B 事务对数据 a 500 操作并提交事。后 A 事务 又对 数据 a 进行查询 a 1500 。
幻读事务 A 将数据库中所有数据类型从默认的 true 改成 false但是事务 B 就在这个时候插入了一条新记录当事务 A改结束后发现还有一条记录没有改过来就好像发生了幻觉一样这就叫幻读。
小结不可重复读的和幻读很容易混淆不可重复读侧重于修改幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行解决幻读需要锁表。
事务的四大特性(ACID)
4.1、原子性Atomicity
原子性是指事务是一个不可分割的工作单位事务中的操作要么全部成功要么全部失败。比如在同一个事务中的SQL语句要么全部执行成功要么全部执行失败
4.2、一致性Consistency
官网上事务一致性的概念是事务必须使数据库从一个一致性状态变换到另外一个一致性状态。以转账为例子A向B转账假设转账之前这两个用户的钱加起来总共是2000那么A向B转账之后不管这两个账户怎么转A用户的钱和B用户的钱加起来的总额还是2000这个就是事务的一致性。
4.3、隔离性Isolation
事务的隔离性是多个用户并发访问数据库时数据库为每一个用户开启的事务不能被其他事务的操作数据所干扰多个并发事务之间要相互隔离。
4.4、持久性Durability
持久性是指一个事务一旦被提交它对数据库中数据的改变就是永久性的接下来即使数据库发生故障也不应该对其有任何影响
事务的四大特性中最麻烦的是隔离性下面重点介绍一下事务的隔离级别
事务的隔离级别
多个线程开启各自事务操作数据库中数据时数据库系统要负责隔离操作以保证各个线程在获取数据时的准确性。
5.1、事务不考虑隔离性可能会引发的问题
如果事务不考虑隔离性可能会引发如下问题
1、脏读
脏读指一个事务读取了另外一个事务未提交的数据。
这是非常危险的假设向转帐100元对应sql语句如下所示 1.update account set moneymoney100 where name‘B’; 2.update account set moneymoney-100 where name‘A’; 当第1条sql执行完第2条还没执行(A未提交时)如果此时查询自己的帐户就会发现自己多了100元钱。如果A等B走后再回滚B就会损失100元。
2、不可重复读
不可重复读指在一个事务内读取表中的某一行数据多次读取结果不同。 例如银行想查询A帐户余额第一次查询A帐户为200元此时A向帐户内存了100元并提交了银行接着又进行了一次查询此时A帐户为300元了。银行两次查询不一致可能就会很困惑不知道哪次查询是准的。 不可重复读和脏读的区别是脏读是读取前一事务未提交的脏数据不可重复读是重新读取了前一事务已提交的数据。 很多人认为这种情况就对了无须困惑当然是后面的为准。我们可以考虑这样一种情况比如银行程序需要将查询结果分别输出到电脑屏幕和写到文件中结果在一个事务中针对输出的目的地进行的两次查询不一致导致文件和屏幕中的结果不一致银行工作人员就不知道以哪个为准了。
3、虚读(幻读)
虚读(幻读)是指在一个事务内读取到了别的事务插入的数据导致前后读取不一致。 如丙存款100元未提交这时银行做报表统计account表中所有用户的总额为500元然后丙提交了这时银行再统计发现帐户为600元了造成虚读同样会使银行不知所措到底以哪个为准。
5.2、事务隔离性的设置语句
MySQL数据库共定义了四种隔离级别
Serializable(串行化)可避免 **脏读、不可重复读、虚读幻读**情况的发生。Repeatable read(可重复读) - 默认的隔离级别可避免脏读、不可重复读情况的发生。Read committed(读已提交)可避免 脏读情况发生。Read uncommitted(读未提交)最低级别以上情况均无法保证。
mysql数据库查询当前事务隔离级别select tx_isolation
*例如* mysql数据库默认的事务隔离级别是Repeatable read(可重复读)
mysql数据库设置事务隔离级别set transaction isolation level 隔离级别名
在java代码中使用
Connection connect JdbcUtils.getConnection();//获取当前数据库的隔离级别System.out.println(connect.getTransactionIsolation());//设置数据库的隔离级别connect.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);//取消事务的自动提交connect.setAutoCommit(false);事务隔离级别如何在java代码中使用
链接【JDBC】ACID、四种隔离级别与Java代码实现【附源码】_java代码如何查看oracle当前事务隔离级别-CSDN博客
用户AA为用户BB转账如果未考虑事务可能会导致数据的不一致状态。 1未考虑事务之前的转账操作 此时若没有异常则转账会成功若有异常出现转账操作就会在还没成功之前被迫结束导致不一致
//未考虑事务的转账操作用户AA给用户BB转账100
Test
public void test01(){String sql update user_table set balance balance - 100 where user ? ;updateTable(sql,AA);
// System.out.println(10/0);//模拟转账过程中出现的异常String sql1 update user_table set balance balance 100 where user ? ;updateTable(sql1,BB);
}
public void updateTable(String sql,Object...args) {Connection connect null;PreparedStatement ps null;try {//1.获取数据库的连接connect JdbcUtils.getConnection();//2.预编译sql语句返回prepareStatement的实例ps connect.prepareStatement(sql);//3.填充占位符for(int i0;iargs.length;i){ps.setObject(i1,args[i]);}//4.执行sqlps.execute();} catch (Exception e) {e.printStackTrace();} finally {//5.关闭资源JdbcUtils.closeConnection(ps,connect);}
}2考虑事务之后的转账操作 我们取消数据库的默认提交操作当转账操作成功结束时对结果手动进行提交若转账未结束时出现异常我们可以对转账事务进行回滚操作不管成功与否数据总是一致的
public void updateTable(Connection connect, String sql,Object...args) {PreparedStatement ps null;try {//1.预编译sql语句返回prepareStatement的实例ps connect.prepareStatement(sql);//2.填充占位符for(int i0;iargs.length;i){ps.setObject(i1,args[i]);}//3.执行sqlps.execute();} catch (Exception e) {e.printStackTrace();} finally {//4.关闭资源JdbcUtils.closeConnection(ps,null);}
}
//考虑事务之后的转账操作用户AA给用户BB转账100
Test
public void test02(){Connection connect null;try {connect JdbcUtils.getConnection();String sql update user_table set balance balance - 100 where user ? ;//1.取消事务的自动提交connect.setAutoCommit(false);updateTable(connect,sql,AA);System.out.println(10/0);//模拟事务处理过程中出现的异常String sql1 update user_table set balance balance 100 where user ? ;updateTable(connect,sql1,BB);//2.事务正常结束后的提交connect.commit();} catch (Exception e) {e.printStackTrace();try {//3.事务操作过程中出现异常时的回滚操作connect.rollback();} catch (SQLException throwables) {throwables.printStackTrace();}} finally {if (connect!null){try {connect.close();} catch (SQLException throwables) {throwables.printStackTrace();}}}
}
若此时Connection没有被关闭还可能被重复使用则需要恢复其自动提交状态主要针对数据库连接池时的操作。 setAutoCommit(true)。尤其是在使用数据库连接池技术时执行close()方法前建议恢复自动提交状态。
Java代码演示及隔离级别的设置
数据库中事务的隔离级别设置结束后若重启数据库的服务则所有的数据库设置都会变成默认。
/*** author wds* date 2021-12-14 9:38*/
public class TransactionTest01 {//模拟事务A对数据库中数据的查询操作Testpublic void testTransactionSelect() throws Exception{Connection connect JdbcUtils.getConnection();//获取当前数据库的隔离级别System.out.println(connect.getTransactionIsolation());//设置数据库的隔离级别connect.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);//取消事务的自动提交connect.setAutoCommit(false);String sql select user,password,balance from user_table where user ?;PreparedStatement ps connect.prepareStatement(sql);User user queryForTable(connect, User.class, sql, CC);System.out.println(user);System.out.println(connect.getTransactionIsolation());}//模拟事务B对数据库中数据的修改操作Testpublic void testTransactionUpdate() throws Exception {Connection connect JdbcUtils.getConnection();//取消事务的自动提交connect.setAutoCommit(false);String sql update user_table set balance ? where user ?;updateTable(connect,sql,50000,CC);//线程休眠15秒Thread.sleep(15000);System.out.println(修改结束...);}//考虑事务之后的通用的增删改操作public void updateTable(Connection connect, String sql,Object...args) {PreparedStatement ps null;try {//1.预编译sql语句返回prepareStatement的实例ps connect.prepareStatement(sql);//2.填充占位符for(int i0;iargs.length;i){ps.setObject(i1,args[i]);}//3.执行sqlps.execute();} catch (Exception e) {e.printStackTrace();} finally {//4.关闭资源JdbcUtils.closeConnection(ps,null);}}//考虑事务之后不同数据表的通用的单行查询操作public T T queryForTable(Connection connect, ClassT clazz, String sql,Object...args) {PreparedStatement ps null;ResultSet resultSet null;try {//1.预编译Sqlps connect.prepareStatement(sql);for(int i0;iargs.length;i){ps.setObject(i1,args[i]);}//2.执行sql操作resultSet ps.executeQuery();//查询结果集的元数据ResultSetMetaData metaData resultSet.getMetaData();//查询结果集的列数int columnCount metaData.getColumnCount();if(resultSet.next()){//newInstance()只能调用无参构造方法创建当前类的对象T t clazz.newInstance();//返回结果集中的每一个列for(int i0;icolumnCount;i){//获取每个列的列值通过resultSetObject columnValue resultSet.getObject(i 1);//通过ResultSetMetaData//获取每个列的列名String columnName metaData.getColumnName(i 1);//获取每个列的别名String columnLabel metaData.getColumnLabel(i 1);//通过反射将对象指定名getColumnName的属性设置为指定的属性值columnValueField field clazz.getDeclaredField(columnLabel);field.setAccessible(true);field.set(t,columnValue);}return t;}} catch (Exception e) {e.printStackTrace();} finally {JdbcUtils.closeConnection(ps,null,resultSet);}return null;}
}5.3、使用MySQL数据库演示不同隔离级别下的并发问题
同时打开两个窗口模拟2个用户并发访问数据库
1、当把事务的隔离级别设置为read uncommitted时会引发脏读、不可重复读和虚读
A窗口 set transaction isolation level read uncommitted; --设置A用户的数据库隔离级别为Read uncommitted(读未提交) start transaction; --开启事务 select * from account; --查询A账户中现有的钱转到B窗口进行操作 select * from account --发现a多了100元这时候A读到了B未提交的数据脏读
B窗口 start transaction; --开启事务 update account set moneymoney100 where name‘A’;–不要提交转到A窗口查询
2、当把事务的隔离级别设置为read committed时会引发不可重复读和虚读但避免了脏读
A窗口 set transaction isolation level read committed; start transaction; select * from account;–发现a帐户是1000元转到b窗口 select * from account;–发现a帐户多了100,这时候a读到了别的事务提交的数据两次读取a帐户读到的是不同的结果不可重复读 B窗口 start transaction; update account set moneymoney100 where name‘aaa’; commit;–转到a窗口
3、当把事务的隔离级别设置为repeatable read(mysql默认级别)时会引发虚读但避免了脏读、不可重复读
A窗口 set transaction isolation level repeatable read; start transaction; select * from account;–发现表有4个记录转到b窗口 select * from account;–可能发现表有5条记录这时候发生了a读取到另外一个事务插入的数据虚读 B窗口 start transaction; insert into account(name,money) values(‘ggg’,1000); commit;–转到a窗口
4、当把事务的隔离级别设置为Serializable时会避免所有问题
A窗口 set transaction isolation level Serializable; start transaction; select * from account; --转到b窗口
B窗口 start transaction; insert into account(name,money) values(‘ggg’,1000); --发现不能插入只能等待a结束事务才能插入
数据库三大范式是什么 第一范式每个列都不可以再拆分。
第二范式在第一范式的基础上非主键列完全依赖于主键而不能是依赖于主键的一部分。
第三范式在第二范式的基础上非主键列只依赖于主键不依赖于其他非主键。
在设计数据库结构的时候要尽量遵守三范式如果不遵守必须有足够的理由。比如性能。事实上我们经常会为了性能而妥协数据库的设计。
mysql有关权限的表都有哪几个
MySQL服务器通过权限表来控制用户对数据库的访问权限表存放在mysql数据库里由mysql_install_db脚本初始化。这些权限表分别userdbtable_privcolumns_priv和host。下面分别介绍一下这些表的结构和内容
user权限表记录允许连接到服务器的用户帐号信息里面的权限是全局级的。 db权限表记录各个帐号在各个数据库上的操作权限。 table_priv权限表记录数据表级的操作权限。 columns_priv权限表记录数据列级的操作权限。 host权限表配合db权限表对给定主机上数据库级操作权限作更细致的控制。这个权限表不受GRANT和REVOKE语句的影响。
MySQL的binlog有有几种录入格式分别有什么区别
有三种格式statementrow和mixed。
statement模式下每一条会修改数据的sql都会记录在binlog中。不需要记录每一行的变化减少了binlog日志量节约了IO提高性能。由于sql的执行是有上下文的因此在保存的时候需要保存相关的信息同时还有一些使用了函数之类的语句无法被记录复制。
row 级别下不记录sql语句上下文相关信息仅保存哪条记录被修改。记录单元为每一行的改动基本是可以全部记下来但是由于很多操作会导致大量行的改动(比如alter table)因此这种模式的文件保存的信息太多日志量太大。
mixed一种折中的方案普通操作使用statement记录当无法使用statement的时候使用row。 此外新版的MySQL中对row级别也做了一些优化当表结构发生变化的时候会记录语句而不是逐行记录。
数据类型
MySQL存储引擎MyISAM与InnoDB区别
存储引擎Storage engineMySQL中的数据、索引以及其他对象是如何存储的是一套文件系统的实现。
常用的存储引擎有以下
Innodb引擎Innodb引擎提供了对数据库ACID事务的支持。并且还提供了行级锁和外键的约束。它的设计的目标就是处理大数据容量的数据库系统。 MyIASM引擎(原本Mysql的默认引擎)不提供事务的支持也不支持行级锁和外键。 MEMORY引擎所有的数据都在内存中数据的处理速度快但是安全性不高。
MyISAM索引与InnoDB索引的区别 InnoDB索引是聚簇索引MyISAM索引是非聚簇索引。 InnoDB的主键索引的叶子节点存储着行数据因此主键索引非常高效。 MyISAM索引的叶子节点存储的是行数据地址需要再寻址一次才能得到数据。 InnoDB非主键索引的叶子节点存储的是主键和其他带索引的列数据因此查询时做到覆盖索引会非常高效。 InnoDB引擎的4大特性 插入缓冲insert buffer)
二次写(double write)
自适应哈希索引(ahi)
预读(read ahead)
存储引擎选择 如果没有特别的需求使用默认的Innodb即可。
MyISAM以读写插入为主的应用程序比如博客系统、新闻门户网站。
Innodb更新删除操作频率也高或者要保证数据的完整性并发量高支持事务和外键。比如OA自动化办公系统。
MYsql如何查询到一条数据的简述其查询原理BufferPool缓存机制事务
链接【mysql学习篇】InnoDB存储引擎事务的实现和BufferPool缓存机制详解-CSDN博客 Mysql的InnoDB是如何使用索引的其索引的原理是什么
链接Mysql(InnoDB)索引原理及的使用_innodb 索引空间、索引数据-CSDN博客
索引是存储引擎实现的InnoDB的索引结构是B树大部分的引擎也都支持B树索引 B树索引的优点 层级少2200W的数据只有3层叶子节点存数据指针(利于排序)支持范围查询支持排序(区间指针)索引全部出现在最下层即便是他们作为了上层分支
InnoDB到底支持Hash索引吗 InnoDB在特定条件下会自适应Hash索引用户不能手动创建可以理解为“索引的索引”可以加快索引查询速度
相比B树的优缺点
不支持范围查询但指定查询效率更高(一般只需一次检索) 哈希冲突问题使用链表解决 无法排序哈希值计算结果没有顺序关系
MySQL-索引innoDB,B树索引
链接 (5条消息) MySQL索引常见面试题2022版_mysql索引优化_未来很长别只看眼前的博客-CSDN博客
链接2 (6条消息) 数据库MySQL-索引含常见面试题_mysql索引面试题_懒羊羊z的博客-CSDN博客
MySQL 索引失效如何解决
以下是索引失效的原因 为什么要建立索引
当在非常大的表中进行查询如果数据库进行全表遍历的话那么速度是会非常慢的而我们的索引则可以建立一个b树的结构可以自上而下的去进行查询有点像二分查找可以在一定程度避免走全表查询这样查询的速度是非常快的
①一般情况下扫描索引的速度是远远大于扫描全表的速度的
②索引是天然有序的具备B树的快速检索类似二分查找
③索引天然聚合存储的数据是去重了的在一些操作(分组排序等)中不会再产生中间表
哪些情况适合建立索引
对于查询占主要的应用来说索引显得尤为重要。很多时候性能问题很简单的就是因为我们忘了添加索引而造成的或者说没有添加更为有效的索引导致。如果不加索引的话那么查找任何哪怕只是一条特定的数据都会进行一次全表扫描如果一张表的数据量很大而符合条件的结果又很少那么不加索引会引起致命的性能下降。但是也不是什么情况都非得建索引不可比如性别可能就只有两个值建索引不仅没什么优势还会影响到更新速度这被称为过度索引。
那么哪些情况下适合建立索引呢
1. 频繁作为where条件语句查询的字段
这是因为在频繁查询的字段列创建索引可以避免查询数据的时候走全表扫描这样查询的速度就会大大增加
2. 关联字段需要建立索引
关联的字段一般都是通过主键来进行两张表的关联主键大部分情况下都是主键如果关联的两个主键都没有索引那么我们一般优先考虑在被驱动表中的字段建立索引因为在外连接的查询中被驱动表是需要被多次重复扫描的那么让它走索引查询是会快很多的可以避免更多次数的全表扫描
3. 排序字段可以建立索引
这是因为B树结构的索引是天然有序的
4.分组字段可以建立索引因为分组的前提是排序
5.统计字段可以建立索引例如count(),max()
这是因为索引是天然聚合的就是存放在b树的数据是已经去重的数据了存储的数据还是比较紧凑的那么通过B树的双向指针可以更快的找到要统计的数据而且在加了索引的列的统计的时候MySQL是不会产生中间表来专门去重了可以减少不必要的性能开销在没有索引的列的统计分组 的SQL语句中MySQL都是会创建临时表来存储数据的
哪些情况不适合建立索引
1.频繁更新的字段不适合建立索引 (因为数据比较大的表的索引的创建是非常耗时的而且如果一个字段被频繁更新那么我们还需要频繁的维护这个树的结构这个开销是非常大的)
2.参与列计算的列不适合建索引因为计算后的列的值最后不一定是有序的不是有序的 那么就会导致索引失效 。
3.表数据可以确定比较少的不需要建索引
4.数据重复且分布比较均匀的的字段不适合建索引因为说不定你对这种索引字段的查询的速度还没有全表扫描快例如性别真假值
5.where条件中用不到的字段不适合建立索引因为索引是可以帮助我们在查询的时候大大的提高查询效率但是在增加删除操作确实异常消耗性能的因为需要不断的维护B树的结构有序你就需要维护你查询的时候都不需要使用到这个字段了那还建立这个字段的索引列干啥等着吃你系统的性能嘛
为什么索引使用的是 B 树——重点
①因为B树是把数据都存放在叶子节点中的在innodb存储引擎中一个b树的节点是 一页(16k)那么在固定大小的容量中 B树的非叶子节点中就可以存放更多的索引列数据也就意味着B树的非叶子节点存储的数据的范围就会更大那么树的层次就会更少IO次数也就会更少
②B树的叶子节点维护了一个双向链表它更有利于范围查询。
③B树中的叶子节点和非叶子节点的数据都是分开存储的分别存放在叶子节点段和非叶子节点段那么进行全表扫描的时候就可以不用再扫描非叶子节点的数据了并且这是一个顺序读取数据的过程顺序读比随机读的速度要快很多很多扫描的速度也会大大提高 链接聚簇索引和非聚簇索引到底有什么区别_聚簇索引和非聚簇索引区别_Linux小百科的博客-CSDN博客
索引分为哪几类
从大类来分分为聚簇索引和非聚簇索引
从具体的种类来分有
主键索引: 也简称主键。它可以提高查询效率并提供唯一性约束。一张表中只能有一个主键。
普通索引就是普普通通的索引。
唯一索引索引的值不能重复。
复合索引在工作中用得比较频繁的一个索引
当有多个查询条件时我们推荐使用复合索引。比如我们经常按照 A列 B列 C列进行查询时通常的做法是建立一个由三个列共同组成的复合索引而不是对每一个列建立普通索引。
创建方式 复合索引中的索引的顺序是非常重要的
alert table test add idx_a1_a2_a3 table (a1,a2,a3) 使用复合索引可以极大的减少回表的带来的性能开销体现在 复合索引可以进行更多的索引覆盖因为你索引的个数明显更加多了呀即便是回表也是携带更少的主键进行回表查询与MySQL5.7后的索引下推有关
复合索引是基于第一个索引的比如你建立了一个a,b,c的复合索引那么你不能跳过a索引直接去查询b索引因为在建立a,b,c这个复合索引的时候是会创建aa,ba,b,c这三个索引的你会发现它们都是基于a索引的 (并不会单独的创建(a,c)这个索引)
hash索引hash天然快最快o(1),最慢o(n),树化(lon(n))但是天然无序
空间索引
全文索引
聚簇索引和非聚簇索引的区别
什么是聚簇索引重点
聚簇索引就是将数据(一行一行的数据)跟索引结构放到一块InnoDB存储引擎使用的就是聚簇索引 注意点
1、InnoDB使用的是聚簇索引聚簇索引默认使用主键作为其索引将主键组织到一棵B树中而行数据就储存在叶子节点上若使用where id 14这样的条件查找主键则按照B树的检索算法即可查找到对应的叶节点之后获得行数据。
2、若对Name列进行条件搜索则需要两个步骤第一步在辅助索引B树中检索Name到达其叶子节点获取对应的主键。第二步使用主键在主索引B树种再执行一次B树检索操作最终到达叶子节点即可获取整行数据。重点在于通过其他键需要建立辅助索引
聚簇索引具有唯一性由于聚簇索引是将数据(一行一行的数据)跟索引结构放到一块因此一个表仅有一个聚簇索引其他辅助索引可能是只有几个列的数据和索引放在一起
表中行的物理顺序和索引中行的物理顺序是相同的在创建任何非聚簇索引之前创建聚簇索引这是因为聚簇索引改变了表中行的物理顺序数据行 按照一定的顺序排列并且自动维护有序就一定需要维护这个顺序
聚簇索引中的索引默认是主键如果表中没有定义主键InnoDB 会选择一个唯一且非空的索引代替。如果没有这样的索引InnoDB 会隐式定义一个6个字节大小的row_id来作为主键这个主键会作为聚簇索引中的索引。如果已经设置了主键为聚簇索引又希望再单独设置聚簇索引必须先删除主键然后添加我们想要的聚簇索引最后恢复设置主键即可。
非聚簇索引
非聚簇索引在 InnoDB 引擎中也叫二级索引
在 MySQL 的 InnoDB 引擎中每个索引都会对应一颗 B 树而聚簇索引和非聚簇索引最大的区别在于叶子节点存储的数据不同聚簇索引叶子节点存储的是行数据因此通过聚簇索引可以直接找到真正的行数据而非聚簇索引叶子节点存储的是主键信息所以使用非聚簇索引还需要回表查询因此我们可以得出
聚簇索引和非聚簇索引的区别主要有以下几个 聚簇索引叶子节点存储的是行数据而非聚簇索引叶子节点存储的是聚簇索引通常是主键 ID。 聚簇索引查询效率更高而非聚簇索引需要进行回表查询因此性能不如聚簇索引。 聚簇索引一般为主键索引而主键一个表中只能有一个因此聚簇索引一个表中也只能有一个而非聚簇索引则没有数量上的限制。
什么叫回表重点
如果一个查询是先走辅助索引聚簇索引外的索引都叫辅助索引的那么通过这个辅助索引innodb中的辅助索引的data存储的是主键没有获取到我们想要的全部数据那么MySQL就会拿着辅助索引查询出来的主键去聚簇索引中进行查询这个过程就是叫回表
MySQL索引失效的几种情况重点
①like查询以%开头因为会导致查询出来的结果无序
②类型转换列计算也会可能会让索引失效因为结果可能是无序的也可能是有序的
③在一些查询的语句中MySQL认为走全表扫描比索引更加快也会导致索引失效
④如果条件中有or并且or连接的字段中有列没有索引那么即使其中有条件带索引也不会使用索引 (这是因为MySQL判断即便你开始走了索引查询但是它发现查询中有Or 也就是说or 后面的还是需要走全表扫描因为or会导致后面的数据是无序的所以MySQL还不如一开始就直接走全表扫描这也是为什么尽量少用or的原因)要想使用or又想让索引生效只能将or条件中的每个列都加上索引当检索条件有or但是所有的条件都有索引时索引不失效可以走【两个索引】这叫索引合并取二者的并集;
⑤复合索引不满足最左原则就不能使用全部索引
MySQL索引优化手段有哪些
① 尽可能的使用复合索引而不是索引的组合
②创建索引尽量让辅助索引进行索引覆盖 而不是回表
③在可以使用主键id的表中尽量使用自增主键id这样可以避免页分裂
④查询的时候尽量不要使用select * 这样可以避免大量的回表
⑤尽量少使用子查询能使用外连接就使用外连接这样可以避免产生笛卡尔集
⑥能使用短索引就是用短索引这样可以在非叶子节点存储更多的索引列降低树的层高并且减少空间的开销
什么叫回表重点
如果一个查询是先走辅助索引聚簇索引外的索引都叫辅助索引的那么通过这个辅助索引innodb中的辅助索引的data存储的是主键没有获取到我们想要的全部数据那么MySQL就会拿着辅助索引查询出来的主键去聚簇索引中进行查询这个过程就是叫回表
什么叫索引覆盖重点
如果一个查询是先走辅助索引的那么通过这个辅助索引就直接获取到我们想要的全部数据了不需要进行回表这个过程就叫做索引覆盖
索引-索引-索引
什么是索引
索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分)它们包含着对数据表里所有记录的引用指针。
索引是一种数据结构。数据库索引是数据库管理系统中一个排序的数据结构以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B树。
更通俗的说索引就相当于目录。为了方便查找书中的内容通过对内容建立索引形成目录。索引是一个文件它是要占据物理空间的。
索引有哪些优缺点
索引的优点
可以大大加快数据的检索速度这也是创建索引的最主要的原因。 通过使用索引可以在查询的过程中使用优化隐藏器提高系统的性能。 索引的缺点
时间方面创建索引和维护索引要耗费时间具体地当对表中的数据进行增加、删除和修改的时候索引也要动态的维护会降低增/改/删的执行效率 空间方面索引需要占物理空间。
索引有哪几种类型
从大类来分分为聚簇索引和非聚簇索引
从具体的种类来分有
主键索引: 数据列不允许重复不允许为NULL一个表只能有一个主键。
唯一索引: 数据列不允许重复允许为NULL值一个表允许多个列创建唯一索引。
可以通过 ALTER TABLE table_name ADD UNIQUE (column); 创建唯一索引
可以通过 ALTER TABLE table_name ADD UNIQUE (column1,column2); 创建唯一组合索引
普通索引: 基本的索引类型没有唯一性的限制允许为NULL值。
可以通过ALTER TABLE table_name ADD INDEX index_name (column);创建普通索引
可以通过ALTER TABLE table_name ADD INDEX index_name(column1, column2, column3);创建组合索引
全文索引 是目前搜索引擎使用的一种关键技术。
可以通过ALTER TABLE table_name ADD FULLTEXT (column);创建全文索引 索引的数据结构b树hash 索引的数据结构和具体存储引擎的实现有关在MySQL中使用较多的索引有Hash索引B树索引等而我们经常使用的InnoDB存储引擎的默认索引实现为B树索引。对于哈希索引来说底层的数据结构就是哈希表因此在绝大多数需求为单条记录查询的时候可以选择哈希索引查询性能最快其余大部分场景建议选择BTree索引。
1B树索引
mysql通过存储引擎取数据基本上90%的人用的就是InnoDB了按照实现方式分InnoDB的索引类型目前只有两种BTREEB树索引和HASH索引。B树索引是Mysql数据库中使用最频繁的索引类型基本所有存储引擎都支持BTree索引。通常我们说的索引不出意外指的就是B树索引实际是用B树实现的因为在查看表索引时mysql一律打印BTREE所以简称为B树索引
创建索引的三种方式删除索引 第一种方式在执行CREATE TABLE时创建索引
CREATE TABLE user_index2 (id INT auto_increment PRIMARY KEY,first_name VARCHAR (16),last_name VARCHAR (16),id_card VARCHAR (18),information text,KEY name (first_name, last_name),FULLTEXT KEY (information),UNIQUE KEY (id_card)
);第二种方式使用ALTER TABLE命令去增加索引
ALTER TABLE table_name ADD INDEX index_name (column_list);ALTER TABLE用来创建普通索引、UNIQUE索引或PRIMARY KEY索引。
其中table_name是要增加索引的表名column_list指出对哪些列进行索引多列时各列之间用逗号分隔。
索引名index_name可自己命名缺省时MySQL将根据第一个索引列赋一个名称。另外ALTER TABLE允许在单个语句中更改多个表因此可以在同时创建多个索引。
第三种方式使用CREATE INDEX命令创建
CREATE INDEX index_name ON table_name (column_list);CREATE INDEX可对表增加普通索引或UNIQUE索引。但是不能创建PRIMARY KEY索引
删除索引
根据索引名删除普通索引、唯一索引、全文索引alter table 表名 drop KEY 索引名
alter table user_index drop KEY name;
alter table user_index drop KEY id_card;
alter table user_index drop KEY information;删除主键索引alter table 表名 drop primary key因为主键只有一个。这里值得注意的是如果主键自增长那么不能直接执行此操作自增长依赖于主键索引
需要取消自增长再行删除
alter table user_index– 重新定义字段
MODIFY id int,
drop PRIMARY