mysql 有自己缓冲层,它的作用也是用来缓存热点数据,这些数据包括索引、记录等。mysql 缓冲层是从自身出发,跟具体的业务无关。这里的缓冲策略主要是 lru。
mysql 数据主要存储在磁盘当中,适合大量重要数据的存储;磁盘当中的数据一般是远大于内存当中的数据。
一般业务场景的关系型数据库(mysql)是作为主要数据库的。
MySQL缓存方案用来缓存用户定义的热点数据 ,用户直接从缓存获取热点数据,降低数据库的读写压力。
缓存数据库可以选用 redis,memcached;它们所有数据都存储在内存当中,当然也可以将内存当中的数据持久化到磁盘当中。
(1)内存访问速度是磁盘访问速度10W倍,访问磁盘的速度比较慢,尽量使获取数据是从内存中获取。
(2)读的需求远远大于写的需求。主要解决读的性能;因为写没必要优化,必须让数据正确的落盘。如果写性能出现问题,那么请使用横向扩展集群方式来解决。
(3)MySQL自身缓冲层跟业务无关。由于 mysql 的缓冲层不由用户来控制,也就是不能由用户来控制缓存具体数据。
(4)MySQL作为项目主要数据库,便于统计分析。项目中需要存储的数据应该远大于内存的容量,同时需要进行数据统计分析,所以数据存储获取的依据应该是关系型数据库。
(5)缓存数据库作为辅助数据库,存放热点数据。缓存数据库可以存储用户自定义的热点数据。
(1)读写分离。
(2)连接池。
(3)异步连接。
(4)预处理。
(5)更换存储引擎。
(6)分库分表。(淘汰的技术)
(7)mycat。(淘汰的技术)
(8)tidb。
复制流程:
由于MySQL的主从复制是异步的,所以同一时刻主数据库和从数据库的数据可能存在不一致的现象,这就造成可能从数据库中读取的数据不是最新的。
读写分离会设置多个从数据库,从数据库可能会在多个机器中。
写操作依然在主数据库中,主数据库提供数据的主要依据。
读写分离通过设置多个从数据库解决读压力。
读写分离主要依据MySQL的主从复制原理,因为MySQL的主从复制是异步复制的,所以读写分离只能保证数据的最终一致性,不能保证实时一致性。
如果读操作有强一致性要求,那么需要读操作去读主数据库。
连接池的定义:在服务端当中创建多个与数据库的连接线程。
解决的问题:并发提升数据库访问性能;同时复用连接,避免连接建立、断开依据安全验证的开销。
原理:利用MySQL的网络模型创建多个连接,每个连接复用去出路SQL语句。值得注意的是,如果发送一个事务(多条SQL语句),这个事务必须要在一个连接里面完成。
在服务端创建一个连接,针对这个连接采用非阻塞IO。这种方式可以节省网络传输时间。
随着技术提升,leveldb的方式被淘汰,使用了更完善的pika方式。pika内部使用的rocksdb,支持redis协议。
没有缓冲层之前,对数据的读写都是基于 mysql;所以不存在同步问题;这句话也不是必然,比如读写分离就存在同步问题(数据一致性问题)。
引入缓冲层后,对数据的获取需要分别操作缓存数据库和mysql,那么这个时候数据可能存在以下状态:
4 和 5显然是没问题的,现在需要考虑1、2以及3。
首先明确一点:获取数据的主要依据是 mysql,只需要将mysql 的数据正确同步到缓存数据库就可以了。
同理,缓存有,mysql 没有,这比较危险,此时可以认为该数据为脏数据;所以需要在同步策略中避免该情况发生;同时可能存在mysql 和缓存都有数据,但是数据不一致,这种也需要在同步策略中避免。
注意:以MySQL为主,保证缓存不可用,整个系统依然要保持正常工作;mysql 不可用的话,系统停摆,停止对外提供服务。
读策略:先读缓存,若缓存有,直接返回;若缓存没有,读mysql;若 mysql 有,同步到缓存,并返回;若 mysql 没有,则返回没有。
写策略:从安全优先方面考虑;先删除缓存,再写 mysql,后面数据同步交由 go-mysql-transfer 等中间件处理(将问题 3 转化成 1)。
先删除缓存,为了避免其他服务读取旧的数据;也是告知系统这个数据已经不是最新,建议从 mysql 获取数据。但是对于服务 A 而言,写入 mysql 后,接着读操作必须要能读到最新的数据。
写策略:从效率优先方面考虑;先写缓存,并设置过期时间(如 200ms),再写mysql,后面数据同步交由其他中间件处理。
这里设置的过期时间是预估时间,大致上是 mysql 到缓存同步的时间。在写的过程中如果 mysql 停止服务,或数据没写入 mysql,则200 ms 内提供了脏数据服务;但仅仅只有 200ms 的数据错乱,即效率优先的写策略也有安全性的问题,但只会影响200ms。
同步方案可以有:
(1)伪装从数据库。比如阿里开源的canal方案、kafka、go-mysql-transfer等。
(2)MySQL的触发器+udf。udf全称User-defined function,是MySQL提供的一种可扩展代码。UDF不具备事务,不能回滚;而且效率较低。
canal会考虑分布式问题,如果一个canal宕机了,会有从canal顶替上来,保证服务正常提供。
go-mysql-transfer只有一个节点,相对canal简单些,没有解决分布式问题。要增强go-mysql-transfer的高可用,可以引入etcd、zk等。
操作步骤:
(1)找到MySQL的my.ini配置文件,修改为主从模式。设置binlog-format=ROW和server-id=1。
(2)修改go-mysql-transfer中的app.yml文件的mysql配置和redis连接配置。
# mysql配置
addr: 127.0.0.1:3306
user: root
pass: root
charset : utf8
slave_id: 1001 #slave ID
flavor: mysql #mysql or mariadb,默认mysql
#redis连接配置
redis_addrs: 127.0.0.1:6379 #redis地址,多个用逗号分隔
#redis_group_type: cluster # 集群类型 sentinel或者cluster
#redis_master_name: mymaster # Master节点名称,如果group_type为sentinel则此项不能为空,为cluster此项无效
#redis_pass: 123456 #redis密码
#redis_database: 0 #redis数据库 0-16,默认0。如果group_type为cluster此项无效
(3)修改go-mysql-transfer中的app.yml文件的规则配置,设置热点数据。
#规则配置
rule:
-
schema: eseap #数据库名称
table: t_user #表名称
#order_by_column: id #排序字段,存量数据同步时不能为空
#column_lower_case:false #列名称转为小写,默认为false
#column_upper_case:false#列名称转为大写,默认为false
column_underscore_to_camel: true #列名称下划线转驼峰,默认为false
# 包含的列,多值逗号分隔,如:id,name,age,area_id 为空时表示包含全部列
#include_columns: ID,USER_NAME,PASSWORD
#exclude_columns: BIRTHDAY,MOBIE # 排除掉的列,多值逗号分隔,如:id,name,age,area_id 默认为空
#column_mappings: USER_NAME=account #列名称映射,多个映射关系用逗号分隔,如:USER_NAME=account 表示将字段名USER_NAME映射为account
#default_column_values: area_name=合肥 #默认的列-值,多个用逗号分隔,如:source=binlog,area_name=合肥
#date_formatter: yyyy-MM-dd #date类型格式化, 不填写默认yyyy-MM-dd
#datetime_formatter: yyyy-MM-dd HH:mm:ss #datetime、timestamp类型格式化,不填写默认yyyy-MM-dd HH:mm:ss
#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: string # 数据类型。 支持string、hash、list、set、sortedset类型(与redis的数据类型一致)
#redis_key_prefix: USER_ #key的前缀
#redis_key_column: USER_NAME #使用哪个列的值作为key,不填写默认使用主键
#redis_key_formatter: '{{.ID}}|{{.USER_NAME}}'
#redis_key_value: user #KEY的值(固定值);当redis_structure为hash、list、set、sortedset此值不能为空
#redis_hash_field_prefix: _CARD_ #hash的field前缀,仅redis_structure为hash时起作用
#redis_hash_field_column: Cert_No #使用哪个列的值作为hash的field,仅redis_structure为hash时起作用,不填写默认使用主键
#redis_sorted_set_score_column: id #sortedset的score,当数据类型为sortedset时,此项不能为空,此项的值应为数字类型
#mongodb相关
#mongodb_database: transfer #mongodb database不能为空
#mongodb_collection: transfer_test_topic #mongodb collection,可以为空,默认使用表名称
#elasticsearch相关
#es_index: user_index #Index名称,可以为空,默认使用表(Table)名称
#es_mappings: #索引映射,可以为空,为空时根据数据类型自行推导ES推导
# -
# column: REMARK #数据库列名称
# field: remark #映射后的ES字段名称
# type: text #ES字段类型
# analyzer: ik_smart #ES分词器,type为text此项有意义
# #format: #日期格式,type为date此项有意义
# -
# column: USER_NAME #数据库列名称
# field: account #映射后的ES字段名称
# type: keyword #ES字段类型
#rocketmq相关
#rocketmq_topic: transfer_test_topic #rocketmq topic,可以为空,默认使用表名称
#kafka相关
#kafka_topic: user_topic #rocketmq topic,可以为空,默认使用表名称
#rabbitmq相关
#rabbitmq_queue: user_topic #queue名称,可以为空,默认使用表(Table)名称
#reserve_raw_data: true #保留update之前的数据,针对rocketmq、kafka、rabbitmq有用;默认为false
(4)写lua同步逻辑。
(5)启动mysql个redis。
假设某个数据 redis 不存在,mysql 也不存在,而且一直尝试读怎么办?缓存穿透,数据最终压力依然堆积在 mysql,可能造成mysql 不堪重负而崩溃。
解决方案:
(1) 发现 mysql 不存在,将 redis 设置为
2. 布隆过滤器,将 mysql 当中已经存在的 key,写入布隆过滤
器,不存在的直接 pass 掉;
(1)缓存设置
(2)部署布隆过滤器。
缓存击穿是某些数据 redis 没有,但是 mysql 有;此时当大量这类数据的并发请求,同样造成 mysql 过大。
解决方案:
(1) 分布式锁。请求数据的时候获取锁,若获取成功,则操作后释放锁;若获
取失败,则休眠一段时间(200ms)再去获取,当获取成功,操作后释放锁。
(2) 将很热的 key,设置不过期。
表示一段时间内,缓存集中失效(redis 无, mysql 有),导致请求全部走 mysql,有可能搞垮数据库,使整个服务失效。
缓存数据库在整个系统不是必须的,也就是缓存宕机不会影响整个系统提供服务。
解决方案:
(1)如果因为缓存数据库宕机,造成所有数据涌向 mysql。采用高可用的集群方案,如哨兵模式、cluster模式。
(2)如果因为设置了相同的过期时间,造成缓存集中失效。设置随机过期值或者其他机制错开失效时间。
(3) 如果因为系统重启的时候,造成缓存数据消失。重启时间短,redis 开启持久化(过期信息也会持久化)就行了; 重启时间长提前将热数据导入 redis 当中。
不能处理多语句的事务。redis不支持回滚,造成redis个MySQL的不一致。