7 数据库以及分布式事务
7.1 数据库的基本概念及原则
7.1.1 存储引擎
常用的存储引擎主要有MyISAM,InnoDB,Memory,Archive和Federated.
1 MyISAM
MyISAM不支持数据库事务、行级锁和外键,因此在在插入或者UPDATE数据时需要锁定整个表,效率较低。
MyISMA的特点是执行读操作的速度快,且占用的内存和存储资源较少。它在设计之初假设数据被组织成固定长度的记录,并且是按顺序存储的。在查找数据时,MyISAM直接查找文件的OFFSET,定位比innnodb要快(InnoDB寻址时要先映射到块,再映射到行)
总体来说,MyISAM的缺点是更新数据慢且不支持事务处理,优点是查询速度快。
2 InnoDB
InnoDB为MySQL提供了事务支持,回滚、奔溃修复能力,多版本并发控制,事务安全的操作。InnoDB底层存储结构为B+树,B+树的每个节点都对应InnoDB的一个page,Page的大小是固定的,一般设置为16KB。其中非叶子节点只有键值,叶子节点包含完整的数据。
InnoDB适用于以下需求的场景:
- 经常有数据更新的表,适合处理多重并发的更新请求
- 支持事务
- 支持灾难恢复(通过bin-log日志恢复)
- 支持自动增加列属性 auto_increment
7.1.2 创建索引的原则
创建索引是我们提高数据库查询数据效率最常用的方法,也是很重要的方法。下面是常见的创建索引的原则。
- 选择唯一性索引:唯一性索引一般基于Hash算法实现,可以快速、唯一地定位某条数据。
- 为经常需要排序、分组和联合操作的字段建立索引
- 为经常作为查询条件的字段建立索引
- 限制索引的数量:索引越多,数据更新表越慢,因为在数据更新是会不断计算和添加索引。
- 尽量使用数据量少的索引:如果索引的值很长,则占用的磁盘较大,查询速度会受到影响。
- 删除不再使用或者者很少使用的索引。
- 尽量选择区分度高的列作为索引:区分度表示字段值不重复的比例。
- 索引列不能参与计算:带函数的查询不建议参与索引。
- 尽量扩展现有索引:联合索引的查询效率比多个独立索引高。
7.1.4 数据库事务
数据库事务执行一些列基本操作,这些基本操作组成一个逻辑单元一起向数据库提交,要么都提执行,要么都不执行。事务是一个不可分割的工作逻辑单元,事务必须具备以下4个属性,简称ACID属性
- 原子性(Atomicity) 事务是一个完整操作,参与事务的逻辑单元要么都执行,要么都不执行。
- 一致性(Consistency) :在事务执行完毕时(无论正常执行完毕还是异常退出,数据都必须处于一致状态)
- 隔离性(Lsolation):对数据进行修改的所有并发事务都是彼此隔离的,它不应以任何方式依赖或影响其他事务
- 永久性(Durability):在事务执行完成后,对数据的修改将被持久化到永久性存储中。
7.2 数据库的并发操作和锁
数据库的并发控制一般采用三种方式,分别是乐观锁、悲观锁和时间戳。
7.2.1 数据库的并发策略
1 乐观锁
乐观锁在读数据时,认为被人不会去写其所读的数据;悲观锁刚好相反,觉得自己读数据时,别人刚好在写自己刚读的数据,态度比较保守;时间戳在操作数据时不加锁,而是通过时间戳来控制并发出现的问题。
2 悲观锁
悲观锁指在其修改某条数据的时候,不允许别人读取该数据,直到自己的整个事务都提交并释放锁,其他用户才能访问该数据。悲观锁又可分为排他锁和共享锁。
3 时间戳
时间戳指在数据库表中额外加也给时间戳列TimeStamp。每次读数据时,都把时间戳也读出来,在更新数据时把时间戳也加一,在提交之前跟数据库的该字段比较一次,如果比数据库的值大,就允许保存,否则不允许保存。这种处理方法虽然不使用数据库小提高你提供的锁机制,但是可以大大提高数据库处理的并发度。
7.2.2 数据库锁
1 行级锁
行级锁指对某行数据加锁,是一种排他锁,防止其他事务修改此行。在执行一下数据库操作是,数据库会自动应用行级锁
- INSERT 、UPDATE、DELETE、SELECT FOR UPDATE
- commit或ROLLBACK
2 表级锁
表级锁指对当前操作的整张表加锁,它的实现简单、消耗资源少,被大多数存储引擎支持。最常使用的是MyISAM与InnoDB都支持表级锁。表级锁分为表共享读锁与表独占写锁。
3 页级锁
页级锁的锁定粒度介于行级锁和表级锁之间。表级锁的加锁速度快,但冲突多,行级锁冲突少,但加锁速度慢。页级锁在二者之间做了平衡,一次锁定相邻的一组记录。
4 基于redis的分布式锁
数据库锁是基于单个数据库实现的,在我们的业务跨多个数据库时,就要使用分布式锁来保护数据的一致性。
使用redis实现一个分布式锁的流程:Redis实现的分布式锁以Reids setnx命令为中心实现,setnx是redis写入操作命令,在且仅在key不存在时,则通插入一个 key为val的字符串,返回1,若key存在,则什么也不做返回0.
7.2.3 数据库分表
数据库分表有垂直分表和水平切分两种
- 垂直分表:将表按照功能模块,关系密切程度将表划到不同的库中。例如,我们会创建定义数据库workDB,商品数据库payDB,用户数据库userDB,日志数据库logDB等,分别用于存储项目定义表,商品定义表,用户数据表、日志数据库等。
- 水平切分:在一个表中的数据量过大时,可以把该表的数据按照某种规则如userID散列进行划分,然后将其存储到多个具有相同表结构的数据库上。
7.3 数据库分布式事务
7.3.1 CAP
CAP原则又称CAP定理,指的是一个分布式系统中,一致性(Consistency)、可用性(Avaliability)、和分区容错性(Partition tolerance)三者不可兼得。
- 一致性:在分布式系统中的所有数据备份中,在同一个时刻是否具有同样的值(等同于所有节点都访问同一份最新的数据副本)
- 可用性:在集群中一部分节点发生故障后,集群整体能否响应客户端的读写请求(对数据库更新具备可靠性)
- 分区容错性:系统如果不能在实现内达成数据的一致性,就意味着发生了分区,必须就当前操作在C和A中选择,以实际效果而言,分区相当于通信的时限要求。
7.3.2 两阶段提交协议
分布式事务指设计操作多个数据库的事务,在分布式系统中,各个节点之间在物理上是相互独立,通过网络进行沟通和协调。
二阶段提交指在计算机网络以及数据库领域内,为了使分布式数据库的所有节点在进行事务提交时都保持一致性而设计的一种算法。在分布式系统中,每个节点虽然都可以知道自己的操作是否成功,却无法知道其他节点的操作是否成功。
在一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来后通过难以掌控所有节点的操作结果,并最终确定这些节点是否真正提交操作结果。因此,二阶段提交的算法思路可以概括为:参与这将操作成败通知协调者,再由协调者根据所有参与者的反馈决定各参与者是提交操作还是中止操作。
1 Prepare(准备阶段)
事务协调者(事务管理者)给每个参与者(资源管理器)都发送PRepare消息,每个参与者要么直接返回失败,要么本地执行事务,写本地的redo和undo日志但不提交,是一种“万事俱备,只欠东风”的状态。
2Commit(提交阶段)
如果协调者接受到了参与者的失败消息或者等待超时,则直接给每个参与者都发送回滚消息,否则发送提交消息,参与者根据协调者的指令执行提交或者回滚操作,释放在所有事务处理过程中使用的锁资源。
3 两阶段提交的缺点
两阶段提交的缺点如下:
- 同步阻塞问题:在执行过程中,所有参与者的任务都是阻塞执行的,
- 单点故障:所有请求都需要经过协调者,在协调者发生故障时,所有参与者都会被阻塞
- 数据不一致:在二阶段提交的第二阶段,在协调者向参与者发送了commit请求后后发生了局部网络异常,或者在发送commit请求过程中协调者发生了故障,导致只有一部分参与者接受到commit请求,于是整个分布式系统出现了数据不一致的情况,这也被称为脑裂
- 协调者宕机后事务状态丢失:
7.3.3 三阶段提交协议
三阶段提交是二阶段提交的改进版本。具体改进如下
- 引入超时机制:在协调者和参与者中引入超时机制,如果协调者长时间接受不到参与者的反馈,则认为参与者执行失败
- 在第一阶段和第二阶段都加入一个有准备阶段,以保证最后任务的提交之前各参与节点的状态都是一致的。也就是说,除了引入超时机制,三阶段提交协议把两阶段提交协议的准备阶段再次一分为二。
7.3.4 分布式事务
1 传统事务
传统事务遵循ACID原则,即原子性、一致性、隔离性和持久性
2 柔性事务
在分布式事务领域,基于CAP理论以及BASE理论,阿里巴巴提出了柔性事务的概念,BASE理论是CAP理论的延伸,包括了基本可用、柔性状态、最终一致性三个原则,并基于这三个原则设计出了柔性事务。
通常所说的柔性事务分为:两阶段型、补偿型、异步确保型、最大努力通知型。
两阶段十五指分布式事务的两阶段提交,对应技术上的XA和JAT/JTS,是分布式环境下事务处理的典型模式。
TTC型事务(Try Confirm Cancel) 为补偿型事务,是一种基于补偿的事务处理模型,服务器A发起事务,服务器B参与事务,如果服务器A的事务和服务器B的事务都顺序李执行完成并提交,则整个事务执行完成。但是如果事务B执行失败,事务B本身就回滚,这时事务A已被提交,所以需要执行一个补偿操作,将已经提交的事务A执行的操作进行回滚操作,恢复到未执行事务A的状态。需要注意的是,发起提交的一般是主业务服务,而状态补偿的一般是业务活动管理者,因为活动日志被存储在业务活动管理中,补偿需要依靠日志进行恢复。TCC事务模型牺牲了一定的隔离性和一致性,但是提高了事务的可用性。
异步确保型事务指将一系列同步的事务操作修改为基于消息队列异步执行的操作,来避免分布式事务中同步阻塞带来的数据操作性能下降。写业务数据A触发后将执行以下流程
- 业务A的模块在数据库上执行数据更新操作
- 业务A调用写消息数据模块
- 写消息日志模块将数据库的写操作状态写入数据库A中
- 写消息日志模块将写日志发送给消息服务器
- 读消息日志模块接受操作日志
- 读消息数据调用写业务B的模块
- 写业务B更新数据到数据库B
- 写业数据B的模块发送异步消息更新数据库A中的写消息日志状态,说明自己已经完成了异步数据更新操作。
8 分布式缓存的原理及应用
缓存是将需要频繁访问的数据存放在内存中以加快用户访问速度的一种技术。缓存分为进程级缓存和分布式缓存,进程级指将数据缓存在服务器内部,通过Map,List等结构实现存储。分布式缓存指将数据缓存在服务内部,以便于缓存的统一管理和存取,常见的分布式缓存系统有Echache、Redis、Memcached。
8.3 Redis的原理及应用
Redis是一个来源的内存中数据结构存储系统,可以用作内存数据库、缓存和消息中间件,支持多种类型的数据结构。例如字符串,Hash(散列)、List(列表)、Set(集合)、ZSet(有序集合)、Bitmap(位图),HyperLogLog(超级日志)和Geosptial(地理空间)。Redis内置了复制,Lua脚本,LRU驱动事件,事务和不同级别的磁盘持久化,并通过Redis哨兵模式和集群模式提供高可用性。
8.3.1 Redis的原理
Redis不但支持丰富的数据类型,还支持分布式事物、数据分片、数据持久化等功能,是部分是系统中不可或缺的内存数据库服务。
2 Redis管道
Redis是基于请求/响应协议的TCP服务,在客户端向服务器发送一个查询请求后,需要监听Socket的返回,该监听过程一直阻塞,知道服务器有结果返回,由于Redis集群是部署在多个服务器上的,所以Redis请求/响应模型在每次请求时都要跨网络在不同的服务器之间传输数据,这样每次查询都存在一定的网络延迟。由于服务器一般采用多线程处理业务,并且内存操作效率很高。所以一次请求延时20MS,则多次请求的网络延迟会不断累加。也就是说,在分布式环境下,Redis的瓶颈主要体现在网络延迟上。
Redis的管道技术指在服务端未响应时,客户端可以继续向服务器发送请求,并最终一次性读取所有服务端的响应。管道技术能够减少客户端和服务器交互的次数,将客户端的请求批量发送给服务端,服务端针对批量请求分别查询并统一回复,能显著提高Redis的性能。
Redis管道技术基于SpringBoot的使用如下:
void executePipelined() {final List<Object> list = redisTemplate.executePipelined(new RedisCallback<Object>() {@Nullable@Overridepublic Object doInRedis(RedisConnection connection) throws DataAccessException {//1 打开 Piplineconnection.openPipeline();//2 执行批量操作for (int i = 0; i < 1000; i++) {String key = "key_" + i;String value = "value_" + i;// connection.set(key.getBytes(StandardCharsets.UTF_8), value.getBytes(StandardCharsets.UTF_8));connection.get(key.getBytes(StandardCharsets.UTF_8));}//3 返回结果,这里返回nullreturn null;// 4 redisTemplate将会把最终结果汇总到外层的list中。}});//5 查看管道批量操作的返回结果for (Object dateTime : list) {System.out.println(dateTime);}}
3 Redis事务
Redis支持分布式环境下的事务操作,其事务可以一次执行多个命令,事务中的所有命令都会序列化地顺序执。在事物在执行过程中,不会被其他客户端发送来的命令请求打断。服务器在执行完事务中的所有命令后,才会继续处理其他客户端的其他请求。Redis的事务操作分为开启事务、命令入队列、执行事务三个阶段。Redis的事务执行流程如下
- 事务开启:客户端执行Multi命令开启事务。
- 提交请求:客户端提交命令到事务
- 任务入队列:Redis将客户端请求放入事务队列中等待执行
- 入队状态反馈:服务器返回QURUD,表示命令已经被放入事务队列。
- 执行命令:客户端通过Exec执行事务
- 事务执行错误:在Redis事务中如果某条命令执行错误,则其他命令会继续执行,不会回滚。可以通过Watch监控事务执行的状态并处理命令执行错误的异样情况。
- 执行结果反馈:服务器向客户端返回事务执行的结果。
void transactionSet(Map<String ,Object> commandList){//1.开启事务权限redisTemplate.setEnableTransactionSupport(true);try {//2. 开启事务redisTemplate.multi();//3. 执行事务命令for (Map.Entry<String, Object> entry : commandList.entrySet()) {final String key = entry.getKey();final Object value = entry.getValue();redisTemplate.opsForValue().set(key,value.toString());}//4. 执行成功,提交事务redisTemplate.exec();} catch (Exception e) {redisTemplate.discard();} finally {}}
4 Redis发布、订阅
Redis发布、订阅是一种消息通信模式:发送者(Pub) 向频道(Channel)发送消息,订阅者(Sub)接受频道上的消息。Redis客户端可以订阅任意数量的频道,发送者也可以向任意频道发送数据。
5 Redis集群数据复制的原理
Redis集群提供了复制功能,可以实现在主数据库中的数据更新后,自动将更新的数据同步到从数据库。一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库。
6 Redis的持久化
Redis支持RDB 和AOF两种持久化方式
- RDB(Redis DataBase) :RDB在指定的时间间隔内对数据进行快照存储,RDB的特点在于:文件格式紧凑,方便进行数据传输和数据恢复;在保存rdb快照文件时父进程会fork出一个子进程,有子进程完成具体的持久化工作,所以可以最大化Redis的性能。同时,与AOF相比,在恢复大的数据集时会更快一些。
- AOF(Append Of File):AOF记录服务器每次写操作,在Redis重启时会重放这些命令来恢复原数据,AOF命令以Redis协议追加和保存每次写操作到文件末尾,Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大,AOF的特点是:可以使用不同的fsync策略(无fsync 每秒 fsync 每次写的时候fsync)将操作追加命令到文件中,操作效率高:同时,AOF文件是日志的格式,更容易被理解和操作。
7 Redis集群模式以及工作原理
1 主从模式
所有的写请求都被发送到主服务器上,再由主服务器将数据同步到从数据库。主数据库主要用于执行写操作和数据同步,从数据库主要用于执行读操作缓解系统的读压力。模型如上图。
Redis的一个主库可以拥有多个从库,从库可以作为其他数据库的主库。
2 哨兵模式
在主从模式上添加了一个哨兵的角色来监控集群的运行状态。哨兵通过发送命令让Redis服务器返回其运行状态。哨兵是一个独立运行的进程,在监测到Master宕机时会自动将Slave切换从master,然后通过发布与订阅模式通知其他从服务器修改配置文件,完成主备热切。
3 集群模式
集群实现了在多个Redis节点之间进行数据分片和数据复制。基于Redis集群的数据自动分片能力,我们能够方便地对Redis集群进行横向扩展,以提高Redis集群的吞吐量。基于Redis集群的数据复制能力,在集群中的一部分节点失效或者无法进行通信时,Redis仍然可以基于副本数据对外提供服务,这提高了集群的可用性。
Redis集群遵循如下原则:
- 所有Redis节点彼此通过 PING-PING机制互联,内部使用二进制协议优化传输速度和宽带
- 在集群中超过半数的节点检测到某个节点Fail后将该节点设置为Fail状态
- 客户端与Redis节点直连,客户端连接急群中任何一个可用节点即可对集群进行操作。
- Redis-Cluster把所有的物理节点都映射到0~16383的slot(槽)上,Cluster负责维护每个节点上的数据槽分配。Redis的具体数据分配策略为:在Redis集群中会先对key使用CRC16算法算出一个结果,然后把结果对16384求余数,这样每个key都会对应一个编号为0~16383的散列槽:Redis会根据节点的数量大致均等地将散列槽映射到不同的节点。
8.4 分布式缓存设计的核心问题
分布式缓存设计的核心问题是以哪种方式进行缓存预热和缓存更新,以及如何解决缓存雪崩、缓存穿透、缓存降级等问题。
8.4.1 缓存预热
缓存预热是指用户请求数据前香江数据加载到缓存系统中,用户查询事先被预热的缓存数据,以提高系统查询效率。缓存预热一般有系统启动加载、定时加载等方式。
8.4.2 缓存更新
指数据发生变化之后及时将变化后的数据更新到缓存中,常见的缓冲更新有如下四种。
- 定时更新:定时将底层数据库内的数据更新到缓存中,该方法比较简单,适合需要缓存的数据量不是很大的应用场景。
- 过期更新:定时缓存中过期的数据更新为最新的数据并更新缓存过期时间
- 写请求更新:在用户写请求是先写数据库同时更新缓存,这适用于用户对缓存数据和数据库的数据有实时强一致性要求的情况
- 读请求更新:在用户有读请求是,先判断该请求数据的缓冲是否存在或过期,如果不存在或者已过期,则进行底层数据库查询并将查询结果更新到缓存中,同时将查询结果返回给用户。
8.4.3 缓存淘汰策略
在缓存数据过多时需要使用某种淘汰算法决定淘汰哪些数据。常用的淘汰算法有以下几种。
- FIFO(先进先出):判断被存储的时间,离目前最远的数据集优先被淘汰
- LUR(Least Recently Used 最近最少使用):判断缓存最新被使用的时间,距离当前时间最远的书优先被淘汰。
LFU(Least Frequently Used ):在一段时间内,被使用次数最少的缓冲优先被淘汰
8.4.4 缓存雪崩
缓存雪崩指在同一时刻由于大量缓存失败,导致大量原本应该访问缓存的请求都去查询数据库,而对数据库的CPU和内存造成巨大压力,严重的话会导致数据库宕机,从而引起一系列连锁反应,事整个系统奔溃,一般有如下三种处理方法。
请求加锁:对于并发量不是很多的应用,使用请求加锁排队的方案防止过多请求数据库。
- 失效更新:为每一个缓存数据都在呢个价过期标记来记录缓存数据是否失效,如果缓存标记失效,则更新数据库缓存。
- 设置不同的失效时间:为不同的数据设置不同的缓冲失效时间,防止同一时刻有大量的数据失效。
8.4.5 缓存穿透
缓存穿透指由于缓存系统故障或者用户频繁查询系统中不存在(在系统中不存在,在自然数据库和缓存中都不存在)的数据,而这时请求穿过缓存不断被发送到数据库,导致数据库过载,进而引发一连串并发问题。
常见的解决缓存穿透问题的方法有布隆过滤器和cache null策略。
- 布隆过滤器:指将所有可能存在的数据都映射到一个足够大的Bitmap中,在用户发起请求时首先经过布隆过滤器的拦截,一个一定不存在的数据会被这个布隆过滤器拦截,从而避免对底层存储系统带来查询上的压力。
- cache null策略:其实cache null策略的核心原理是在缓存中记录一个短暂的数据在系统中是否存在的状态,如果不存在,则直接返回null,不再查询数据库从而避免缓存穿透到数据库上。
8.4.6 缓存降级
缓存降级指由于访问量剧增导致服务出现问题时,优先保障核心业务的运行,减少或关闭非核心业务对资源的使用。常见的降级策略如喜爱:
- 写降级:在写请求增大时,可以只进行Cache的更新,然后将数据异步更新到数据库中,保障最终一致性,即将写请求从数据库降级为cache。
- 读降级:在数据服务负载过高或数据库系统故障时,可以只对cache进行读取并将结果返回给用户,在数据库服务正常后再去查询数据库,即将读请求从数据库降级为Cahce。这种方式适用于对数据库实时性要求不高的场景,保障了在洗头工发送故障的情况下用户仍然能够继续访问到数据,只是访问到的数据相对有延迟。
