diff --git a/Database.md b/Database.md index 5a936c4..9c5148d 100644 --- a/Database.md +++ b/Database.md @@ -1141,7 +1141,7 @@ InnoDB实现回滚靠的是undo log。当事务对数据库进行修改时,Inn **一致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。** -数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变)。 +数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变)。假如A账户给B账户转10块钱,不管成功与否,A和B的总金额是不变的。 @@ -1167,7 +1167,7 @@ InnoDB实现回滚靠的是undo log。当事务对数据库进行修改时,Inn 隔离性追求的是并发情形下事务之间互不干扰。主要分为两个方面: -**① 锁机制保证隔离性**:(一个事务)写操作对(另一个事务)写操作的影响 +**① 加锁机制保证隔离性**:(一个事务)写操作对(另一个事务)写操作的影响 事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其它事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。 @@ -1234,10 +1234,20 @@ MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议 在事务的并发操作中,不做隔离操作则可能会出现 **脏读、不可重复读、幻读** 问题: -- **脏读**:**事务A中读到了事务B中未提交的更新数据内容**。然后B回滚操作,那么A读取到的数据是脏数据 -- **不可重复读**:**事务A读到事务B已经提交后的数据**。即事务A多次读取同一数据时,返回结果不一致 -- **幻读**:事物A执行select后,事物B**增或删**了一条数据,事务A再执行同一条SQL后发现多或少了一条数据 -- **第一类丢失更新:** A事务撤销事务时,覆盖了B事务提交的事务(现代关系型数据库中已经不会发生) +- **脏读**:**指一个事务读取到了另一个未提交事务修改过的数据** + + 事务A中读到了事务B中未提交的更新数据内容,然后B回滚操作,那么A读取到的数据是脏数据。 + +- **不可重复读**:**同一个事务内,前后多次读取,读取到的数据内容不一致** + + 事务A读到事务B已经提交后的数据,即事务A多次读取同一数据时,返回结果不一致。 + +- **幻读**:**指一个事务先根据某些搜索条件查询出一些记录,在该事务未提交时,另一个事务写入了一些符合那些搜索条件的记录(如insert、delete、update),再次查询出的结果则出现不一致** + + 事物A执行select后,事物B增或删了一条数据,事务A再执行同一条SQL后发现多或少了一条数据。 + +- **第一类丢失更新:** A事务撤销事务时,覆盖了B事务提交的事务(现代关系型数据库中已经不会发生) + - **第二类丢失更新:** A事务提交事务时,覆盖了B事务提交的事务(是不可重复读的特殊情况) **小结**:不可重复读的和幻读很容易混淆,**不可重复读**侧重于**修改**,**幻读**侧重于**新增或删除**。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。查看 `mysql` 事务隔离级别:`show variables like 'tx_iso%';`。 @@ -1257,11 +1267,11 @@ MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议 ### Read Uncommitted(读未提交) -**即读取到了其它事务未提交的内容(可能会被回滚)**。在该隔离级别,**所有事务都可以看到其他未提交事务的执行结果**。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为**脏读(Dirty Read)**。 +只限制了两个数据**不能同时修改**,但即使事务未提交也会**读取到其它事务未提交的内容(可能会被回滚)**。会有**脏读、重复读、幻读**的问题,读取未提交的数据,也被称之为**脏读(Dirty Read)**。 **特点**:最低级别,任何情况都无法保证 -**读未提交的数据库锁情况** +**数据库锁情况** - 读取数据:**未加锁,每次都读到最新数据,性能最好** - 写入数据:**只对数据增加行级共享锁,写完释放** @@ -1270,13 +1280,13 @@ MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议 ### Read Committed(读已提交) -**即读取到了其它事务已提交的内容**。一个事务只能看见已经提交事务所做的改变。这种隔离级别也支持所谓的**不可重复读(Nonrepeatable Read)**,因为同一事务的其他实例在该实例处理期间可能会有新的commit,所以同一select可能返回不同结果。 +当前事务只能读取到其它事务**已提交**的数据。因同一事务的其它实例在该实例处理期间可能会有新的commit,所以同一select可能返回不同结果,这就是所谓的**不可重复读(Nonrepeatable Read)**。该隔离级别**解决了脏读**问题,但还是会存在**重复读、幻读**问题。 **特点**:避免脏读 -**脏读解决方案:基于乐观锁理论的MVCC(多版本并发控)实现** +**脏读解决方案**:基于乐观锁理论的MVCC(多版本并发控)实现 -**读已提交的数据库锁情况** +**数据库锁情况** - 读取数据:**加行级共享锁(读到时才加锁),读完后立即释放** - 写入数据:**在更新时的瞬间对其加行级排它锁,直到事务结束才释放** @@ -1285,15 +1295,15 @@ MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议 ### Repeatable Read(可重复读) -**它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行**。但会导致**幻读 (Phantom Read)**问题。 +限制了读取数据时**不可以进行修改**,所以**解决了不能重复读**的问题。但是读取范围数据的时候,是可以插入或删除数据,所以还会存在**幻读(Phantom Read)**问题。 **幻读** 是户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当该用户再读取该范围的数据行时,会发现有新的“幻影” 行。 **特点**:避免脏读、不可重复读。MySQL默认事务隔离级别 -**不可重复读解决方案:基于乐观锁理论的MVCC(多版本并发控)实现** +**不可重复读解决方案**:基于乐观锁理论的MVCC(多版本并发控)实现 -**可重复读的数据库锁情况** +**数据库锁情况** - 读取数据:**开始读取的瞬间对其增加行级共享锁,直到事务结束才释放** - 写入数据:**开始更新的瞬间对其增加行级排他锁,直到事务结束才释放** @@ -1302,11 +1312,11 @@ MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议 ### Serializable(可串行化) -**指一个事务在执行过程中完全看不到其他事务对数据库所做的更新**。当两个事务同时操作数据库中相同数据时,如果第一个事务已经在访问该数据,第二个事务只能停下来等待,必须等到第一个事务结束后才能恢复运行。因此这两个事务实际上是串行化方式运行。 +所有事务都是进行**串行化顺序**执行的。可以避免**脏读**、**不可重复读**与**幻读**所有并发问题。但该事务隔离级别下,事务执行很耗性能。 **特点**:避免脏读、不可重复读、幻读 -**可序列化的数据库锁情况** +**数据库锁情况** - 读取数据:**先对其加表级共享锁 ,直到事务结束才释放** - 写入数据:**先对其加表级排他锁 ,直到事务结束才释放** diff --git a/Middleware.md b/Middleware.md index d375244..561bbca 100644 --- a/Middleware.md +++ b/Middleware.md @@ -1260,6 +1260,112 @@ cluster_stats_messages_received:3021 +## 拓展方案 + +### 分区(Partitioning) + +指在面临**单机**的**存储空间**瓶颈时,即将全部数据分散在多个Redis实例中,每个实例不需要关联,可以是完全独立的。 + + + +**使用方式** + +- 客户端处理 + 和传统的数据库分库分表一样,可以从**key**入手,先进行计算,找到对应数据存储的实例在进行操作。 + - **范围角度**,比如orderId:1~orderId:1000放入实例1,orderId:1001~orderId:2000放入实例2 + - **哈希计算**,就像我们的**hashmap**一样,用hash函数加上位运算或者取模,高级玩法还有一致性Hash等操作,找到对应的实例进行操作 +- 使用代理中间件 + 我们可以开发独立的代理中间件,屏蔽掉处理数据分片的逻辑,独立运行。当然Redis也有优秀的代理中间件,譬如Twemproxy,或者codis,可以结合场景选择是否使用 + + + +**缺点** + +- **无缘多key操作**,key都不一定在一个实例上,那么多key操作或者多key事务自然是不支持 +- **维护成本**,由于每个实例在物理和逻辑上,都属于单独的一个节点,缺乏统一管理 +- **灵活性有限**,范围分片还好,比如hash+MOD这种方式,如果想**动态**调整Redis实例的数量,就要考虑大量数据迁移 + + + +### 主从(Master-Slave) + +分区暂时能解决**单点**无法容纳的**数据量问题**,但是一个Key还是只在一个实例上。主从则将数据从**主节点**同步到**从节点**,然后可做**读写分离**,将读流量均摊在各个从节点,可靠性也能提高。**主从**(Master-Slave)也就是复制(Replication)方式。 + + + +**使用方式** + +- 作为主节点的Redis实例,并不要求配置任何参数,只需要正常启动 +- 作为从节点的实例,使用配置文件或命令方式`REPLICAOF 主节点Host 主节点port`即可完成主从配置 + + + +**缺点** + +- slave节点都是**只读**的,如果**写流量**大的场景,就有些力不从心 +- **故障转移**不友好,主节点挂掉后,写处理就无处安放,需要**手工**的设定新的主节点,如使用`REPLICAOF no one` 晋升为主节点,再梳理其他slave节点的新主配置,相对来说比较麻烦 + + + +### 哨兵(Sentinel) + +**主从**的手工故障转移,肯定让人很难接受,自然就出现了高可用方案-**哨兵**(Sentinel)。我们可以在主从架构不变的场景,直接加入**Redis Sentinel**,对节点进行**监控**,来完成自动的**故障发现**与**转移**。并且还能够充当**配置提供者**,提供主节点的信息,就算发生了故障转移,也能提供正确的地址。 + + + +**使用方式** + +**Sentinel**的最小配置,一行即可: + +```properties +sentinel monitor <主节点别名> <主节点host> <主节点端口> <票数> +``` + +只需要配置master即可,然后用``redis-sentinel <配置文件>`` 命令即可启用。哨兵数量建议在三个以上且为奇数。 + + + +**使用场景问题** + +- 故障转移期间短暂的不可用,但其实官网的例子也给出了`parallel-syncs`参数来指定并行的同步实例数量,以免全部实例都在同步出现整体不可用的情况,相对来说要比手工的故障转移更加方便 +- 分区逻辑需要自定义处理,虽然解决了主从下的高可用问题,但是Sentinel并没有提供分区解决方案,还需开发者考虑如何建设 +- 既然是还是主从,如果异常的写流量搞垮了主节点,那么自动的“故障转移”会不会变成自动“灾难传递”,即slave提升为Master之后挂掉,又进行提升又被挂掉 + + + +### 集群(Cluster) + +**Cluster**在分区管理上,使用了“**哈希槽**”(hash slot)这么一个概念,一共有**16384**个槽位,每个实例负责一部分**槽**,通过`CRC16(key)&16383`这样的公式,计算出来key所对应的槽位。 + + + +**使用方式** + +配置文件 + +```properties +cluster-enabled yes +cluster-config-file "redis-node.conf" +``` + +启动命令 + +```bash +redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 \ +127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \ +--cluster-replicas 1 +``` + + + +**存在问题** + +- 虽然是对分区良好支持,但也有一些分区的老问题。如如果不在同一个“槽”的数据,是没法使用类似mset的**多键操作** +- 在select命令页有提到, 集群模式下只能使用一个库,虽然平时一般也是这么用的,但是要了解一下 +- 运维上也要谨慎,俗话说得好,“**使用越简单底层越复杂**”,启动搭建是很方便,使用时面对带宽消耗,数据倾斜等等具体问题时,还需人工介入,或者研究合适的配置参数 + + + ## 常见问题 **题目**:保证Redis 中的 20w 数据都是热点数据 说明是 被频繁访问的数据,并且要保证Redis的内存能够存放20w数据,要计算出Redis内存的大小。 diff --git a/Solution.md b/Solution.md index a21f6de..22c141d 100644 --- a/Solution.md +++ b/Solution.md @@ -2045,6 +2045,83 @@ public void unlock() { ## Redis +https://mp.weixin.qq.com/s?__biz=MzAwMDg2OTAxNg==&mid=2652055114&idx=1&sn=f4d73fa2e294d633224f4d94a0667e70&chksm=8105d1bdb67258ab458389bd23d8e0211d34835f9da3745a0e6c244ec943911220db0c011665&mpshare=1&scene=23&srcid=1014HUmkIFVfi7rEQQ6bmuqH&sharer_sharetime=1634173594040&sharer_shareid=0f9991a2eb945ab493c13ed9bfb8bf4b%23rd + +### 分布式锁的问题 + +#### 非原子操作 + +`加锁操作`和后面的`设置超时时间`是分开的,并`非原子操作`。解决方案: + +- **set命令** +- **LUA脚本** + + + +#### 忘了释放锁 + +在redis中还有`set`命令是原子操作,加锁和设置超时时间,一个命令就能轻松搞定。 + +```java +String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime); +if ("OK".equals(result)) { + return true; +} +return false; +``` + +其中: + +- `lockKey`:锁的标识 +- `requestId`:请求id +- `NX`:只在键不存在时,才对键进行设置操作 +- `PX`:设置键的过期时间为 millisecond 毫秒 +- `expireTime`:过期时 + +使用`set`命令加锁,表面上看起来没有问题。但如果仔细想想,加锁之后,每次都要达到了超时时间才释放锁,会不会有点不合理?加锁后,如果不及时释放锁,会有很多问题。分布式锁更合理的流程如下: + +![Redis释放锁流程](images/Solution/Redis释放锁流程.jpg) + +释放锁的伪代码如下: + +```java +try{ + String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime); + if ("OK".equals(result)) { + return true; + } + return false; +} finally { + unlock(lockKey); +} +``` + + + +#### 释放了别人的锁 + + + +#### 大量失败请求 + + + +#### 锁重入问题 + + + +#### 锁竞争问题 + + + +#### 锁超时问题 + + + +#### 主从复制的问题 + + + ### LUA+SETNX+EXPIRE 先用`setnx`来抢锁,如果抢到之后,再用`expire`给锁设置一个过期时间,防止锁忘记了释放。 diff --git a/images/Solution/Redis释放锁流程.jpg b/images/Solution/Redis释放锁流程.jpg new file mode 100644 index 0000000..43e167f Binary files /dev/null and b/images/Solution/Redis释放锁流程.jpg differ