线程
常见的锁:
乐观锁、悲观锁、sychronized锁、lock锁、可重入锁、公平锁、非公平锁、分段锁、读写锁、还有死锁。
1、乐观锁&悲观锁
乐观锁认为别人每次去拿数据的时候都不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用场景,提高吞吐量。
而悲观锁就比较悲观,认为别人每次去拿数据的时候都会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。适用于多写操作。
2、死锁如何产生、场景
死锁是一个互相争抢的过程,互相拥有对方需要的资源又不释放。
产生死锁有四个必要条件,第一个是互斥条件。指进程对所分配到的资源进行排他性使用,即在同一时间内资源只能由一个进程占用,如果此时还有其他进程请求资源,则请求者只能等待,直至占有资源的进程使用完毕释放。
第二个是请求和保持条件。指进程已经至少保持一个资源,但又提出了新的资源请求,恰好这个资源又被其他进程占有,然后又不释放自己拥有的资源
第三个是不剥夺条件,指进程已获得的资源,在未使用完毕之前不能被剥夺,只能由自己释放
第四个是多个进程形成环形等待链。a需要b,b需要c,c需要a这种
场景:比如有用户a和b,他们同时对相同的商品g1,g2下单,这时候需要对商品表减掉相应的库存,然后用户a先对商品g2加锁,然后等待,用户b就对商品g1进行加锁,然后等待,这样就会产生死锁。(因为加锁顺序不一样)(解决办法就是对那些需要加锁的记录一次性加锁就行了)
事务A首先更新表X,然后获取锁,然后在没有释放锁的情况下尝试更新表Y;而事务B首先更新表Y,然后获取锁,也是在没有释放锁的情况下尝试更新表X。这种情况下,两个事务会相互等待对方的锁释放,从而形成死锁。
3、怎样避免或解除死锁
实际应用中不能百分百避免死锁,凡事都没有绝对嘛
但可以尽量减少,只要打破刚才说的四个条件之一就行,比如给锁加个时限,若超过了这个时间就放弃对这个锁的请求,并且回滚之前加锁后所干的事。
使用超时机制:在获取锁时,可以设置超时时间。
也可以在程序中引入死锁检测机制来检测死锁的存在,并通过释放某些资源或终止某些进程来恢复系统。
另外尽量避免循环等待条件,减少对资源长时间的独占
5、 synchronized 和 volatile 的区别是什么?
volatile 本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从 主存中读取; 他能保证变量的有序性和可见性。(就是当一个线程修改了这个变量后,其他线程能读取到这个变量修改后的值),但不保证原子性。sychronized则是加锁,使资源在同一时刻只能由一个线程访问,其他会被阻塞。既保证了原子性又保证了可见性,还有有序性(程序的执行顺序会按照代码的先后顺序执行,有效解决重排序问题
)
volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法、和代码块,不能修饰基本数据类型的。
volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
synchronized 关键字底层原理属于 JVM 层面
修饰普通方法,默认为当前实例对象this为锁对象,修饰静态方法和代码块为Class类为锁对象。
synchronized关键字不能修饰int、double等基本数据类型;
6、synchronized 和 Lock 有什么区别?
synchronized是内置的java关键字,无法获取锁的状态,并且会自动释放锁,是一个可重入,不可中断,非公平的锁。适合锁少量的代码同步问题。
ReentrantLock是一个java接口,可以判断是否获取到了锁,必须手动释放锁,否则会产生死锁,ReentrantLock是一个可重入,非公平的锁,适合锁大量的同步代码。
7、创建线程有几种方式
1.继承 Thread 类并重写 run 方法创建线程,实现简单但不可以继承其他类。
2.实现 Runnable 接口并重写 run 方法。避免了单继承局限性,编程更加灵活,实 现解耦。
3…实现 Callable 接口并重写 call 方法,创建线程。可以获取线程执行结果的返回值,并且可以抛出异常。
4.使用线程池创建(使用 java.util.concurrent.Executor 接口)。
8、线程有哪几种状态以及各种状态之间的转换?
刚创建的时候,刚new出来的时候就是新建状态,然后调用了线程对象的start方法后进入就绪状态,就是可以执行的状态。第三是运行态,线程获得cpu时间片后开始运行run函数中的代码,然后中间有个阻塞状态,因为某种原因使线程放弃了,或者不得不放弃cpu使用权,比如调用了sleep、wait还有join这些方法,然后sleep时间过了,还有其他线程调用了相同对象的notify或notifyAll方法后,或者调用join方法的线程执行结束后,就重新回到就绪状态。最后一个是死亡状态,线程生命周期结束,也可能因为异常中断。
(3)同步阻塞 – 线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),它会进入同 步阻塞状态。
9、创建线程池代码
private static final int THREAD_COUNT = 20;//线程数
static int sum = 0;//素数总数
public static void main(String[] args) {
// 创建拥有固定数量20个线程的线程池
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
// 计算每个线程需要处理的范围
int range = (MAX - MIN) / THREAD_COUNT;//4500
// 创建线程任务并提交给线程池
for (int i = 0; i < THREAD_COUNT; i++) {
int start = MIN + i * range;
int end = (i == THREAD_COUNT - 1) ? MAX : start + range - 1;
//提交一个任务,在线程中执行。
executor.submit(new FIndPrimeNumberTask(start,end));
}
//关闭线程池
executor.shutdown();
//通过线程池的isTerminated()方法,判断线程池是否已经关闭。线程池成功关闭,就意味着所有线程已经运行完毕了。
while (true) {
if (executor.isTerminated()) {
System.out.println("所有线程执行结束了,总素数为:" + sum);
break;
}
try {
Thread.sleep(200);//减小循环次数
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
10、线程池原理、好处、工作流程
原理:
线程池就是一种能够提前创建好线程的机制,能不断复用线程,执行任务的时候直接从线程池获取线程来获取就行,免得每次都去创建和销毁线程,节约了资源也能提高执行速度。(限制线程个数)
当有一个请求任务的时候,线程池中的线程,正在运行的线程,如果小于核心线程数的话,就会创建一个线程来执行这个任务,否则就会加入阻塞队列中,排队等待执行,如果队列也满了,就会看正在运行的线程数是否达到了最大线程数,没达到就创建非核心线程来执行,否则就只能用那个拒绝策略,就是把他拒绝掉,拒绝执行。
好处:
线程池就是用来限定线程个数的,避免线程创建过多导致服务器崩溃,运行缓慢
然后因为他提前创建好了线程, 所以不用每次都去创建或者销毁线程,不仅节约了资源,还能提高响应速度。
数据库连接池也是一样
流程:
首先当有一个请求任务的时候,线程池中的线程,正在运行的线程,如果小于核心线程数的话,就会创建一个线程来执行这个任务,否则就会加入阻塞队列中,排队等待执行,如果队列也满了,就会看正在运行的线程数是否达到了最大线程数,没达到就创建非核心线程来执行,否则就只能用那个拒绝策略,就是把他拒绝掉,拒绝执行。
1、默认情况下,创建完线程池后并不会立即创建线程, 而是等到有任务提交时才会创建线程来进行处理。(除非调用prestartCoreThread或prestartAllCoreThreads方法)
12、Join和Yield
join():类似于插队,在当前线程中用另一个线程执行join方法,当前线程会阻塞,直到调用join()方法的线程执行完毕才会转为就绪状态。重新参与CPU抢夺。
yield():使调用该方法的线程从运行状态转为就绪状态,重新参与抢夺cpu。一般只有比当前线程优先级更高或者相同的才有机会参与抢夺。
13、多进程和多线程分别适用于什么业务场景?
多进程适用的场景:
多进程可以实现资源的隔离,每个进程都拥有独立的内存空间和资源。适用于需要保护资源不被其他进程干扰的场景,像那些安全性要求较高的环境。还可以利用多核处理器实现并行计算,每个进程独立执行,可以提高计算性能,可以用在图像处理这种需要大量计算的场景
- 容错和稳定性:多进程可以提高系统的容错性,当某个进程发生故障时,不会影响其他进程的正常运行。适用于对系统稳定性有较高要求的场景,如服务器应用。
多线程适用的场景:
多线程适用于高并发的场景,因为他能同时处理多个请求。而且它是属于进程的,一个进程可以包含多个线程,线程间可以共享同一进程的数据资源,能够直接通信,适用于线程池、消息队列这些需要共享数据的场景,
- 响应性和用户体验:多线程可以将耗时的操作放在后台线程中进行,保持前台线程的响应,提高用户体验。适用于需要保持界面流畅和响应灵敏的GUI应用、移动应用等。
需要注意的是,多进程和多线程都有各自的优势和适用场景,但也存在一些问题和挑战,如进程间通信、数据同步、资源竞争等。在选择使用多进程还是多线程时,需要根据具体的业务需求、系统架构和性能要求等因素进行综合考虑。
14、七大参数、拒绝策略
1)corePoolSize:3 线程池的核心线程数(常驻线程数)
线程池的核心线程数(常驻线程数),一般情况下不管有没有任务都会一直在线程池中一直存活
2)maximumPoolSize: 7 线程池所能容纳的最大线程数
线程池所能容纳的最大线程数,当活动的线程数达到这个值后,后续的新任务将会被阻塞。
3)keepAliveTime:4 线程闲置时的超时时长
控制线程闲置时的超时时长,超过则终止该线程。一般情况下用于非核心线程
4)unit: 时间单位
用于指定 keepAliveTime 参数的时间单位,TimeUnit 是个 enum 枚举类型,常用的有:TimeUnit.HOURS(小时)、TimeUnit.MINUTES(分钟)、TimeUnit.SECONDS(秒) 和 TimeUnit.MILLISECONDS(毫秒)等。
5)workQueue:任务队列(阻塞队列)
当核心线程数达到最大时,新任务会放在队列中排队等待执行。
6)threadFactory:线程工厂
线程工厂,它是一个接口,用来为线程池创建新线程的。
7)RejectedExecutionHandler handler: 拒绝策略(银行有10个窗口,核心是3个窗口,所有窗口都开放,等待的座位也坐满了,银行再来新的顾客,银行没有能力接受新的顾客,银行就要做一个拒绝策略,建议去别的银行)
16、 核心线程数 最小线程数 最大线程数 它们之间的关系?
-
核心线程数是线程池中一直存活的线程数。即使这些线程当前没有任务执行,也不会被销毁。当有新的任务提交时,如果核心线程数没有达到上限,会创建新的线程来执行任务。
-
最小线程数是指线程池中允许存在的最小线程数。即使线程池中没有任务执行,也不会销毁低于这个数量的线程。
-
最大线程数指线程池中允许存在的最大线程数。当有新的任务提交时,如果核心线程数已满,并且任务队列也已满,会创建新的线程来执行任务,直到达到最大线程数为止。
当有一个请求任务的时候,线程池中的线程,正在运行的线程,如果小于核心线程数的话,就会创建一个线程来执行这个任务,否则就会加入阻塞队列中,排队等待执行,如果队列也满了,就会看正在运行的线程数是否达到了最大线程数,没达到就创建非核心线程来执行,否则就只能用那个拒绝策略,就是把他拒绝掉,拒绝执行。
17、锁用过吗?
用过,平常学习的时候也写过一些,像那个懒汉式就用到了锁,其他更多是为了练习而开多个线程。
锁是多线程编程中的同步机制,保证共享资源的互斥访问,也就是保证数据的一致性。常见的问题就是死锁了,是一个互相争抢的过程。互相拥有对方需要的资源又不释放。
18、守护线程
创建守护线程可以通过设置线程的daemon属性为true来实现。守护线程是一种后台线程,当程序中所有其他线程都结束时,守护线程也会自动结束。
守护线程是运行在后台的一种特殊线程。
它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
在 Java 中垃圾回收线程就是特殊的守护线程。
19、前台线程和后台线程
前台线程是用户可见的,后台不可见,一般前台线程的执行顺序会在后台线程之前, 还有前台占用的资源比较多,可以与用户进行交互。
- 定义:前台线程是指在任务栏中有显示的线程,它是用户可见的线程。后台线程是指在任务栏中没有显示的线程,它是用户不可见的线程。
- 执行顺序:前台线程的执行顺序在后台线程之前,也就是说,前台线程会先于后台线程执行。
- 资源占用:前台线程会占用较多的系统资源,如CPU、内存等,而后台线程会占用较少的系统资源。
- 用户交互:前台线程可以与用户进行交互,而后台线程不可以。
- 进程退出:前台线程必须等待所有后台线程执行完毕才能退出,而后台线程可以在前台线程未执行完毕时退出。
20、重入锁
ReentrantLock可重入指的是在获取锁时,如果锁已经被其他线程持有,则当前线程可以再次获取锁,并且不会影响其他线程获取锁。只不过要保证加锁和放锁的次数一样多,不然还是会导致死锁
锁的使用更加灵活,同一个线程可以多次获取锁。尽量避免死锁的发生。
- 锁的可重入性使得锁的使用更加灵活,同一个线程可以多次获取锁,只要锁没有被其他线程占用,就可以一直获取。
- 可重入锁可以避免死锁的发生,当一个线程已经获取锁时,再次获取锁时会检查锁的可重入性,如果锁已经被其他线程占用,则当前线程会等待,直到锁被释放。
- 可重入锁可以提高程序的性能,因为锁的可重入性可以避免不必要的同步操作,从而减少锁的开销。
- 可重入锁还可以实现公平锁和非公平锁,公平锁可以保证线程按照申请锁的顺序获取锁,而非公平锁则不保证。
21、公平锁和非公平锁
公平锁就是本着先到先得的原则,按照线程请求锁的顺序获取锁,优点就是避免某些线程很久都得不到执行。但是他的实现比较复杂,性能比较低,一般是通过维护一个等待队列,记录所有等待锁的线程,锁被释放时,就从等待队列中选择最先等待的线程来获取锁。
非公平锁是随机就近分配的原则,不需要什么等待队列,所以他性能会比较高,但是可能让某些倒霉的线程一直得不到执行。
22、线程池中的线程和手动new出来的线程
前者是后台线程,后者默认是前台线程
- 生命周期不同:线程池中的线程有固定的生命周期,当线程池中的线程空闲时,它们会被回收,当线程池中的线程繁忙时,它们会被创建。而手动new出来的线程的生命周期由应用程序控制,当应用程序不再需要它们时,它们会被回收。
- 性能不同:线程池中的线程在执行时,可以共享线程池中的资源,因此性能更好,而手动new出来的线程需要创建自己的资源,因此性能较差。
23、线程间的通信方式
使用volatile关键字,还有wait和notify或notifyAll,synchronized或lock锁,join等待执行,其他还有信号量、管道这些。
24、MySQL锁
\1. 乐观锁、悲观锁
\3. 全局锁:对整个数据库实例加锁,限制除了超级用户外的所有查询和修改操作。一般用于备份、恢复等操作。
\4. 表级锁:对整个表加锁,其他连接无法修改或者读取这个表的数据,还是可以对其他表进行操作的,没有全局锁那么狠
\5. 页级锁:对数据页(通常是连续的几个行)加锁,比表级锁更宽松一点,控制并发事务对该页的访问。适用于数据较大且并发量较高的场景。
\6. 行级锁:对单个行加锁,只锁定需要修改的数据行,其他行可以被同时修改或读取。并发性高,但锁管理较复杂。
\7. 共享锁:也称为读锁,多个事务可以同时持有共享锁并读取数据,但不能修改数据。适用于同时读取同一数据的场景。
\8. 排它锁:也称为写锁,事务持有排它锁时,其他事务无法同时持有共享锁或排它锁,用于保护数据的写操作。
\9. 意向共享锁:表级锁的辅助锁,表示事务要在某个表或页级锁上获取共享锁。
\10. 意向排它锁:表级锁的辅助锁,表示事务要在某个表或页级锁上获取排它锁。
间隙锁、临键锁和记录锁都能解决幻读的问题。
\11. 间隙锁可以锁定一个范围,在执行查询时可以防止其他事务插入或修改该范围内的数据
\12. 临建锁可以锁定一个记录的前一个键值
\13. 记录锁以锁定一个记录
反射
1、什么是反射?
反射就是在程序运行的时候,我们可以获取任意一个类的属性和方法,就是能够动态获取信息和调用对象的方法。
2、反射API类
1、Class类:反射的核心类,可以获取类的属性,方法等信息。
2、Field类:Java.lang.reflec包中的类,表示类的成员变量,可以用来获取和设置类之中的属性值。
3、Method类: Java.lang.reflec包中的类,表示类的方法,它可以用来获取类中的方法信息或者执行方法。
4、Constructor类: Java.lang.reflec包中的类,表示类的构造方法。
3、怎么使用反射?
先要获取类的Class对象,然后就可以用那些Field类、Method类或者Constructors类来操作这个类的属性和方法了。
4、获取类的方式
第一种是用类对象对象.getClass方法,第二种是类名.class,第三种是Class.forName()后面跟类的全路径名。
5、反射的使用场景有哪些?
Spring框架中的依赖注入和AOP、动态代理、序列化和反序列化、JDBC加载驱动的时候就是用Class.forName()来获取
还有很久以前不用spring,用jsp写的时候,我只写了一个类来继承HttpServlet,也是只需要重写一个service方法来接收用户请求,我设定的是将请求的方法名都以键值对的形式附加在url末尾,然后再service方法中,通过反射获取继承本类的对象的和那些方法,就可以用invoke函数调用对应的请求对应的方法来处理请求。就不用每个控制器类都要继承HttpServlet和重写service方法。
Class<? extends BaseServlet> clazz = this.getClass();//这里的this指的是继承BaseServlet对象
//获取要执行的方法 通过类的字节码对象获取方法的字节码对象 获取当前类的所有方法
Method method = clazz.getDeclaredMethod(m, HttpServletRequest.class,HttpServletResponse.class);
method.setAccessible(true);
//让方法执行
method.invoke(this, req,resp);
缓存
你对Redis的认识
redis就是个keyvalue形式的非关系型数据库,key是String类型,value常见的有五种,它是基于内存存储的,与mysql不同,它主要做缓存方面的工作。也支持持久化数据到硬盘。还有非常常见的三个问题穿透、击穿还有雪崩。其他更高级的话像分布式锁、redis集群,听过名字,但还没开始深入研究他们。
1、一二级缓存、Redis缓存
(一二级缓存是mybatis的缓存,跟Redis没关系,这里只是为了对比)
区别:
一级缓存时默认开启的,在进行关联查询的时候,不会进行重复的关联查询,是单个sqlSession级别的缓存,而二级缓存需要手动开启,是nameSpace级别的缓存,一个mapper对应一个二级缓存,对多个sqlSession生效,开启后查询标签的useCache参数默认为true,增删改语句的flushCache也是默认为true。
在开启二级缓存时,查出来的数据默认先先存储在一级缓存中,当有sqlSession关闭时,它里面的一级缓存数据就会被存储到mapper的二级缓存中,这样该mapper种的其他会话执行了相同的方法时,就会在二级缓存中找到匹配的数据,如果没有找到,才会到数据库中查找。
二级缓存的作用?
二级缓存需要手动开启,是nameSpace级别的缓存,一个mapper对应一个二级缓存,对多个sqlSession生效,开启后,查出来的数据默认先先存储在一级缓存中,当有sqlSession关闭时,它里面的一级缓存数据就会被存储到mapper的二级缓存中,这样该mapper种的其他会话执行了相同的方法时,就会在二级缓存中找到匹配的数据,如果没有找到,才会到数据库中查找。作用就是提高了系统性能,减少对数据库的访问,提高相应速度,避免重复执行sql
如何开启二级缓存?
在配置文件中开启,然后在xml文件中用cache标签定义缓存的实现。开启后查询标签的useCache参数默认为true,增删改语句的flushCache也是默认为true。
二级缓存与Redis缓存
二级缓存是mybatis框架的一种缓存实现,是namespace级别的缓存,针对多个sqlSession生效,一个mapper对应一个二级缓存,缓存的时sql的返回结果。而Redis是keyvalue形式的nosql数据库,基于内存存储,也会根据一定的策略持久化到磁盘,即使断电也不会丢失数据。支持的数据类型比较多,可以跨多个应用程序共享缓存,还有分布式锁、消息队列和集群等强大功能。
Redis缓存怎么保证不丢失数据?
Redis缓存支持持久化,将内存中的数据写入硬盘里面,一种是快照方式RDB,另一种是只追加文件AOF
RDB将内存中的数据,完整的生成一个快照,以.rdb结尾的文件保存在硬盘上,当需要恢复时,再从文件中加载到内存中。
AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的,服务器在启动时,可以通过载入和执行aof文件中保存的命令来还原服务器关闭之前的数据库状态。
2、redis缓存使用方式:
1、在pom文件中引入redis的启动器
2、然后在配置文件中配置redis用户名、密码、存活时间等等各种配置。
3、再写一个RedisTemplate工具类,并且自定义一个序列化器,使存进缓存中的数据易读性更强
4、现在就能用redis缓存了,在配置类上或者主启动类上添加@EnableCaching注解,开启缓存,在有需要的方法上添加@Cacheable注解,设置好缓存名,就能将方法的返回值缓存起来了,@CacheEvict用来清空缓存
3、Redis为什么比MySQL快?
Redis是基于内存存储的,存储的是键值对数据,时间复杂度O(1)常数阶,而且是单线程的多路复用IO,而MySQL是基于磁盘存储的,底层实现是B+Tree,时间复杂度是O(logn)(I/O 多路复用就是只有单个线程,通过跟踪 每个 I/O 流的状态,来管理多个 I/O 流。)
4、Redis事务
是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序的执行。事务在执行过程中,不会被其他客户端发送来的命令请求所打断。
Redis事务的主要作用就是串联多个命令防止别的命令插队。
5、Redis为啥要设置缓存过期时间
因为内存是有限的,如果一直保存,内存很快就会被占满。
很多时候某些数据比如短信验证码可能只需要存在5分钟,以后就再也不用了,不清除的话会浪费内存。手动清除又比较麻烦
6、缓存穿透,缓存击穿、缓存雪崩及解决办法
缓存穿透是指请求的是缓存和数据库中都没有的数据,而用户不断发起请求,这时的用户很可能是攻击者,攻击会导致数据库压力过大。为了应对这种情况,我们可以将查询结果,也就是这个空值也缓存起来,并设置一个相对较短的过期时间,这样当下次发起相同的请求的时候就直接返回缓存中的这个空值,不用再访问数据库。
缓存击穿是指查询缓存中没有但数据库中有的数据,一般是缓存的时间刚好到期,这时刚好并发用户特别多,缓存中读取不到数据,就都去数据库里去取,引起数据库压力瞬间增大。这种情况,比较笨的应对方法是设置热点数据永不过期,也可以加互斥锁。(当查询结果为空时,先锁上,再从数据库加载,加载完毕释放锁,其他线程发现获取锁失败,就过会再重试,锁的类型这个,单机环境用并发包的Lock类型就行,集群环境用分布式锁。最后一个办法是,将不同的数据设置不同的过期时间。)
缓存雪崩是指在某一刻,缓存中的数据大面积的同时失效,这时又有很大的流量涌入,这些请求都打到数据库上,数据库承受不住这么大的压力,出现崩溃的现象。针对这种情况,除了设置热点数据永不过期外,还可以给不同的数据设置不同的过期时间,另外也可以使用Redis集群,或者直接限流,避免同时处理大量请求。
7、Mysql和Redis的区别、Redis优点:
mysql是一种关系型数据库,使用二维表来存储数据,而且他的数据是存储在硬盘上,支持各种简单的复杂的增删改查
redis是keyvalue形式的非关系型数据库,主要是做缓存的,基于内存存储,所以容量有限,redis的优势是能够实现一些强大的功能如分布式锁,消息队列、集群什么的。反正就是非常厉害。
Redis是纯内存操作,所以他的性能非常出色,并且采用了非阻塞的io多路复用机制,是一个单线程操作。
1 因为是纯内存操作,Redis 的性能非常出色,每秒可以处理超过 10 万次读写操 作,是已知性能最快的 Key-Value 数据库。Redis 支持事务 、持久化 2、单线程操作,避免了频繁的上下文切换。 3、采用了非阻塞 I/O 多路复用机制。I/O 多路复用就是只有单个线程,通过跟踪 每个 I/O 流的状态,来管理多个 I/O 流。
8、redis怎么刷新,怎么保证数据一致RDB
手动刷新,使用@CacheEvict清空缓存
Redis缓存支持持久化,将内存中的数据写入硬盘里面,一种是快照方式RDB,另一种是只追加文件AOF
RDB将内存中的数据,完整的生成一个快照,以.rdb结尾的文件保存在硬盘上,当需要恢复时,再从文件中加载到内存中。
AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的,服务器在启动时,可以通过载入和执行aof文件中保存的命令来还原服务器关闭之前的数据库状态。
有两种方式,先删缓存再更新数据,或者先更新数据再删缓存,其实我觉得两种都有他们的风险。
缓存-》数据库:
在高并发情况下,第一个线程删除缓存,还没来得及去操作数据库,这时第二个线程访问缓存,发现为null,于是去数据库查询,获取到需要的值,这时候第一个线程才开始操作数据库,然后设置缓存,但是第二个线程又恰好将第一个线程刚设置的缓存给覆盖掉,然后就出现“乌龙”,数据不一致的问题也出现了!
延迟双删,先删除缓存数据,再把数据更新到数据库中,休眠一会(根据业务逻辑的耗时,更改休眠时间)后再次删除该缓存数据。若线程1是更新请求,线程2是查询请求,延迟双删,可以保证再这两个请求同时存在的情况下的数据一致性!确保查询请求结束,更新请求可以删除查询请求造成的缓存脏数据。
先删除“数据库”再去更新“缓存”:
数据库写完之后,再删除缓存,但删除失败了,这会导致数据不一致。
可以引入MQ,将删除缓存的操作放在MQ中,如果删除操作失败了,启动MQ重试机制,在重试的这段时间,缓存数据不会更新。
9、为什么不使用HashMap而使用Redis?
HashMap是有最大容量的,也不能持久化,并且线程不安全,功能比较少。
而Redis可以持久化,也可以定时时间,可以跨多个应用程序共享缓存,还有分布式锁、消息队列和集群等强大功能。
10、存储原理
Redis是基于内存存储的,是一个非关系型数据库,数据以kyevalue的形式存储
-
数据存储在内存中:Redis 是一款基于内存的数据库,其数据是存储在内存中的。当 Redis 启动时,会将数据加载到内存中,并在运行时将数据缓存在内存中。
-
数据持久化:虽然 Redis 是一款基于内存的数据库,但是它也支持数据持久化。Redis 支持将数据持久化到磁盘上,以便在 Redis 重启时可以重新加载数据。Redis 还支持在内存中保存数据,并在 Redis 重启时将数据加载到内存中。
-
数据结构:Redis 支持多种数据结构,包括字符串、哈希、列表、集合、有序集合等。这些数据结构都是基于 Redis 的底层数据结构实现的,例如链表、哈希表、二叉树等。
-
存储引擎:Redis 支持多种存储引擎,包括默认的内存存储引擎和 RDB 存储引擎等。这些存储引擎都是基于 Redis 的底层存储实现的,例如 RDB 存储引擎就是将 Redis 中的数据保存到磁盘上