一、数据库提升读写性能的方式
数据库有哪些提升读写性能的方式
1、连接池,阻塞io+线程池
2、异步连接 非阻塞io
3、sql执行出发:即时执行+预编译执行(跳过了词法句法分析,权限验证,优化器)prepare接口
我们来举个例子
mysql的连接过程
跟mysql连接之后,会进行一个验证,会主动发送一个连接给服务器,采用的密码以及连接方式,比如说安装mysql8.0会遇到一个问题,Navicat有些版本不支持mysql8.0默认连接方式,但是可以连接mysql5.7,因为5.7版本采用的是一个mysql_native_password,也就是直接把密码加密的方式
但是呢8.0采用的是一个caching_sha2_password的验证方式,这种方式比较增强,没法进行登录,所以必须改成mysql_native_password才可以
我们怎么去改呢,修改mysql表的结构
修改plugin为mysql_native_password,我们就可以用老版本的Navicat去连mysql8.0
4、读写分离
写操作写主数据库,读操作读从数据库,这样也能提升读写性能,这是一个一致性的问题也就是用异步的方式去获取同步,某一时刻主从数据库会不一致,主从复制的方式能解决单点故障的问题,单点故障问题就是主数据库突然宕机了,我们的数据会在从数据中进行备份。
对一致性要求高咋办
1、主从分离
2、去主数据读取
5、缓存方案
关系型数据库的好处在于,主要数据存在磁盘当中,数据安全比较高,方便统计分析,mysql作为主要的数据库,是数据的主要依据。
因为mysql主要是在磁盘当中,所以需要频繁访问磁盘,性能会比较低,我们就需要考虑给mysql增加缓存,我们引入一种缓存数据库叫cache,把一些热点的数据缓存在cache当中,cache也有很多解决方案。
以前没有redis的时候,我们用的是memcached,还有我们现在比较热门的就是redis,这就是我们现有的缓存方案,缓存用户的热点数据
mysql里边有一种缓冲池,缓存的是mysql内部的热点数据,那这个跟我们用户的热点数据的差异在哪呢?
比如说,12点有一个秒杀活动,需要提前预约,12点左右会涌入大量用户,那这个会缓存进那个缓冲池当中吗,我们只会缓存少部分,因为这些大量用户会在我们12点钟左右会密集登录,在12点钟之前很多用户都是不会登陆的,那么我们会很少去操作mysql,就不会成为mysql的热点数据,MySQL的缓冲池不会存大量的用户数据,那么我们怎么让这些12点钟登陆的用户正常登录进系统当中呢,我们就需要提前缓存用户觉得的热点数据,我们就可以把这些用户缓存进cache当中,我们用户大量涌入的时候,就可以从cache去查找数据,缓存呢是由我们的内存进行操作,内存是比较快的,就能快速处理用户数据,就能降低mysql的访问。
这就是mysql内部的热点数据和用户的热点数据的区别
读我们可以直接读mysql,写我们也可以直接写mysql,那么cache当中的热点数据呢,我们就需要针对热点数据进行讨论。
二、热点数据处理
我们的热点数据有哪些状态呢
1、mysql有,cache没有,这是一种比较正常的状态
我们要通过策略避免
2、mysql没有,cache有,可能是缓存出错了
这是一种不正常的状态,要去避免
3、mysql和cache都有,可能出现数据不一致,
4、mysql和cache都有,可能一致
这是我们的最终目标
5、mysql和cache都没有,这是一种正常状态
制定读写策略
读策略
我们的目标是MySQL和cache都有
1、先看cache是否有数据,如果有直接返回
2、如果cache没有,就去访问mysql,如果mysql有,缓存数据到cache
如果mysql都没有,就是没有
写策略
分情况讨论,到底是解决最终一致性的问题,还是强一致性的问题
最终一致性是指mysql和cache都有了,但是数据不一致,最终达到一致了,有时候是可以接受的,比如说写博客提交的事情,比如说有个粉丝刷到了你本没有提交的数据,有个粉丝刷到了提交后的数据,这不影响博客产生的功能,这就是最终一致性。
强一致性,举个例子,A给B100块钱,C呢他不管中间经历了什么,他必须看到A少了100块钱,B多了100块钱,这就是强一致性,不管咋样必须始终保持一致性,不能接受A少了100,但是B的账号一点没多。
在强一致性要求下
从删除出发,先去删除缓存,再去删除mysql,为啥先删除cache,因为我们先读取的缓存
我们修改前,先思考一下,我们先修改cache,然后又有一个人去修改cache,再去修改mysql,又有一个来修改cache再修改mysql,这时候有一个人来读cache的时候,这个数据应该以谁的为准呢,此时就应该告诉这个人,cache是不可用的,不确定是什么状态,应该去访问mysql,mysql是主要的数据库,这是数据的主要依据。所以在我们发觉这个数据不那么可靠的时候,我们应该以mysql作为主要依据。那么我们应该先删除缓存cache,再去修改mysql
我们插入数据前,我们思考一下,插入的数据是一种新数据,新数据呢就要避免操作失误,也就是缓存当中已经出现了这种同样的数据的时候,用户误认为这是插入的新数据,同样在插入前也要删除缓存,再去插入mysql
这些操作使得状态变成了,mysql有,cache没有的情况,我们的目标是mysql有,cache也要有,那么我们怎么把mysql的数据送入缓存中呢,我们就需要把mysql的数据同步到redis当中去
这里的数据不能频繁更改,效率会降低
关于强一致性,我们还有种方案
修改缓存,设置过期时间(200ms),这个过期时间是访问mysql,mysql同步到cache的一段时间,当我们的mysql回写cache的时候,set,setget命令会把这个过期时间去掉,插入和修改会采用这种方案
我们来思考一下,这个方案有什么问题,性能的话肯定比上边那个方案高,我们来看这种情况,我们修改缓存成功后,mysql宕机了,那么我们获取数据就直接访问cache了,不去访问mysql。mysql宕机导致cache里边的数据无法同步到mysql,客户端会认为cache里边是合理的数据,作为业务逻辑的依据,就会导致问题,这种问题呢,我们不需要去担心,因为mysql宕机,整个业务都会停摆,这种错误没有影响数据库,但是可能会影响客户端的显示,重要数据不会作为热点数据存储。
多个数据中心
出现多个数据中心的时候,将其转换成一个数据中心,用一个代理层,我们也会考虑使用kafka
同步阻塞与异步非阻塞方式
如果是同步的方式,发送一个请求后返回,才能发送下一个请求再返回,这是我们同步的方式,之前提到过连接池的方案,开启多个连接,开启多个线程就是这么做的
异步在于同时可以发送多个请求过去,然后同时多个返回,节约了网络传输的时间
我们来讨论一下异步的方式,我们用到了协程,在用户层依然是同步的使用方式,异步由转发层进行转发,返回之后把用户的协程唤醒,相当于一个异步连接
每一个连接都会有一个读缓冲区,多个请求都会堆积在写缓冲区,每一个连接相当于一个线程
三、实现原理
我们先思考mysql怎么同步到redis当中去的,我们思考一个原理叫做主从复制原理
触发器+UDF
对表中进行增删改操作,会触发触发器,触发器中有代码,代码当中有udf,就是用户定义的方法,udf当中有扩展,扩展去连接我们的redis,再把数据写到redis当中去
这种方法不建议,因为mysql常用InnoDB,需要一个事务支持,UDF不具备事务,会产生脏数据,不能回滚,效率会非常低
阿里给了一个方案,将mysql同步到一个canel的中间件当中,然后是canelclient,在同步到redis当中,做java的可以采用这种方案
还有一种就是go-mysql-transfer的方法
这两种方案原理主要用的就是主从复制,以上这两种中间件,都伪装成了mysql的从数据库,操作mysql的时候,我们的中间件伪装成从数据从而能够更新到redis当中去
我们来看看从数据库的原理,在数据库修改的时候,会发生对数据的同步
,增删改会修改两个文件,一个是binlog,还有就是Redo,redolog记录了B+对应磁盘的文件,我们以insert的例子来看,产生binlog之后呢,就会产生一个IO线程,会不断地去读取binlog,把数据读到从数据库里边,binlog读完以后,会去写到一个中心日志Relaylog,然后就会开启四个sql thread,读出内容进行回放,在回放的时候会重新insert
主从复制具体流程:
1.Slave上面的IO进程连接上Master,并请求从指定日志文件(bin文件)的指定位置(或者从最开始的日志)之后的日志内容。
2. Master接收到来自Slave的IO进程的请求后,负责复制的IO进程会根据请求信息读取日志指定位置之后的日志信息,返回给Slave的 IO进程。返回信息中除了日志所包含的信息之外,还包括本次返回的信息已经到Master端的bin-log文件的名称以及bin-log的位置。
3. Slave的IO进程接收到信息后,将接收到的日志内容依次添加到Slave端的relay-log文件的最末端,并将读取到的Master端的 bin-log的文件名和位置记录到master-info文件中,以便在下一次读取的时候能够清楚的告诉Master从何处开始读取日志。
4. Slave的Sql进程检测到relay-log中新增加了内容后,会马上解析relay-log的内容成为在Master端真实执行时候的那些可执行的内容,并在自身执行。
那我们怎么去伪装从数据库的呢
比如说我们常常看见的机器人,伪装成一个用户,实现通信协议发送相应指令
那么中间件该怎么使用呢
1、到主数据库的时候,show master status,获取同步的位置
# 查询 master 状态,获取 日志名和偏移量
mysql> show master status;
# 重置同步位置 (假设通过上面命令获取到日志名和偏移量为 mysql-bin.000025 993779648)
./go-mysql-transfer -config app.yml -position mysql-bin.000025 993779648 # 全量数据同步
./go-mysql-transfer -stock 123456
/* 构建主从复制
mysql 配置文件 my.cnf
log-bin=mysql-bin(记录二进制文件,产生binlog)
# 开启 binlog binlog-format=ROW (选择行模式)
# 选择 ROW 模式 server_id=1(表示这个会作为主数据库)
# 配置 MySQL replaction 需要定义,不要和 go-mysql-transfer 的 slave_id 重复 */
CREATE TABLE `user` (
`id` BIGINT,
`name` VARCHAR (100),
`height` INT8,
`sex` VARCHAR (1),
`age` INT8 PRIMARY KEY (`id`) );
insert into `user` values (10001, 'mark', 180, '1', 30);
update `t_user` set `age` = 31 where id = 10001;
delete from `t_user` where id = 10001;
-- go-mysql-transfer
--[[ 安装步骤: GO111MODULE=on
git clone https://gitee.com/0k/go-mysql-transfer.git
go env -w GOPROXY=https://goproxy.cn,direct
go build
修改 app.yml 执行
go-mysql-transfer ]]
local ops = require("redisOps")
--加载redis操作模块
local row = ops.rawRow()
--当前数据库的一行数据,table类型,key为列名称
local action = ops.rawAction()
--当前数据库事件,包括:
insert、updare、delete if action == "insert" then
-- 只监听insert事件
local id = row["id"]
--获取ID列的值
local name = row["name"]
--获取USER_NAME列的值
local key = name .. ":" .. id local sex = row["sex"] local height = row["height"]
--获取PASSWORD列的值
local age = row["age"] local createtime = row["createtime"]
--获取CREATE_TIME列的值
ops.HSET(key, "id", id)
-- 对应Redis的HSET命令
ops.HSET(key, "name", name)
-- 对应Redis的HSET命令
ops.HSET(key, "sex", sex)
-- 对应Redis的HSET命令
ops.HSET(key, "height", height)
-- 对应Redis的HSET命令
ops.HSET(key, "age", age) -- 对应Redis的HSET命令
end
我们需要修改哪些内容呢,我们创建从数据库,是不是需要去知道主数据库的地址以及redis的地址,热点数据怎么做,定义热点数据同步
# mysql配置
addr: 本地mysql
user:
pass:
charset : utf8
slave_id: 1001 #slave ID,作为从数据库
flavor: mysql #mysql or mariadb,默认mysql
#目标类型
target: redis #缓存数据库
#制定规则,就是制定什么是热点数据
rule:
schema: practice #数据库名称
table: user #表名称
order_by_column: id #排序字段,存量数据同步时不能为空
column_underscore_to_camel: true #列名称下划线转驼峰,默认为false
lua_file_path: lua/t_user.lua #定义热点数据同步,指定这个lua脚本文件
#lua_script: #lua 脚本
value_encoder: json #值编码,支持json、kv-commas、v-commas;默认为json
#value_formatter: '{{.ID}}|{{.USER_NAME}}' # 值格式化表达式,如:{{.ID}}|{{.USER_NAME}},{{.ID}}表示ID字段的值、{{.USER_NAME}}表示USER_NAME字段的值
#redis相关
redis_structure: hash # 数据类型。 支持string、hash、list、set、sortedset类型(与redis的数据类型一致)
我们来操作一下,我们查询后修改插入数据,都会同步到mysql和redis当中去
我们刚刚思考的都是正常的流程和方式,我们来看看异常情况
缓存穿透
假设某个数据redis不存在,mysql也不存在,而且一直尝试读怎么办?缓存穿透,数据最终压力依然堆积在mysql,可能造成mysql不堪重负而崩溃;
解决
- 发现mysql不存在,将redis设置为 <key, nil> 设置过期时间 下次访问key的时候 不再访问mysql 容易造成redis缓存很多无效数据;
- 布隆过滤器,将mysql当中已经存在的key,写入布隆过滤器,不存在的直接pass掉;
缓存击穿
缓存击穿 某些数据redis没有,但是mysql有;此时当大量这类数据的并发请求,同样造成mysql过大;
解决
- 加锁
请求数据的时候获取锁,如果获取成功,则操作,获取失败,则休眠一段时间(200ms)再去获取;获取成功,则释放锁首先读redis,不存在,读mysql,存在,写redis key的锁整个流程走完,才让后面的服务器访问 - 将很热的key,设置不过期,不设置过期时间
缓存雪崩
表示一段时间内,缓存集中失效(redis无 mysql 有),导致请求全部走mysql,有可能搞垮数据库,使整个服务失效;
解决
缓存数据库在整个系统不是必须的,也就是缓存宕机不会影响整个系统提供服务; - 如果因为缓存数据库宕机,造成所有数据涌向mysql; 采用高可用的集群方案,如哨兵模式、cluster模式;
- 如果因为设置了相同的过期时间,造成缓存集中失效;设置随机过期值或者其他机制错开失效时间;
- 如果因为系统重启的时候,造成缓存数据消失;重启时间短,redis开启持久化(过期信息也会持久化)就行了; 重启时间长提前将热数据导入redis当中;