//Condition定义了等待/通知两种类型的方法
Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();
...
condition.await();
...
condition.signal();
condition.signalAll();
char的长度是不可变的,varChar的长度是可变的
char默认占用10个字节,varChar最大的长度可达65,532字节
而在效率上面char比varChar更高
public class ThreadDemo01 extends Thread{
public ThreadDemo01(){
//编写子类的构造方法,可缺省
}
public void run(){
//编写自己的线程代码
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args){
ThreadDemo01 threadDemo01 = new ThreadDemo01();
threadDemo01.setName("我是自定义的线程1");
threadDemo01.start();
System.out.println(Thread.currentThread().toString());
}
}
public class ThreadDemo02 {
public static void main(String[] args){
System.out.println(Thread.currentThread().getName());
Thread t1 = new Thread(new MyThread());
t1.start();
}
}
class MyThread implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getName()+"-->我是通过实现接口的线程实现方式!");
}
}
public class ThreadDemo03 {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Callable<Object> oneCallable = new Tickets<Object>();
FutureTask<Object> oneTask = new FutureTask<Object>(oneCallable);
Thread t = new Thread(oneTask);
System.out.println(Thread.currentThread().getName());
t.start();
}
}
class Tickets<Object> implements Callable<Object>{
//重写call方法
@Override
public Object call() throws Exception {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getName()+"-->我是通过实现Callable接口通过FutureTask包装器来实现的线程");
return null;
}
}
就像一个鱼塘一样,鱼塘里面有很多条鱼,而这里的鱼就相当于线程。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
// 看了底层的源码你就会发现,其实还是使用了ThreadPoolExecutor 来实现线程池的。
ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
RejectedExecutionHandler handler)
ArrayBlockingQueue
//是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
LinkedBlockingQueue
//一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
SynchronousQueue
//一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
PriorityBlockingQueue
//一个具有优先级得无限阻塞队列。
//1.当线程池的线程数等于于最大线程时,就会抛出 java.util.concurrent.RejectedExecutionException异常,涉及到该异常的任务也不会执行。
ThreadPoolExecutor.AbortPolicy();
//2.当线程池的线程数等于于最大线程时,后台静默的丢弃不能执行的任务,并不会报错。
ThreadPoolExecutor.DiscardPolicy();
//3. 当线程池的线程数等于于最大线程时,它会一直重试添加当前任务,并且一直调用execute()方法,直到成功为止。
ThreadPoolExecutor.CallerRunsPolicy();
//4. 当线程池的线程数等于于最大线程时,抛弃线程池中工作时间最久的任务,并执行当前任务。
ThreadPoolExecutor.DiscardOldestPolicy();
它是一个复合注解,它包括了
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
三个注解
可以将某个类定义成一个配置类,在Spring IOC容器启动的时候将该类和该类中被@Bean标注的方法返回值加载到容器中。
导入自动化配置类,主要就是解析类路径下META-INF/spring.factories文件中定义的自动化配置类,导入到spring ioc容器中。
主要就是将@SpringBootApplication标注的类所在的当前包及其子包中的被@Controller,@Service,@Repository,@Component这4个注解标注的类加入到spring容器中 。
excludeFilters属性就是排除哪些类不需要扫描并装配到spring容器中,源码中有两种类型的类不需要扫描装配到容器中,
一是:TypeExcludeFilter.class,如果用户需要将某个类不装配到spring容器中,即使这个类上面被标注上述4个注解中的某一个,就需要自定义类继承TypeExcludeFilter来实现具体的不装配规则。
二是:AutoConfigurationExcludeFilter.class,因为在@EnableAutoConfiguration注解中已经将自动化配置类装配到了spring容器中,所有在这里就不需要再次将这些自动化配置类扫描导入到容器中。
当你更新数据到数据库的时候就同步删除缓存数据,但是会有几个问题。
该例子中,由于线程2在查询完数据库数据之后,写入缓存之前,数据库的数据被线程1更新并且执行完同步删除操作了,所以最终导致脏数据问题,并且脏数据可能会持续很久。
当然,由于更新数据库操作耗时一般比写缓存更久,所以该例子发生的概率并不会太大,但还是有可能的。
最典型的场景就是,线程2查询完数据库之后,写缓存之前,线程2所在服务器发生了YGC,这个时候线程2可能就需要等待几十毫秒才能执行写缓存操作,这种情况就很容易出现上面这个例子了。
小结:由于难以收拢所有更新数据库入口,同时可能存在长期的脏数据问题,该方案一般不会被单独使用,但是可以作为一个补充,下面的方案会提到。
在方案一的基础上删延时再删除一遍缓存,但是也会有问题:
可能有的人觉得稍微调大点延迟时间就可以解决这个问题,但是其实主库在写压力比较大的时候,主从之间的同步延迟甚至可能是分钟级的。
小结:由于延迟时间难以确认,同时无法绝对保障数据的一致性,该方案一般不会使用。
使用binlog来监听删除缓存,如果缓存删除失败则使用MQ进行不断重试删除。流程如下:
该方案是当前的主流方案,整体上没太大的问题,但是极端场景下可能还是有一些小问题。
脏数据时间窗口“较大”
这个脏数据时间窗口较大,是相对方案一来说的。在你收到binlog之前,他中间要经过:binlog从主库同步到从库、binlog从库到binlog监听组件、binlog从监听组件发送到MQ、消费MQ消息,这些操作每个都是有一定的耗时的,可能是几十毫秒甚至几百毫秒,所以说它其实整体是有一个脏数据的时间窗口。
而同步删除是在更新完数据库后马上删除,时间窗口大概也就是1毫秒左右,所以说binlog的方式相对于同步删除,可能存在的脏数据窗口会稍微大一点。(如果你在意这个问题甚至可以结合方案一来使用,这样,这个方案就会比较完美了)
极端场景下存在长期脏数据问题
binlog抓取组件宕机导致脏数据。该方案强依赖于监听binlog的组件,如果监听binlog组件出现宕机,则会导致大量脏数据。还有就是拆库拆表流程中可能存在并发脏数据。
我们来看下面这个例子:
表A正在进行数据库拆分,当前进行到灰度切读流量阶段:部分读新库,部分读老库
数据库拆分大致流程:增量数据同步(双写)、全量数据迁移、数据一致性校验、灰度切读、切读完毕后停写老库。
此时表A存在数据 a=1,并发情况下可能有以下流程
该例子中,灰度切读阶段中,我们还是优先保障老库的流程,因此还是先写老库,由于写新库和写老库之间存在时间间隔,导致线程2并发查询到新库的老数据,同时在监听binlog删除缓存流程之后将老数据写入缓存,从而导致脏数据问题,并且脏数据可能会持续很久。
双写的方式有很多种,我们使用的是通过公司的中间件直接将老库数据通过binlog的方式同步到新库,该方案通过监控发现在写压力较大的情况下,延迟可能会达到几秒,因此出现了上述问题。
而如果是使用代码进行同步双写,双写之间的时间间隔会较小,该问题出现的概率会相对低很多,但是还是无法保障绝对不会出现,就像上面提过的,写老库和写新库2个操作之间如果发生了YGC或者FGC,就可能导致这两个操作之间的时间间隔比较大,从而可能发生上面的案例。
还有就是代码双写的方式必须收敛所有的写入口,上文提到过的,通过命令行或者数据库管理平台的方式修改的数据,代码双写也是无法覆盖的,需要执行者在新老库都执行一遍,如果遗漏了新库,则可能导致数据问题。
小结:该方案在大多数场景下没有太大问题,业务比较小的场景可以使用,或者在其基础上进行适当补充。
这个方案也是我现在在我们项目中里面使用的方案,这里面有很多很多的思考,踩了各种坑之后不断优化而来的方案。该方案整体以方案3加上方案1作为主体,然后增加了各种优化。
整体方案如下:
更新数据库同步删除缓存
监听数据库的binlog异步删除缓存:带重试,保障一定会最终删除成功
缓存数据带过期时间,过期后自动删除,越近更新的数据过期时间越短,主要用于进一步防止并发下的脏数据问题,解决一些由于未知情况,导致需要更换缓存结构的问题
监听数据库的binlog延迟N秒后进行数据一致性校验,解决一些极端场景下的脏数据问题
存在数据库更新的链路禁用对应缓存,防止并发下短期内的脏数据影响到更新流程
强制读Redis主节点
查询异步数据一致性校验、灰度放量
更新数据库后同步删除缓存
这个同步删除缓存其实是为了解决我们上面说的那个异步binlog删除不一致时间窗口比较大的问题。更新完数据之后,我们马上进行一次同步删除,不一致的时间窗口非常小。
监听数据库的binlog异步删除缓存
该步骤是整个方案的核心,也就是方案3,因为binlog理论上是绝对不会丢的,他不像同步删除存在无法收敛入口的问题。因此,我们会保障该步骤一定能删除成功,如果出现失败,则通过MQ不断重试。
通过前面两个方案点,我们其实已经保障了绝大多数场景下数据是正确的。
缓存数据带过期时间,过期后自动删除,越近更新的数据过期时间越短
该策略的设计初衷,是因为我们前面讲的那些并发问题其实都是在存在并发更新跟一些并发查询的场景下出现的,因此最近刚刚更新过的数据,他出现不一致的概率相对于那种很久没更新过的数据来说会大很多。
例如最近一个小时内更新的数据,我可能给他设计的过期时间很短,当然这个过期时间很多是相对于其他数据而言,绝对时间还是比较长的,例如我们使用的是一个小时。
这边是因为我们整体的请求量和数据量太大,如果使用的过期时间太短,会导致写缓存流量特别大,导致缓存集群压力很大。
因此,如果使用该策略,建议过期时间一开始可以设置大一点,然后逐渐往下调,同时观察缓存集群的压力情况。
该方案可以进一步保证我们数据的一个最终一致性。
同时带过期时间可以解决另一个问题,如果你在缓存上线后发现缓存数据结构设计不合理,你想把该缓存替换掉。如果该缓存有过期时间,你不需要处理存量数据,让他到期自动删除就行了。如果该缓存没有过期时间,则你需要将存量数据进行删除,不然可能会占用大量空间。
监听数据库的binlog延迟N秒后进行数据一致性校验
这个操作也是非常关键,方案3存在的问题就可以通过这个操作来解决掉。就如上面提过的,脏数据都是在更新操作之后的很短时间内触发的。
因此,我们对每一个更新操作,都在延迟一段时间后去校验其缓存数据是否正确,如果不正确,则进行修复,这样就保障了绝大多数并发导致的脏数据问题。
至于延迟多久,我个人建议是延迟几分钟,不能延迟太短,否则起不到兜底的效果。
存在数据库更新的链路禁用对应缓存
在数据库更新的场景里面,我们可能会有一些查询操作。例如我更新完这个数据之后,我马上又查了一下。这个时候其实如果你去走缓存,很有可能是会存在脏数据。因为他更新完之后,马上读取这个间隔是非常短的。你的缓存其实可能还没有删除完,或者存在短期内的不一致,我们还没有修复。
这种更新场景他对数据的一致性要求一般是比较高的。因为更新完之后,他要拿这个查询出来的数据去做一些其他操作。
例如记录数据变更的操作日志。
我把一个数据从a=1改成a=2,我在更新之前,我先查出来a=1,更新完之后我立马就去查出来a=2,这个时候我就记录一条操作日志,内容是a从1变成2。
这种情况下,如果你在更新完之后的这个查询去走缓存,就有很大的概率查到a=1,这时候你的操作日志就变成a从1变成1,导致操作日志是错的。
所以说这种更新后的查询,我们一般会让他不走缓存,因为他这个时效性就是太快了,缓存流程可能还没处理完成。
这个方案点其实是借鉴了MySQL事务的设计思想,MySQL中,事务对于自己更新过的内容都是实时可见的。因此,我们这边也做了一个类似的设计。
强制读Redis主节点
Redis跟MySQL一样,也会有主从副本,也会有主从延迟。当你将数据写入Redis后,马上去查Redis,可能由于查询从副本,导致读取到的是老数据,因此我们可以通过直接强制读主节点来解决这个问题,进一步增加数据的准确性。
Redis 不像 MySQL 有主节点压力过大的问题,Redis 是分布式的,可以将16384个槽分摊到多个分片上,每个分片的主节点部署在不同的机器上,这样强制读主时,流量也会分摊到多个机器上,不会存在MySQL的单节点压力过大问题。
查询异步数据一致性校验、灰度放量
这一步是缓存功能使用前的一些保障措施,保障缓存数据是准确的。
对于查询异步数据一致性校验,我们一般在查询完数据库之后,通过线程池异步的再查询一次缓存,然后把这个缓存的数据跟刚才数据库查出来的数据进行比较,然后将结果进行打点统计。
然后查看数据一致性校验的一致率有多少,如果不一致的概率超过了1%,那可能说明我们的流程还是有问题,我们需要分析不一致的例子,找出原因,进行优化。
如果不一致的概率低于0.01%,那说明整个流程可能基本上已经没啥问题了。这边理论上一定会存在一些不一致的数据,因为我们查询数据库和缓存之间还是有一定的时间间隔的,可能是1毫秒这样,在高并发下,可能这个间隔之间数据已经被修改过了,所以你拿到的缓存数据和数据库数据可能其实不是一个版本,这种情况下的不一致是正常的。
对于灰度放量,其实就是保护我们自己的一个措施。因为缓存流程毕竟还没经过线上的验证,我们一下全切到缓存,如果万一有问题,那可能就导致大量问题,从而可能导致线上事故。
如果我们一开始只是使用几个门店来进行灰度,如果有问题,影响其实很小,可能是一个简单的事件,对我们基本没影响。
在有类似比较大的改造时,通过灰度放量的方式来逐渐上线,是一种比较安全的措施,也是比较规范的措施。
小结:该方案的链路确实比较长,但是高并发下确实会有很多问题,因此我们需要有很多措施去保障。
当然,这个方案也不是一下子就是这样的,也是通过不断的实践和采坑才逐渐演进而来的。目前该方案在我们线上环境使用了挺长一段时间了,基本没有什么问题,整体还是比较完善的。
最后缓存和数据库一致性保障方案,目前网上的资料大多是类似于方案3,如果你能在面试中说出我给的这个方案,相信可以让面试官眼前一亮,这其实就是你的加分项,可以帮助你从众多候选人之中脱颖而出。
以上关于redis缓存一致性问题参考于@程序员囧辉的博客
参考原文链接:https://blog.csdn.net/v123411739/article/details/124237900
Kafka本身会有一个策略来避免重复消费问题,这个策略就是,Kafka 的Broker块上面存储的消息都会有一个offSet标记,然后Kafka的消费者是通过这个offSet这个标记来维护当前已经消费了的数据,然后消费者每消费一批数据Kafka Broker就会去更新offSet的一个值从而避免重复消费问题。
默认情况下,消费者消费完这个消息的时候会自动提交offSet的值来避免重复消费。
Kafka消费端的自动提交逻辑里面会有一个默认的5S的一个时间间隔,意思是这个消息消费完成了,会在5S后的下一次向Broker去获取消息的同时提交offSet,所以在这5S内应用程序强制被kill掉或者是宕机的时候就会可能导致offSer没有被提交,从而当程序恢复后就会导致重复消费问题。
其次,Kafka里面会有一个叫Partition Balance的机制,这个机制的意思是把多个partition均衡的分配给多个消费者,然后消费者会从分配到的partition里面去消费消息,但是如果消费端在默认的5分钟内没有把这些分配到的partition全部消费完成,就会触发Kafka的Rebalance(重新分配)机制从而导致offset自动提交失败,而在重新Rebalance之后,消费端还是会从之前上一次提交的offset的位置开始消费,从而产生重复消费问题。
针对每条消息用md5进行加密,然后保存在MySQL或者redis里面,在消费信息之前去数据库里面查找判断是否已经被消费过。
redis支持的数据类型(set zset String List Hash)
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
Java文件执行流程,首先将Java文件编译成为.class文件,然后在JVM里面转化为相对应的机器码运行。
.class文件加载到JVM虚拟机,由类装载子系统加载到运行时数据区,然后在输出到执行引擎
浏览器发送请求到DisPatcherServlet
-----然后转发到HandMapping
-----然后由HandMapping返回的执行链去找的对应的Control
-----然后Control会返回一个视图解析器对象
-----然后去视图解析器找到view
-----最后渲染返回给前端
索引是帮助MySQL高效获取数据的排好序的数据结构。
点这里可以跳转到数据结构的可视化工具
二叉树(key -value)key是值,value是当前值的行的内存地址
当索引的数据结构是二叉树的时候,建索引的列的值不能是自增的,否则会导致索引失效,查询数据时间没有减少。(因为当索引列是自增的话,最终建成的二叉树就会是一个链表)如下图:
红黑树(自旋平衡二叉树)
红黑树的底层逻辑与二叉树一样,都是小的在左边,大的在右边,但是它比二叉树多出了一个自动平衡功能。当你插入1到7的数据就不会生成链表,而是会自动平衡,如图。(jdk1.8后hashMap的数据结构就变成了数组与红黑树了),这样就没有二叉树的缺点了。但是,这样的数据结构的深度会有问题,当你数据量非常大的时候,你的磁盘io就会非常大,所以它不适用于大数据量的场景。
Hash表
hash表的原理是(key - value)结构,key是索引列的字段的hash值,对应的value就是当前行的内存地址,它的优点就是不会像上面的一样要很大的磁盘io,它只需要计算(一次磁盘io)找到相应的hash值就可以找到目标行了。但是它有一个很大的缺点,就是当你的sql的条件不是等于,而是大于或小于这种查询范围的,这个索引就不会生效了。
B-Tree(红黑树的升级版)
我们发现红黑树的一个很严重的问题就是深度,而B-Tree比红黑树的优点就是解决了这个深度的问题。
问题的引出: 既然是因为深度的问题,那么我们思考,是什么原因造成的,怎么优化?
首先最直接的原因就是: 红黑树的每一个节点只能存储一个元素。
思考: 那么我们想,一个节点能不能存储多个元素呢?比如十个百个上万个等。或者直接将全部的元素都放在一个节点上面,那么不管怎么查找,深度都只是1了。但是,如果你将所有的元素(索引+数据–目标所在行的内存地址)都放在一个节点里面,特别是数据量大的时候就会占用很大的空间,几乎是上百M甚至是G,而一次磁盘io加载到内存根本加载不到这么大的内存,即使可以加载得到,所耗费的时间空间又是一个成本,而且我们每次sql查询需要用到的数据都不足一张表的百分之二十,这样就会造成很大的内存空间的浪费,所以说一次性加载完所以元素是不现实的。
并且MySQL限制了一个节点的大小是16kb。
如图:B-Tree虽然解决了深度问题,但是仍然没有解决范围查找问题。
B+Tree(B-Tree的升级版)
B+Tree相对于B-Tree的不同有:
首先: B-Tree的每一个节点都会存放着数据与索引,而B+Tree则是在叶子节点才会存储数据与索引,非叶子节点只存储索引。
其次: B+Tree的叶子点都会有一个连接着下一个叶子点的双向指针,这样就可以实现范围查找了。
并且: 因为非叶子结点只存一个索引,这样一个节点就可以存储很多个元素,这个就是为什么索引可以支撑千万级数据量的快速查找了。
如果索引使用bigint类型的话,一个索引就是8B然后指针是6B,一个节点最多放16kb,所以一个节点可以存放1170个索引数据,而叶子节点可以存放16个元素,那么(1170 x 1170 x 16 大约等于两千多万)
还有就是,MySQL会把存有数据的叶子节点存在内存中,所以只需要进行两次io就可以找到目标了
联合索引就是一个表里面有多个索引列,单值索引只有一个索引列。
以下举例的是常用的存储引擎
MyISAM: 索引文件和数据文件是分离的(非聚集索引)
MyISAM存储引擎的表,会有三种类型的文件如下图:
.frm: 存储表结构的相关信息。
.MYD: 存储表的数据。
.MYI: 存储表的索引。
MyISAM引擎查找数据的流程(比如找30): 首先是在B+Tree的索引上面找到30,然后获取它所对应的数据的内存地址0xF3,最后根据内存地址 (磁盘文件指针) 去 .MYD 文件里面找到相应的数据。
InnoDB: 索引文件和数据文件是同一个文件(聚集索引)
InnoDB存储引擎的表,只有两种类型的文件如下图:
.frm: 存储表结构的相关信息。
.ibd: 存储表的数据以及表的索引,.ibd=.MYI+.MYD。
InnoDB引擎查找数据的流程(比如找20): 首先是在B+Tree的索引上面找到20,然后获取它所对应的叶子节点,由于它的索引以及数据都是存储在同一个文件里面,所以就不需要像MyISAM 一样再去数据文件找数据,而是直接就拿到数据了,这个也是MyISAM是有指针而InnoDB没有指针的原因 。
聚集索引(InnoDB主键索引)就是索引以及数据存储在同一个文件里,非聚集索引就是分开,就像InnoDB与MyISAM一样。
首先: 如果你没有给InnoDB类型的表设置主键,那么MySQL底层就会自动帮你计算选取一个可以唯一标识的列来充当主键,如果找不到则会生成一个隐藏的列来充当主键。
其次: 设置主键的根本原因是为了查找数据比较快。
最后: 相信大家都认识uuid,这里为什么建议使用自增并且是整型数据当主键:
为什么是要整型 ?
是因为B+Tree存储或查找数据是根据大小来对比的,如果你使用uuid的是String类型的话又要转换为可比较大小的ASCII类型,其次String所占用的空间是比较大的,所以使用整型的优点是性能高节约并且存储空间。
为什么要自增?
我们都知道B+Tree的叶子节点都是从左到右是依次递增的,如果说你使用的不是自增的,那么当你的叶子节点里面有一个2和一个4这时你需要向里面插入一个3,这个时候就会需要重新分配索引以及分裂平衡树,但是如果是自增的,则永远都是在叶子节点后面追加就行了。
StringBuilder线程不安全(因为它底层的方法都是用synchronized 修饰的的),性能高
StringBuffer线程安全,性能低