1.新增Redis扩展方案

2.新增Redis锁存在的8大问题
3.更新数据库事务隔离级别
pull/3/head
liruyu 3 years ago
parent e722fe6fcc
commit c0bd15de86

@ -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(可串行化)
**指一个事务在执行过程中完全看不到其他事务对数据库所做的更新**。当两个事务同时操作数据库中相同数据时,如果第一个事务已经在访问该数据,第二个事务只能停下来等待,必须等到第一个事务结束后才能恢复运行。因此这两个事务实际上是串行化方式运行
所有事务都是进行**串行化顺序**执行的。可以避免**脏读**、**不可重复读**与**幻读**所有并发问题。但该事务隔离级别下,事务执行很耗性能
**特点**:避免脏读、不可重复读、幻读
**可序列化的数据库锁情况**
**数据库锁情况**
- 读取数据:**先对其加表级共享锁 ,直到事务结束才释放**
- 写入数据:**先对其加表级排他锁 ,直到事务结束才释放**

@ -1260,6 +1260,112 @@ cluster_stats_messages_received:3021
## 拓展方案
### 分区(Partitioning)
指在面临**单机**的**存储空间**瓶颈时即将全部数据分散在多个Redis实例中每个实例不需要关联可以是完全独立的。
**使用方式**
- 客户端处理
和传统的数据库分库分表一样,可以从**key**入手,先进行计算,找到对应数据存储的实例在进行操作。
- **范围角度**比如orderId:1~orderId:1000放入实例1orderId: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**个槽位,每个实例负责一部分**槽**,通过`CRC16key&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内存的大小。

@ -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`给锁设置一个过期时间,防止锁忘记了释放。

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Loading…
Cancel
Save