分布式缓存
CDN
反向代理
本地
分布式
ConcurrentHashMap 解决HashMap并发死循环
hashmap并发死循环原理
参考用例
前提:线程1进行到了扩容后的第一步,记录当前节点为3,下一个为7;线程2扩容完并重新散列完。接下来线程1继续工作,e=7,读取线程2重新散列完的map,得到next=3;接下来把3用头插法塞到最前面,同时记录next=7,fine,开始死循环。
死锁和死循环:死锁是因为无法获得资源陷入僵持,死循环是一直在跑,但跑不出去。
JDK8中ConcurrentHashMap参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计。
红黑树
参考教程
看完教程后可以直接理解具体操作
用五条定义来近似平衡树,并不追求完美平衡,以此换速度。用红黑结点将2-3-4树转化为二叉树,也表达了一种父子关系。插入时将插入结点 视作红色节点,避免红-红左倾、右倾或单链结构的产生;删除时进行染色,通过转化为3/4结点的方式避免删除2结点。
JDK1.7中ConcurrentHashMap分为好几个segment,每个segment加锁。segment中再次hash找到entry。内部大量采用CAS操作,这里我简要介绍下CAS。CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。
悲观锁
访问加锁,一个线程访问完另一个再访问。
乐观锁
先获取一个值,操作时比较一下获取的值和当前值是否相同,如果相同,进行操作。
乐观锁和悲观锁
JDK8中彻底放弃了Segment转而采用的是Node,其设计思想也不再是JDK1.7中的分段锁思想。
Node:保存key,value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的可见性。
volatile关键字
相对加锁较为轻量级,有两个特点:一是更新时对内存可见,即更新时强制将线程本地缓存刷新到主内存中,并使得其他线程缓存无效;二是防止编译器对指令重排。该关键字不适合复合操作,如ans++,即不保证原子性。
部分源码分析:深入浅出ConcurrentHashMap1.8
明天看redis 2022/3/4
# 利用mybatis自身本地缓存结合redis实现分布式缓存
mybatis
MyBatis 是一个半自动化的ORM框架 (Object Relationship Mapping)
原理及使用
ORM框架
为了解决JDBC的繁琐过程和重复代码,借鉴面向对象的思想,让程序员以操作对象的方式操作数据库,无需编写 sql 语句。通过转换对象属性和表字段解决。
ORM思想,里面也介绍了MyBatis的实现。
mapper
一个映射,告诉mybatis执行什么sql语句,结果放在哪里。
每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。
作用域
生效范围。目的是收到请求打开sql session,请求结束立刻关闭。
mybatis一级二级缓存
参考链接
Mybatis的一级缓存是默认开启的,其实指的是Mybaits中SqlSession对象的缓存。当我们执行查询之后,查询的结果会同时存入到 SqlSession为我们提供的一块区域中。该区域的结构是一个Map。当我们再次查询同样的数据,Mybatis会先去SqlSession中查询是否存在,存在的话直接返回,而不会再去数据库查询。
Mybatis的二级缓存是默认关闭的,它指的是Mybatis中SqlSessionFactory对象的缓存,由同一个SqlSessionFactory对象创建的SqlSession共享其二级缓存。
在做自定义 Cathe 的时候自定义一个类,像 PerpetualCache(mybatis二级缓存用的类,主要建立在HashMap上) 内部一样,并且实现 Cache 接口,在mapper配置文件中指定使用这个我们自定义的RedisCache即可。不过在这之前我们需要先想一想如何获取redisTemplate对象,在操作RedisCache的时候我们肯定需要用到RedisTemplate对象。
RedisTemplate
用来操作Redis,获得key和value之类的一组API。
Redis
IO多路复用
IO多路复用只需要一个进程就能够处理多个套接字,从而解决了上下文切换的问题。利用了reactor设计思想,同时监控多个文件描述符(FD)状态,有可读可写的就去处理。
套接字
网络通信交换的一段
在实现上,因为 select 函数是作为 POSIX 标准中的系统调用,在不同版本的操作系统上都会实现,所以将其作为保底方案,每次都要遍历所有fd,复杂度O(n);
POSIX标准
可移植操作系统接口 Portable Operating System Interface of UNIX
Redis 会优先选择时间复杂度为 O(1) 的 I/O 多路复用函数作为底层实现,包括 Solaries 10 中的 evport、Linux 中的 epoll 和 macOS/FreeBSD 中的 kqueue,上述的这些函数都使用了内核内部的结构,并且能够服务几十万的文件描述符。
多路复用函数底层实现
select:被调用了,让我把所有FD拷贝到内核轮询一遍,看看是哪几个请求要读写;
epoll:FD提前加入内核,以红黑树的形式存储。有数据来了的FD自己加入等待队列,等着我被调用的时候处理。
下次看跳表 2023/3/5
跳表
链表在有序的情况下查询效率低,因此给它建立了好几层索引。但这样插入会有点麻烦。skiplist想了一个很灵性的解决方法,每次插入随机一层出来建立索引,在一些复杂数学计算后可以证明复杂度还是log级的。
淘汰策略
定期删除(每100ms抽查删除)+惰性删除(调用时检查)+内存淘汰(LRU)
缓存和数据库双写一致性
1. 先更新缓存再更新数据库
原文直接否定了这个方案,一开始有点困惑,因为体系结构里就是这么做的。仔细思考一下这里的缓存是分布式的,不能保证先访问缓存。
2. 先更新数据库再更新缓存
反例:A更新数据库,B更新数据库,B更新缓存,A更新缓存。
并且每次都更新缓存在读少的情景下不划算。
3. 先删除缓存再更新数据库
反例:A写,B读。A删除缓存,B没读到缓存,B读数据库,B读到旧值,B把旧值写到缓存,A更新数据库。数据不一致。
4. 先更新数据库再更新缓存
好方案。
mysql主从复制、读写分离
主从复制:主数据库更新后记录到binlog,通知从数据库,将更新内容写进relaylog,从数据库根据它更新。
读写分离:字面意思。可以在JDBC层或DataSource层加入。如果同一线程内既有写又有读,那么读从主数据库读。顺带一提主数据库叫master,从数据库直接叫slave。
下次看缓存穿透和缓存雪崩 2022/3/6
接下来有三种大量访问打在数据库上的情况。
缓存穿透
访问很多缓存里不存在的key,导致大量访问打到数据库上
缓存击穿
大量访问同一个key,然后key失效了。
解决:熔断或设置热点key不过期。
缓存雪崩
很多key同时失效,导致访问这些key的请求打在数据库上。
解决:熔断,分库分表,在原有的失效时间上加随机值。
(有种当年背地理的感觉
todo 后面看看redis源码分析,好像有哨兵模式什么的
mysql binlog
事务
数据库要执行的操作,不可分割,不可被其他事务干扰。
事务提交:将所有的DML(insert、update、delete)语句操作历史记录和底层硬盘数据来一次同步。
脏读、幻读、不可重复读
脏读:读了其他事务没有提交的数据。事务A还没提交,事务B读到了修改的数据,这时A回滚操作,B读了假的数据。
幻读:比如事务A读了一个值,发现不存在,准备插入;事务B过来,把这个值插入了;事务A插入的时候发现值已经存在,很困惑。总结起来就是事务B通过增加数据,影响了A操作的结果。
不可重复读:事务A读了一个值,事务B过来,修改了这个值,事务A再读,发现数不对了,很困惑。
事务的隔离级别
Read Uncommitted
一个事务可以读另一个事务未提交的数据
会出现脏读、幻读、不可重复读。
Read Commited
一个事务只能读其他事务提交的数据。避免脏读。
Repeatable Read
读取事务开启后,不允许进行修改操作。避免脏读、不可重复读。
Serializable
所有事务排队进行。
终于决定卸载vs2013了。漫长的卸载过程。看点别的换换口味
ElasticSearch
一个json格式存储的面向文档型数据库。主要采用倒排索引的方式提高查询速度(并不那么在乎插入删除)。
倒排索引
正常的索引是key-value,倒排索引是每个value,存储有哪些key。很适合搜索引擎用。
对索引的优化:时间序列数据库的秘密 (2)——索引
接下来研究下集群管理。
有很多个结点,结点数量少的时候它们每台都可以接收请求、处理数据等等。结点数量多的时候可以给结点分工以提高效率。
Elasticsearch 8.X 节点角色划分深入详解
以及,ES中每个结点都可以做协调结点,大家都能通过路由找到存放位置。
还有ES的一个特点,分片。
配了好几天大数据的环境,学一下大数据全家桶是干嘛的。
ZooKeeper(动物管理员)
Kafka
未完待续
Hutool
Hutool是Hu + tool的自造词,前者致敬我的“前任公司”,后者为工具之意,谐音“糊涂”,寓意追求“万事都作糊涂观,无所谓失,无所谓得”的境界。
一个Java工具包,探索一下。
布隆过滤
判断一个元素是否在一个集合里。有n个哈希函数,和一个很大的二进制向量。新来一个元素,计算出n个哈希值,把向量对应位置置1.查询的时候如果n个位置有0,则不在集合里;如果都是1,则有一定概率在集合里。
Docker
用于解决分布式系统分区问题,也就是一份数据放到哪个节点上。首先我们想到的是取模,但取模当模数改变的时候对映射影响很大,容易造成缓存雪崩。我们肯定想缩小对映射的影响,那么可以使得模数固定,比如232.但实际上我们没有这么多机器,该怎么把哈希出来的数映射到机器上呢。脑部一个首尾相连的环,上面是0-232-1,我们均匀地把现有的结点放上去,然后针对每个哈希出来的数,找离它最近的结点就可以了。这样如果增加一台机器,或者一台机器挂掉,只影响它两侧的映射。
从这个角度再看一看es的分片,上次没看懂(
ES分片
分片和分区差不多,都是如何均衡地放东西。ES将分片分为主分片和复制分片,其中复制分片防止数据丢失,是一个备份。主节点呢个数是确定的,和上面模2的32次方避免一样的问题。主分片和复制分片都可以处理读请求。
接下来是存储的一些优化。
1. 索引不可修改,只有标记删除,隔一段时间更新一次;
2. 为更新添加索引。
3. 每次更新先缓存在内存里,隔一段时间写入硬盘;
4. 为了避免数据丢失,增加事务日志。
共识算法:Paxo
现在你已经理解这个算法的规则了,来看看证明吧!
复述一下:有一些机器提出一些提议,这些提议都有编号,我们认为编号大的提议更新,想要尽可能采取新的提议;但提议提出去后呢不知道几百年才能通知到每台机器,有时候新的提议到的还比老提议早;而且我们其实也不是很关心提议够不够新,如果新提议在一百年后才能被大多数机器收到,那这个提议不要也罢。综上,我们想要尽快达成一个共识,且这个共识是大多数人(超过一半)收到的最新版本。
机器开始提议了啊,现在我们看看收到提议的机器怎么处理:如果我之前没有收到过提议,那么这个提议不错,告诉提议者我采纳;如果我收到过提议,但到达的提议更新,那么我也告诉提议者采纳;如果到达的提议比我现在采纳的旧,那我不理它。
再来看提议者。提议者呢收到了一些反馈,如果我的提议被大多数人采纳,那我试图向所有机器推广我的提议,你们都给我更新成我的数据。这时候有的机器不答应了,明明我现在采纳的提议更新,为什么要更新成你的数据;但有的机器采纳的版本小于等于现在发布的,那肯定告诉提议者我没问题。
这时提议者又收到了一些反馈,如果同意的机器超过一半,那么已经产生了结果,强制执行;如果没有超过一半,说明大多数机器采纳了更新的提议,你过时啦。
如此反复,直到产生一个结果。
证明懂了一点,但没有全懂,以后再说。
2023/3/24