首页> 搜索结果页
"查看postgresql内存" 检索
共 953 条结果
PolarDB-PG | PostgreSQL + 阿里云OSS 实现高效低价的海量数据冷热存储分离
背景数据库里的历史数据越来越多, 占用空间大, 备份慢, 恢复慢, 查询少但是很费钱, 迁移慢. 怎么办?冷热分离方案:使用PostgreSQL 或者 PolarDB-PG, 将历史数据存成parquet文件格式, 放到aliyun OSS存储里面. 使用duckdb_fdw对OSS内的parquet文件进行查询.《DuckDB DataLake 场景使用举例 - aliyun OSS对象存储parquet》《PolarDB 开源版通过 duckdb_fdw 支持 parquet 列存数据文件以及高效OLAP》duckdb 存储元数据(parquet 映射)《DuckDB parquet 分区表 / Delta Lake(数据湖) 应用》方案特点:内网oss不收取网络费用, 只收取存储费用, 非常便宜.oss分几个档, 可以根据性能需求选择.parquet为列存储, 一般历史数据的分析需求多, 性能不错.duckdb 支持 parquet下推过滤, 数据过滤性能也不错.存储在oss内, 可以使用oss的函数计算功能, 仅计算时收费. 而且使用OSS存储打破数据孤岛, OSS与PG和PolarDB以及其他数据源打通, 形成数据联邦, 更容易发挥更大层面的数据价值.架构如下: PolarDB-PG 或 PostgreSQL ↑↓ ↑↓ 热数据: 高速本地存储 ↑↓ ↓↓ ↑↓ ↓↓ LibDuckDB ForeignServer 层: ↓↓ 1、(通过 duckdb_fdw 读写OSS) 2、(通过 postgres_scanner 读高速本地存储) ↑↓ ↑↓ 归档数据: OSS 冷暖存储 (Parquet格式) demo在以下debian 容器中部署1、部署duckdb和依赖的parquet、httpfs插件《Debian学习入门 - (作为服务器使用, Debian 操作系统可能是长期更好的选择?)》确认编译了httpfs 和 parquet 插件root@9b780f5ea2e8:~/duckdb/build/release/extension# pwd /root/duckdb/build/release/extension root@9b780f5ea2e8:~/duckdb/build/release/extension# ll total 72K -rw-r--r-- 1 root root 2.3K Mar 3 06:16 cmake_install.cmake -rw-r--r-- 1 root root 6.2K Mar 3 06:16 Makefile drwxr-xr-x 15 root root 4.0K Mar 3 06:16 . drwxr-xr-x 2 root root 4.0K Mar 3 06:16 CMakeFiles drwxr-xr-x 4 root root 4.0K Mar 3 06:40 jemalloc drwxr-xr-x 10 root root 4.0K Mar 3 06:43 .. drwxr-xr-x 4 root root 4.0K Mar 3 06:45 icu drwxr-xr-x 3 root root 4.0K Mar 3 06:47 parquet drwxr-xr-x 4 root root 4.0K Mar 3 06:47 tpch drwxr-xr-x 4 root root 4.0K Mar 3 06:47 tpcds drwxr-xr-x 3 root root 4.0K Mar 3 06:47 fts drwxr-xr-x 3 root root 4.0K Mar 3 06:48 httpfs drwxr-xr-x 3 root root 4.0K Mar 3 06:48 visualizer drwxr-xr-x 5 root root 4.0K Mar 3 06:49 json drwxr-xr-x 4 root root 4.0K Mar 3 06:49 excel drwxr-xr-x 4 root root 4.0K Mar 3 06:50 sqlsmith drwxr-xr-x 3 root root 4.0K Mar 3 06:50 inet 2、安装postgresql 或 PolarDB开源版本.PolarDB开源版本部署请参考: 《如何用 PolarDB 证明巴菲特的投资理念 - 包括PolarDB on Docker简单部署》以下是使用postgresql的例子:apt install -y curl fastjar mkdir /home/postgres useradd postgres chown postgres:postgres /home/postgres su - postgres curl https://ftp.postgresql.org/pub/source/v15.2/postgresql-15.2.tar.bz2 -o ./postgresql-15.2.tar.bz2 tar -jxvf postgresql-15.2.tar.bz2 cd postgresql-15.2 ./configure --prefix=/home/postgres/pg15.2 make world -j 4 make install-world 3、部署duckdb_fdwsu - postgres git clone --depth 1 https://github.com/alitrack/duckdb_fdw 将duckdb的lib包拷贝到postgresql的lib目录root@9b780f5ea2e8:~/duckdb/build/release/src# pwd /root/duckdb/build/release/src root@9b780f5ea2e8:~/duckdb/build/release/src# ll libduckdb.so -rwxr-xr-x 1 root root 58M Mar 3 06:42 libduckdb.so cp libduckdb.so /home/postgres/pg15.2/lib/ 安装duckdb_fdw插件su - postgres export PATH=/home/postgres/pg15.2/bin:$PATH cd duckdb_fdw USE_PGXS=1 make USE_PGXS=1 make install 4、初始化postgresql数据库集群initdb -D /home/postgres/pgdata -E UTF8 --lc-collate=C -U postgres 5、简单配置一下pg配置文件vi /home/postgres/pgdata/postgresql.conf listen_addresses = '0.0.0.0' port = 1921 max_connections = 100 unix_socket_directories = '/tmp,.' shared_buffers = 128MB dynamic_shared_memory_type = posix max_wal_size = 1GB min_wal_size = 80MB log_destination = 'csvlog' logging_collector = on log_directory = 'log' log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' log_file_mode = 0600 log_rotation_age = 1d log_rotation_size = 10MB log_truncate_on_rotation = on log_timezone = 'Etc/UTC' datestyle = 'iso, mdy' timezone = 'Etc/UTC' lc_messages = 'C' lc_monetary = 'C' lc_numeric = 'C' lc_time = 'C' default_text_search_config = 'pg_catalog.english' 6、启动数据库, 加载duckdb_fdw插件pg_ctl start -D /home/postgres/pgdata $ psql -h 127.0.0.1 -p 1921 psql (15.2) Type "help" for help. postgres=# create extension duckdb_fdw ; CREATE EXTENSION 创建oss实验环境可以使用阿里云云起实验免费创建oss实验环境, 参考如下:《DuckDB DataLake 场景使用举例 - aliyun OSS对象存储parquet》1、初始化实验环境后, 得到一些需要的内容如下, 将被duckdb用于连接oss.AK ID: LTAI5t6eUHtPZiFKLCNQro8n AK Secret: 5wHLZXCbTpNbwUUeqRBqr7vGyirFL5 Endpoint外网域名: oss-cn-shanghai.aliyuncs.com Bucket名称: adc-oss-labs01969 Object路径: ECSOSS/u-bimcc3ei/ duckdb读写OSS的方法COPY <table_name> TO 's3://<Bucket名称>/<Object路径>/filename'; SELECT * FROM read_parquet('s3://<Bucket名称>/<Object路径>/filename'); 在debian中, 测试duckdb是否能正常使用OSS, 并生成100万测试数据, 写入oss.root@9b780f5ea2e8:~/duckdb/build/release# pwd /root/duckdb/build/release root@9b780f5ea2e8:~/duckdb/build/release# ./duckdb v0.7.1 b00b93f Enter ".help" for usage hints. Connected to a transient in-memory database. Use ".open FILENAME" to reopen on a persistent database. D load 'httpfs'; D set s3_access_key_id='LTAI5t6eUHtPZiFKLCNQro8n'; // AK ID D set s3_secret_access_key='5wHLZXCbTpNbwUUeqRBqr7vGyirFL5'; // AK Secret D set s3_endpoint='s3.oss-cn-shanghai.aliyuncs.com'; // Endpoint外网域名|内网域名 D COPY (select id, md5(random()::text) as info, now() as ts from range(0,1000000) as t(id)) TO 's3://adc-oss-labs01969/ECSOSS/u-bimcc3ei/test_duckdb1.parquet'; 测试创建视图是否正常使用IT-C02YW2EFLVDL:release digoal$ ./duckdb v0.7.1 b00b93f Enter ".help" for usage hints. Connected to a transient in-memory database. Use ".open FILENAME" to reopen on a persistent database. D set s3_access_key_id='LTAI5t6eUHtPZiFKLCNQro8n'; D set s3_secret_access_key='5wHLZXCbTpNbwUUeqRBqr7vGyirFL5'; D set s3_endpoint='s3.oss-cn-shanghai.aliyuncs.com'; 将parquet文件映射为view D create or replace view test_duckdb1 as SELECT * FROM read_parquet('s3://adc-oss-labs01969/ECSOSS/u-bimcc3ei/test_duckdb1.parquet'); D select count(*) from test_duckdb1; ┌──────────────┐ │ count_star() │ │ int64 │ ├──────────────┤ │ 1000000 │ └──────────────┘ D select * from main."test_duckdb1" limit 10; 100% ▕████████████████████████████████████████████████████████████▏ ┌───────┬──────────────────────────────────┬────────────────────────────┐ │ id │ info │ ts │ │ int64 │ varchar │ timestamp with time zone │ ├───────┼──────────────────────────────────┼────────────────────────────┤ │ 0 │ 87a144c45874838dbcd3255c215ababc │ 2023-03-08 17:28:12.902+08 │ │ 1 │ cce8d1f5d58e72e9f34a36ccd87188ed │ 2023-03-08 17:28:12.902+08 │ │ 2 │ 0ea50d2769b01c26537e09902dc5f732 │ 2023-03-08 17:28:12.902+08 │ │ 3 │ 70a6c5f594def5d1d1bbb993260a2fd7 │ 2023-03-08 17:28:12.902+08 │ │ 4 │ 5a7924f417b480210601508e2c144a2f │ 2023-03-08 17:28:12.902+08 │ │ 5 │ d1fde1c1dc8f268d9eb9fce477653bb0 │ 2023-03-08 17:28:12.902+08 │ │ 6 │ 1aac9556fd1b259c56ecef3ef4636a66 │ 2023-03-08 17:28:12.902+08 │ │ 7 │ 04181693f9b6c8576bb251612ffbe318 │ 2023-03-08 17:28:12.902+08 │ │ 8 │ 332b9bb9d00e8fa53a5661804bd1b41a │ 2023-03-08 17:28:12.902+08 │ │ 9 │ f0189d662187cc436662a458577a7ed2 │ 2023-03-08 17:28:12.902+08 │ ├───────┴──────────────────────────────────┴────────────────────────────┤ │ 10 rows 3 columns │ └───────────────────────────────────────────────────────────────────────┘ Run Time (s): real 9.773 user 1.121633 sys 0.928902 D .timer on D select max(id) from test_duckdb1; ┌─────────┐ │ max(id) │ │ int64 │ ├─────────┤ │ 999999 │ └─────────┘ Run Time (s): real 0.482 user 0.087439 sys 0.065868 在postgresql中使用duckdb_fdw访问oss内的parquet文件你可以创建duckdb内存数据库, 也可以指定为一个持久化文件, 使用持久化文件的话可以拥有一些元数据存储的能力, 不用每次都创建映射和配置.下面用的是内存存储(非持久化)例子:在psql内执行postgres=# CREATE SERVER DuckDB_server FOREIGN DATA WRAPPER duckdb_fdw OPTIONS (database ':memory:'); CREATE SERVER -- 设置为保持连接(会话内保持) postgres=# alter server duckdb_server options ( keep_connections 'true'); ALTER SERVER 接下来创建一个duckdb视图, 用以查询parquet.一定要分开执行:SELECT duckdb_execute('duckdb_server', $$ set s3_access_key_id='LTAI5t6eUHtPZiFKLCNQro8n'; $$); SELECT duckdb_execute('duckdb_server', $$ set s3_secret_access_key='5wHLZXCbTpNbwUUeqRBqr7vGyirFL5'; $$); SELECT duckdb_execute('duckdb_server', $$ set s3_endpoint='s3.oss-cn-shanghai.aliyuncs.com'; $$); SELECT duckdb_execute('duckdb_server', $$ create or replace view test_duckdb1 as SELECT * FROM read_parquet('s3://adc-oss-labs01969/ECSOSS/u-bimcc3ei/test_duckdb1.parquet'); $$); 检查是否保持连接postgres=# select * from duckdb_fdw_get_connections(); server_name | valid ---------------+------- duckdb_server | t (1 row) 创建duckdb_fdw外部表, 指向刚才创建的duckdb视图:create foreign TABLE ft_test_duckdb1( id int, info text, ts timestamp) SERVER duckdb_server OPTIONS (table 'test_duckdb1'); 我们查看一下duckdb_fdw的下推能力, 非常帮, 过滤、limit、排序、distinct等都进行了下推, 详细参考duckdb_fdw开源项目:postgres=# explain verbose select id from ft_test_duckdb1 limit 1; QUERY PLAN -------------------------------------------------------------------------- Foreign Scan on public.ft_test_duckdb1 (cost=1.00..1.00 rows=1 width=4) Output: id SQLite query: SELECT "id" FROM main."test_duckdb1" LIMIT 1 (3 rows) postgres=# explain verbose select * from ft_test_duckdb1 where id<100; QUERY PLAN ----------------------------------------------------------------------------------------- Foreign Scan on public.ft_test_duckdb1 (cost=10.00..401.00 rows=401 width=44) Output: id, info, ts SQLite query: SELECT "id", "info", "ts" FROM main."test_duckdb1" WHERE (("id" < 100)) (3 rows) postgres=# explain verbose select * from ft_test_duckdb1 where id<100 order by ts limit 100; QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------- Foreign Scan on public.ft_test_duckdb1 (cost=1.00..1.00 rows=1 width=44) Output: id, info, ts SQLite query: SELECT "id", "info", "ts" FROM main."test_duckdb1" WHERE (("id" < 100)) ORDER BY "ts" ASC NULLS LAST LIMIT 100 (3 rows) postgres=# explain verbose select count(distinct id) from ft_test_duckdb1; QUERY PLAN ---------------------------------------------------------------------- Foreign Scan (cost=1.00..1.00 rows=1 width=8) Output: (count(DISTINCT id)) SQLite query: SELECT count(DISTINCT "id") FROM main."test_duckdb1" (3 rows) postgres=# select * from ft_test_duckdb1 limit 1; id | info | ts ----+----------------------------------+------------------------- 0 | 87a144c45874838dbcd3255c215ababc | 2023-03-08 09:28:12.902 (1 row) 执行查询, 看看性能如何. 以下对比pg本地表、parquet(实验环境在公网, 如果是内网还不好说谁快谁慢.)postgres=# create table t as select * from ft_test_duckdb1 ; SELECT 1000000 Time: 21196.441 ms (00:21.196) postgres=# \timing Timing is on. postgres=# select count(distinct id) from ft_test_duckdb1; count --------- 1000000 (1 row) Time: 1281.537 ms (00:01.282) postgres=# select count(distinct id) from t; count --------- 1000000 (1 row) Time: 260.007 ms postgres=# select count(*) from ft_test_duckdb1 where id<100; count ------- 100 (1 row) Time: 806.976 ms postgres=# select count(*) from t where id<100; count ------- 100 (1 row) Time: 60.254 ms 多个会话同时访问相同server, 相同foreign table使用同一个server, 每次建立连接都会新建一个duckdb inmemory进程. 每次都需要设置oss配置, 创建duckdb view. 然后就能通过ft读取数据. session a:访问正常postgres=# select * from ft_test_duckdb1 limit 1; id | info | ts ----+----------------------------------+------------------------- 0 | 77e736e2033e489f3134607dcfd63d05 | 2023-03-09 05:45:25.506 (1 row) session b:访问正常postgres=# select * from ft_test_duckdb1 limit 1; id | info | ts ----+----------------------------------+------------------------- 0 | 77e736e2033e489f3134607dcfd63d05 | 2023-03-09 05:45:25.506 (1 row) 通过duckdb_fdw将历史数据写入oss, 实现历史数据归档操作准备工作, 配置需要密码连接postgresql.vi pg_hba.conf # IPv4 local connections: host all all 127.0.0.1/32 md5 pg_ctl reload -D $PGDATA psql alter role postgres encrypted password '123456'; 1、建立pg本地表postgres=# create table t1 (id int, info text, ts timestamp); CREATE TABLE postgres=# insert into t1 select generate_series(1,1000000), md5(random()::text), clock_timestamp(); INSERT 0 1000000 2、在duckdb中使用postgres插件可以读取pg本地表的数据root@9b780f5ea2e8:~# cd duckdb/build/release/ root@9b780f5ea2e8:~/duckdb/build/release# ./duckdb v0.7.1 b00b93f Enter ".help" for usage hints. Connected to a transient in-memory database. Use ".open FILENAME" to reopen on a persistent database. D load 'postgres'; D select * from POSTGRES_SCAN_PUSHDOWN('dbname=postgres user=postgres hostaddr=127.0.0.1 port=1921 password=123456', 'public', 't1') limit 1; ┌───────┬──────────────────────────────────┬────────────────────────────┐ │ id │ info │ ts │ │ int32 │ varchar │ timestamp │ ├───────┼──────────────────────────────────┼────────────────────────────┤ │ 1 │ c8ecbcc36395bfa4d39b414e306c1b81 │ 2023-03-09 05:49:30.184854 │ └───────┴──────────────────────────────────┴────────────────────────────┘ D 3、在duckdb中可以打通pg和oss, 也就是将pg的数据写入ossset s3_access_key_id='LTAI5tJiSWjkwPHRNrJYvLFM'; set s3_secret_access_key='6WUWvNCv1xOdf2eC6894L9sOVdG0a0'; set s3_endpoint='s3.oss-cn-shanghai.aliyuncs.com'; COPY ( select * from POSTGRES_SCAN_PUSHDOWN('dbname=postgres user=postgres hostaddr=127.0.0.1 port=1921 password=123456', 'public', 't1') ) TO 's3://adc-oss-1872hd2/ECSOSS/u-ijr7vhba/abc.parquet'; 100% ▕████████████████████████████████████████████████████████████▏ 4、紧接着, 直接在pg里面使用duckdb_fdw插件, 让duckdb来读取pg的数据写入oss.SELECT duckdb_execute('duckdb_server', $$ install 'postgres'; $$); SELECT duckdb_execute('duckdb_server', $$ load 'postgres'; $$); SELECT duckdb_execute('duckdb_server', $$ COPY ( select * from POSTGRES_SCAN_PUSHDOWN('dbname=postgres user=postgres hostaddr=127.0.0.1 port=1921 password=123456', 'public', 't1') ) TO 's3://adc-oss-1872hd2/ECSOSS/u-ijr7vhba/test_import_from_pg1.parquet'; $$); 使用如上方法install postgres时, 会自动从duckdb官方下载对应版本编译好的插件, 例如:https://extensions.duckdb.org/v0.7.1/linux_amd64/postgres_scanner.duckdb_extension.gz详见:https://duckdb.org/docs/extensions/overview.html方法没问题, 目前bug可能和gpdb postgres_fdw遇到的问题一样, 感兴趣的朋友可以参与一起解决: https://github.com/alitrack/duckdb_fdw/issues/15postgres=# SELECT duckdb_execute('duckdb_server', $$ COPY ( select * from POSTGRES_SCAN('dbname=postgres user=postgres hostaddr=127.0.0.1 port=1921 password=123456', 'public', 't1') ) TO 's3://adc-oss-1872hd2/ECSOSS/u-ijr7vhba/test_import_from_pg1.parquet'; $$); ERROR: HV00L: SQL error during prepare: IO Error: Unable to connect to Postgres at dbname=postgres user=postgres hostaddr=127.0.0.1 port=1921 password=123456: libpq is incorrectly linked to backend functions COPY ( select * from POSTGRES_SCAN('dbname=postgres user=postgres hostaddr=127.0.0.1 port=1921 password=123456', 'public', 't1') ) TO 's3://adc-oss-1872hd2/ECSOSS/u-ijr7vhba/test_import_from_pg1.parquet'; LOCATION: sqlite_prepare_wrapper, duckdb_fdw.c:504 相关代码sqlite3_prepare_v2:/* Wrapper for sqlite3_prepare */ static void sqlite_prepare_wrapper(ForeignServer *server, sqlite3 * db, char *query, sqlite3_stmt * *stmt, const char **pzTail, bool is_cache) { int rc; // db = sqlite_get_connection(server, false); // elog(DEBUG1, "duckdb_fdw : %s %s %p %p %p %p\n", __func__, query, server,db,&stmt,stmt); rc = sqlite3_prepare_v2(db, query, -1, stmt, pzTail); // elog(DEBUG1, "duckdb_fdw : %s %s %d \n", __func__, query, rc); if (rc != SQLITE_OK) { ereport(ERROR, (errcode(ERRCODE_FDW_UNABLE_TO_CREATE_EXECUTION), errmsg("SQL error during prepare: %s %s", sqlite3_errmsg(db), query) )); } /* cache stmt to finalize at last */ if (is_cache) sqlite_cache_stmt(server, stmt); } gpdb类似的一个issue. https://github.com/greenplum-db/gpdb/issues/11400 https://github.com/greenplum-db/gpdb/commit/667f0c37bc6d7bce7be8b758652ef95ddb823e19Fix postgres_fdw's libpq issue (#10617) * Fix postgres_fdw's libpq issue When using posgres_fdw, it reports the following error: unsupported frontend protocol 28675.0: server supports 2.0 to 3.0 root cause: Even if postgres_fdw.so is dynamic linked to libpq.so which is compiled with the option -DFRONTEND, but when it's loaded in gpdb and run, it will use the backend libpq which is compiled together with postgres program and reports the error. We statically link libpq into postgres_fdw and hide all the symbols of libpq.a with --exclude-libs=libpq.a to make it uses the frontend libpq. As postgres_fdw is compiled as a backend without -DFRONTEND, and linked to libpq which is a frontend, but _PQconninfoOption's length is different between backend and frontend as there is a macro in it. The backend's _PQconninfoOption has field connofs, but the frontend doesn't. This leads to the crash of postgres_fdw. So we delete the frontend macro in _PQconninfoOption. * Add FRONTEND macro on including libpq header files postgres_fdw is compiled as a backend, it needs the server's header files such as executor/tuptable.h. It also needs libpq to connect to a remote postgres database, so it's staticly linked to libpq.a which is compiled as a frontend using -DFRONTEND. But the struct PQconninfoOption's length is different between backend and frontend, there is no "connofs" field in frontend. When postgres_fdw calls the function "PQconndefaults" implemented in libpq.a and uses the returned PQconninfoOption variable, it crashes, because the PQconninfoOption variable returned by libpq.a doesn't contain the "connofs" value, but the postgres_fdw thinks it has, so it crashes. In last commit, we remove the FRONTEND macro in struct PQconninfoOption to make PQconninfoOption is same in backend and frontend, but that brings an ABI change. To avoid that, we revert that, and instead, we add the FRONTEND macro on including libpq header files, so that postgres_fdw can process the libpq's variables returned by libpq.a's functions as frontend. * Report error if the libpq-fe.h is included before postgres_fdw.h postgres_fdw needs to include frontend mode libpq-fe.h, so if the libpq-fe.h is included before the postgres_fdw.h, and we don't know if it is frontend mode, so we just report the error here. 感谢steven贡献duckdb_fdw未来duckdb_fdw的优化期待: 1、在server中加入更多的option, 例如设置s3的参数, 连接时就默认配置好, 这样的话就可以直接查询foreign table, 不需要每次都需要通过execute接口来配置.启动时设置allow_unsigned_extensions, 允许使用未签名的外部extension.https://duckdb.org/docs/extensions/overview.html参考https://github.com/alitrack/duckdb_fdw《Debian学习入门 - (作为服务器使用, Debian 操作系统可能是长期更好的选择?)》《DuckDB DataLake 场景使用举例 - aliyun OSS对象存储parquet》《用duckdb_fdw加速PostgreSQL分析计算, 提速40倍, 真香.》《PolarDB 开源版通过 duckdb_fdw 支持 parquet 列存数据文件以及高效OLAP》《如何用 PolarDB 证明巴菲特的投资理念 - 包括PolarDB on Docker简单部署》
文章
存储  ·  NoSQL  ·  关系型数据库  ·  数据库  ·  对象存储  ·  PostgreSQL  ·  容器  ·  PolarDB  ·  分布式数据库  ·  Docker
2023-03-08
百问求答(3)Flink专场!回答问题赢行李箱等好礼
阿里云开发者社区作为一个充满活力的技术社区,有许多技术同道在这里勤学好问。为了让这些用户的疑惑得到解答,我们举办了“百问求答”活动,本期为Flink专场,期待用你的技术知识储备帮助同行解决难题,赢取行李箱等好礼,还有机会成为阿里云开发者社区“乘风问答官”,享受专属权益!赶快参与起来! 奖项设置: 注:问题如有回答,若你有其他解决方案也可作答,将记录在内;若回答雷同,将不计数。  乘风问答官权益可点击https://developer.aliyun.com/ask/469378进行查看。 活动流程 1、点击https://yida.alibaba-inc.com/o/wdg进行报名; 2、回答文末Flink问题 活动时间 2023年1月16日至1月31日14:00 获奖名单及奖品邮寄时间 获奖名单将于活动结束后7个工作日公布,奖品将于10个工作日内进行邮寄,节假日顺延。 活动规则及注意事项 1、本活动回答数据不记录问答官排位赛活动中; 2、 回答仅限文章链接中的问题,其他回答不计数; 3、 回答请解答能力范围之内的问题进行,充数回答将不计算在内,如111、等与问题无关的回答; 4、 问题及回答需为中文,英文不记录数据; 5、 回答发布后将进入审核状态,审核完成即可查看; 6、 标题党、黑稿、通稿、包含违法违规、未被许可的商业推广、外站链接、非原创内容、营销软文、抄袭嫌疑的文章审核将不予通过,同时取消参赛资格。 Flink问题链接: 1、flink有api可以像 spark那样批出 kafka数据吗? 2、flinksql可以用状态吗? 把一些数据存在状态里,然后根据后面数据来更新状态里的数据或者取出来输出sink 3、flink将checkpoint写入到hdfs中七天之后token过期,有大佬解决过这个问题吗? 4、大佬们,flink的日志你们怎么处理的,进一步写到了es里了吗?还是就在log文件里 5、什么job都没运行, flink堆内存一直慢慢增长(会到2G以上), 是flink本身问题还是? 6、flink1.13要使用2.3的话是要把guava30改成18再重新打包吗? 7、flink刚启动时通过sql创建任务时数据能复制到es,然后mongo里的数据就没了,再添加es也不同步cdc模拟一个mongo节点,是不是应该通过rs.status能查呢?我查着还是一个主节点,没有子节点,是不是监听有问题啊 8、请问flink有没有日志呢?按照文档搭建的测试,同步不成功,mongodb添加了数据es里没有同步 9、flink ml支持自己线下训练好的模型部署到流里面进行预测推理吗 10、大佬们,flink sql 连接阿里云数据库报禁止,什么情况 11、我想问一下flink sql 如何读取一个小时之前的一条数据 12、flink_on_yarn ,Idea 能跑,咋提交到服务器就跑不起来 13、问下 flink-sql平台 写holo 怎么 做到宽表merge? 原理明白, 但是sql 写不通 14、flink里面的容器哪里可以调优 15、我的flinksql可以从doris中select到数据,但是还是无法insert,请问有遇到过类似事情的没flink版本1.14.4 16、Flink session 集群中我们怎么添加 lib下面的connector呢? 17、flink1.15的cdc需要自己编译吗 18、有大佬做native flink on k8s的部署方案吗,按照官网的配置,报这种异常,大佬帮忙瞅瞅 19、flink 托管内存 默认情况下是这样显示的 按一定比例分配大小,是不是代表这块内存都被rocksdb使用了,也不参与垃圾回收? 20、Flink 窗口计算流读iceberg(iceberg已经修改过支持水位线)这个错误如何解决呢? 21、有大佬搞过flink on k8s的案例吗? 22、我现在flink启动的实话,会提示这个语句失败,怎么回事呀 23、flinkcdc 采集的数据,如果是更新的话,op=d、c,如果新增的话 op=c,怎么判断是真正的新增呢? 24、flink checkpoint 保留机制问题 集群模式是 standlane模式 我设置参数state.checkpoints.num-retained 参数为2 清除策略设置 RETAIN_ON_CANCELLATION 25、大佬们 为啥有时候提交flink任务会这样 26、咨询一下,flink读取mysql的数据完成了,是否会自动停止服务? 27、这是flink内置的kafka包的错吧 28、flinksql能指定uid么 29、flink作业不是每个任务里面点进去都有各自的日志吗? 30、flink sql,1.14以后怎么流join mysql呢 官网文档显示不支持join了? 31、flink版本升级到1.15.2,能从link 1.13.2的保存点恢复吗,保存点是否兼容 32、flink lookup join 的proc_time可以出现在 sum这样的聚合函数里面吗? 33、flink运行参数有一个想动态变化,你们有方案吗 34、谁给说Flink链接doris 这个应该怎么操作 35、大佬们,现在FLink写入MySQL和oracle,是不是只有jdbc这种方式啊? 36、用的flink2.3.0得版本 报错这个 哪位同仁遇到过吗? 37、请教一下,flink1.14版本连接mysql cdc流时,debezium的String流转Row流时候,字段field都在f0里分不出来 38、flink cdc的sql实现方式能单独设定算子的并行么?现在flink sql 有几个算子背压很高,没办法解决 39、flink 支持计算列吗?建表的时候 40、flink跑流任务,pg作为维表每隔一段时间会出现ck超时失败的情况有谁碰到过这个问题吗 41、flinkSQL对于两条具有水位线join的流可以使用触发器吗 42、flink_streaming读hudi读不到数据这个是什么情况呢 43、有个问题 想咨询一下 filnk的自定义source 里不能用spring容器里的东西吗?因为好多类都没有实现序列化 ,所以flink和spring到底能一起用吗? 44、请各位大神指点一下,使用flink将数据写入到HIVE中,过几天之后token自动过期 导致执行不成功,有解决办法吗 45、大佬们 flinksql转datastream混用如何启动一个job 46、请教下,flink-sql client 如何获取到插入之后的主键值? 47、flink-cdc 跟踪postgresql库 但是表都是建的timescaledb 超表 跟踪不到新增数据 有人遇到过这种情况么? 48、flink 启动时报错怎么解决? 49、flink cdc(mysql) -> elasticsearch7, 任务每次持续跑了一段时间之后(elasticsearch connection reset by peer),就自动结束了,请问一下是啥原因呀? 50、请教大佬flink如何在代码逻辑更新升级时如何保留confimap,以便自动从检查点恢复吗? 51、flink-cdc 在抽取mysql binlog,运行一段时间 source 就报错 52、大佬们flink-format-changelog-json这个包在github上找不到源码了是怎么回事呢? 53、flink sql 窗口排序怎么做 按事件时间排序flink sql 窗口排序怎么做 按事件时间排序 54、有没有对flink 离线任务压缩 hudi表 增大一次压缩的commit个数,感觉默认的太慢了 55、大佬们,我想用flinksql 开10s的窗,根据这个dim 字段来分组 处理数据然后写入kafka里,现在有个问题 , 在第一个 10s窗扣造了 dim01字段值的数据,必须在第二个 10s窗里再造个 dim01字段值的数据 这样第一个10s窗口的dim01的数据才会输出, 有知道是什么原因的吗? 56、我今天准备把flink1.13.6升级到1.14.6,但是有几个jar在阿里云仓库里没找到,哪位朋友方便提供一下么 57、flink 2.2.0 升级到2.2.1 switched from INITIALIZING to FAILED with failure cause: org.apache.flink.util.FlinkRuntimeException: Failed to deserialize value 这个是什么问题? 58、大佬们,请问个问题,flink 多个并行消费rabbitmq消息,怎么保证顺序消费 59、有大佬知道 flink1.11版本 提交参数 -Dyarn.provided.lib.dirs="hdfs://test-bigdata/flinkTestJar/dependencies/lib" 无法生效的原因吗,使用./bin/flink run -m yarn-cluster 提交的,是不是yarn-cluster 不支持呀 60、有大佬知道 flink1.11版本 提交参数 -Dyarn.provided.lib.dirs="hdfs://test-bigdata/flinkTestJar/dependencies/lib" 无法生效的原因吗,使用./bin/flink run -m yarn-cluster 提交的,是不是yarn-cluster 不支持呀 61、各位有遇到过这个错误的吗?依赖版本:flink 1.14.5flink-connector-mysql-cdc 2.2.0flink-connector-debezium 2.2.0flink-sql-connector-mysql-cdc 2.2.0,怎么办? 62、请问下Flink 用Map类型接收Kafka中的嵌套json,MySQL 用json类型接收,使用sql方式怎么写? 63、有没有大佬 flink写hudi 采用flink离线压缩出现过这个问题 64、大家有测试过 一个脚本采用flink cdc 同步mysql 能同时同步多少表吗 ? 65、请问下当flink集群重启之后 kafka消息还是重复 有什么办法解决吗 kafka sink 已设置 'sink.semantic' = 'exactly-once', 66、请问,flink sql 方式可以同步修改DDL语句吗? 67、想问下大佬们,如果flink设置了整体的并行度为2,cdc的source是不是会读取两次重复的binlog数据 68、这条flinksql,拉了全库的数据,where条件没有下推 怎么回事? 69、flink 有没有遇到这种情况? 70、flink1.15.1配置全局保存点(S3),启动时报错如图,到minio那里看目录是已经创建了的,哪位大佬知道怎么回事吗? 71、flink1.4 版本 使用的EmbeddedRocksDBStateBackend , Managed Memory 一直100% 这种情况正常吗 72、大佬们flink都是用的hivecatelog吗,hadoopcatelog有用过吗,这俩有撒区别吗 73、flink大作业启动频繁报akka.pattern.AskTimeoutException 大家有遇到过这个问题么?除了加timeout还有啥其他可以尝试的办法么? 74、有人知道flinksql 客户端运行在flink on yarn 上配置jobmanager内存不生效,怎么办?每次都取最小的 该配的都配置了 75、flink cdc 同步mysql数据库时 全量阶段出现 读取快照失败 连接重置的异常 76、flink 有人遇到这个报错吗? 77、报ORA-04030 ,flink 任务能一直跑吗? 78、flink同步oracle报这个错,请问这是因为啥? 79、flink1.16不支持hive2.1了吗? 80、执行flinksql 能转化成datastream吗 81、flinkcdc同步对数据库 的性能有影响吗? 会影响业务的写入吗? 82、有碰到在编译完flink-doris-connector ,通过stream load 写入doris 时连接不上doris fe的问题吗? 83、这种错误大家遇到过嘛 ,过一段时间 flink taskmanager就异常推出 84、请问flink cdc支持mysql主从切换吗? 85、flink-connector-redis使用的是jedis,性能可以吗?有人用lettuce重写吗 86、请问一下flinkcdc 怎么配置能同步到mysql的truncate操作呢?现在源表truncate,目的表不会有反应。 87、flink对于自增ID为主键的MySQL表怎么处理一致性的 88、flinksql中没有 COLLECT_SET 函数吗? 89、请问,FlinkCDC的postgre connector,支持startup.positiion吗 90、flinksql 的source表是加了SASL_SSL的Kafka,作业跑在yarn上,这个jks这些文件怎么能放在hdfs上并能够使用啊,现在用这个配置 写hdfs路径 说不存在 91、flink消费rabbitmq,设置prefetch count不生效有人碰到过吗 92、请问下flink cdc有提供什么手段对比源端和目标端同步数据一致性的方案没? 93、flink怎么判断全量已经跑完了,开始走增量的数据处理逻辑呢?有没有什么api可以知道的或者怎么打个标记? 94、安装flink12版本 ,可以用 flink cdc 吗? 95、flink kafkasink sink一致初始化起不来 有什么好的解决或者检查的办法么? 96、在用阿里云flink嘛 能知道上面的flink版本是15.几吗? 97、我通过Flink SQL 去读取kudu的数据报的这个错误可以给些建议嘛? 98、flink cdc 2.2.1 能同步mysql数据到clickhouse 一直报这些 是什么原因呢 99、Flink-MySQL-CDC多表关联怎么写入clickhouse? 100、flink版本1.14.0在k8s 部署出现上面的问题,有遇到过的吗
问答
流计算
2023-01-16
【数据库设计与实现】第6章:并发控制
并发控制设计原则事务的并发控制首先要保证并发执行的正确性,满足可序列化要求,即并发执行的结果和某种串行执行的结果是一致的,然后在满足正确性的前提下尽可能地获得最高的并发度。当然在某些业务场景下,可以适当牺牲部分正确性(即接受某些异常),从而获得更高的并发性能。并发控制大体分为悲观算法和乐观算法,为了尽可能深入了解各种算法的优缺点,本章在Oracle、MySQL的基础上增加了PostgreSQL、CockroachDB和VoltDB。Oracle、MySQL、PostgreSQL采用了悲观控制策略,同时通过MVCC进一步提高并发性,而PostgreSQL在此基础上实现了Serializable Snapshot Isolation。CockroachDB完全采用了乐观控制,是乐观控制的开源和商业化实现。VoltDB在并发控制策略上做了突破新创新,舍弃了悲观控制和乐观控制,采用了全串行化的执行策略。在设计和实现并发控制时,有如下几点需要考虑:并发控制算法的用户友好度和正确性,与ANSI SQL隔离级别的匹配度;并发控制算法的效率;并发控制算法的死锁策略;Oracle设计原理事务Oracle数据库支持ANSI SQL定义的Read Committed、Serializable隔离级别以及一个自定义的Read Only隔离级别,且默认Read Committed隔离级别。不支持ANSI SQL定义的Read Uncommitted和Repeatable Read隔离级别,主要基于如下考虑:Read Uncommitted:脏读主要有两个作用,其一是读不加锁,降低读操作的成本,以提高并发度。其二是可以读到最新的未提交数据。Oracle采用了多版本设计,读语句天然不会对记录加锁,同时读取最新脏数据的应用场景也比较少,基于上述考虑,Oracle不支持Read Uncommitted隔离级别;Repeatable Read:Repeatable Read和Serializable的主要区别是读断言锁是长期的还是短期的(详细情况请回顾“事务”章节的“Locking”小节),而Oracle在记录上没有设计读锁,所以两者没有区别,因此Oracle只提供了Serializable隔离级别,不支持Repeatable Read隔离级别;Oracle在事务的并发控制上综合运用了锁机制和多版本机制(MVCC),在锁机制中仅在记录上设计了行级写锁,没有设计行级读锁,读导致的相关异常通过多版本机制和用户加锁来解决(select ... for update)。在Read Committed隔离级别下,记录被修改时会在该记录上加上写锁且一直持续到事务提交或回滚,而读取记录则通过多版本机制读取已经提交的最新记录(多版本一致性读的原理,请回顾“数据前像与回滚”章节的“一致性读”小节)。为了能够读到最新的已提交数据,在每次查询语句开始前会获取当前的最新scn,该scn之前的最新已提交记录都可以被读取。这样既可以保证读到最新的已提交事务的数据,又保证了语句执行过程中的一致性。在Serializable隔离级别下,记录被修改时会在该记录上加上写锁且一直持续到事务提交或回滚。多版本机制和Read Committed下有所不同,Read Committed在每条查询语句开始之前都会获取当前最新的scn,而Serializable在事务开始前获取当前最新的scn。整个事务运行期间保持该scn不变,从而解决了不可重复读和幻读异常。然而只有写加锁,读不加锁,从而存在如下异常:Lost Update:r1[x=100]r2[x=100]w2[x=120]c2w1[x=130]c1是“事务”章节中的示例,事务T2对x增加的20将丢失。Oracle是通过报错来解决Lost Update异常的。当事务修改某条记录时,发现该记录的当前提交scn大于本事务的开始scn,说明该记录在本事务运行期间被其它事务修改并提交过,此时已经无法达成可序列化,报“ORA-08177: can’t serialize access for this transaction”错误,本次事务执行失败;Write Skew:r1[x=50]r1[y=50]r2[x=50]r2[y=50]w1[y=-40]w2[x=-40]c1c2是“事务”章节中的示例,需满足x+y为正数的约束,从单个事务T1和T2来看都是满足该约束的,但执行成功后不再满足该约束;解决上述异常的方式是使用select ... for update,即应用通过语句要求数据库在读操作时在记录上加锁(原则上此处加读锁即可,但Oracle没有记录级读锁,所以此处加的仍然是记录级写锁,一定程度上影响并发性),从而解决上述两种异常。实际上,在Read Committed隔离级别下,应该也可以通过select ... for update强制读语句加上写锁以达成可序列化的效果,缺点是降低了并发性。在Read Only隔离级别下,多版本机制和在Serializable隔离级别下是一样的,不同的是Read Only隔离级别下不允许执行DML写语句。Read Only对于分析型等只读场景是非常有意义的,既可以读到一致性的数据,同时又不阻塞正常的写事务。记录锁图6.2-1 记录级锁示意图 在上节我们知道Oracle只有记录级写锁,没有记录级读锁,即完全是通过记录级写锁达成事务的并发控制。图6.2-1给出了Oracle记录级写锁的示意图,在每行记录的头部都有一个字节的lb字段,记录本条记录被ITL中的哪个事务给锁定了。如果某条记录的lb指向ITL中的事务A,且该事务处于活跃态,那么该记录就被事务A锁住了,即事务A在该记录上加了记录级写锁。此处引出了Oracle的一个重要的设计理念,锁就是数据的一部分(占用1个字节),存在于block(data block、index block)中。这样的设计有如下优势:锁资源轻量且无限大:不需要在独立的内存区域中设计锁结构,锁就在数据中,随着block在内存和持久设备中换入换出,锁资源无限大,所以Oracle不需要设计多层次的锁粒度,并根据锁记录的数目在不同锁粒度间升级;易于传输:锁是记录的一部分,可以随着block进行传输,这一点在Oracle RAC中体现得非常明显,当block在数据库实例间传输时锁信息自然也就传输过去了;表锁当我们在做DDL语句时需要对操作的表加表锁,从而防止其他用户同时对该表做DDL操作。在更改表结构时还需要防止此时有其它事务正在更改本表中的记录,为此需要逐行检查本表的记录上是否有锁。如果表中的记录非常多,逐行检查表上记录是否有锁非常消耗资源,可能还涉及block的读入与写出,导致性能进一步恶化。为了解决此问题,可以在表上引入新的锁类型,以表明其所属的行上有锁,这就是意向锁。意向锁指如果对某个节点加意向锁,则说明该节点的下层节点正在被加锁。对任一节点加锁,必须先对上层节点加意向锁。对应到表和记录,对表中的任何记录加记录锁前,必须先对该表加意向锁,然后再对该记录加记录锁。这样DDL对表加锁时,不需要再逐行检查表中每条记录上的锁标志了,直接判断表上是否有意向锁即可,系统效率得以提升。意向锁有如下锁类型:意向共享锁(Intent Share Lock,IS锁):如果对记录加S锁,需要先对表加IS锁,以表示该表的记录准备(意向)加S锁;意向排它锁(Intent Exclusive Lock,IX锁):如果对记录加X锁,需要先对表加IX锁,以表示该表的记录准备(意向)加X锁;表上有基本的S锁和X锁,意向锁又引入了IS锁和IX锁,这样可以组合出新的S+IS、S+IX、X+IS、X+IX四种锁。但实际上只有S+IX有意义,其它三种组合都没有使锁的强度得以增强(即:S+IS=S,X+IS=X,X+IX=X,等于指强度相等)。这样我们引入了一种新的锁类型:共享意向排它锁(Shared Intent Exclusive Lock,SIX锁)。事务对某表加SIX锁,表示该事务要读取整个表(所以要对该表加S锁),同时会更新表中的部分记录(所以要对该表加IX锁)。意向锁封锁的策略:加锁:申请封锁时,应按照自上而下的次序进行;解锁:释放锁时,应按照自下而上的次序进行;可见,数据库表上的锁类型有S、X、IS、IX、SIX五种。Oracle的表锁分别有S、X、RS、RX、SRX,与S、X、IS、IX、SIX一一对应。需要注意的是Oracle在记录上只提供X锁,所以与RS(通过select ... for update语句获得)对应的记录锁也是X锁(该行实际上海没有被修改),这与理论上的IS锁有所区别的。表6.2-1 表锁相容矩阵 SXRSRXSRXSYNYNNXNNNNNRSYNYYYRXNNYYNSRXNNYNN表6.2-2 语句与表锁的对应关系锁锁语句场景NULL1select ... from table_nameRS2select ... from table_name for update(9.2.0.5之前版本)lock table table_name in row share modeRX3insert into table_nameupdate table_namedelete from table_nameselect ... from table_name for update(9.2.0.5及后继版本)lock table table_name in row exclusive modeS4create index ...lock table table_name in share mode外键上没有索引SRX5lock table table_name in share row exclusive mode外键约束定义成on   delete cascadeX6alter table ...drop table ...drop index ...truncate table ...lock table table_name in exclusive mode表6.2-1为Oracle表锁的相容矩阵,Y表示相容,N表示不相容,需要阻塞等待。表6.2-2给出了语句与表锁之间的对应关系示例,锁给出了字符和数值两种表达方式。当Oracle执行select ... for update、insert、update、delete等DML语句时,会在操作的表上自动加上表级RX锁。当执行alter table、drop table等DDL语句时,会在操作的表上自动加上表级X锁。另一方面,应用程序或者操作人员也可以通过lock table语句指定需要获得某种类型的表锁。最后再介绍一下Oracle的breakable parse locks(分析锁)。Oracle会在share pool中缓存分析和优化过的SQL语句和PL/SQL程序,这样再次执行这些相同的SQL或PL/SQL程序时,不必再进行解析、编译、生成执行计划,直接使用缓存的执行计划。缓存的执行计划对所涉数据库表是有依赖的,即当表结构发生变更时,缓存的所涉的执行计划需要及时失效。分析锁就是为了解决及时通知问题的,当缓存执行计划时,会在所涉数据库对象上加上分析锁。该分析锁会一直持有,直到对应的执行计划失效。分析锁不会产生任何阻塞,当表结构发生变更时,会及时通知对缓存的相关执行计划失效。Enqueue在上面章节我们知道Oracle有记录级X锁,有多种模式的表锁。通过这些锁在保证正确性的前提下,提供了最大的事务并发度。但从实现层面来看,我们还有两个关键问题尚未解决:问题1:如何高效地知道某个数据库对象上已经加了锁,加了什么模式的锁;问题2:当发生冲突时如何对事务排队,持有者释放锁时如何及时唤醒阻塞事务并保证公平性;表6.2-3 部分常见enqueue type大类类型场景User enqueuesTXAllocating an ITL entry in order to begin a transaction;Lock held by a transaction to allow other transactions to wait for it;Lock held on a particular row by a transaction to prevent other transactions from modifying it;Lock held on an index during a split to prevent other operations on it;TMSynchronizes accesses to an object;ULLock used by user applications(通过DBMS_LOCK.REQUEST加锁);System enqueuesSTSynchronizes space management activities in dictionary-managed tablespace;CICoordinates cross-instance function invocations;TTSerializes DDL operations on tablespace;USLock held to perform DDL on the undo segment;CFSynchronizes accesses to the controlfile;TCLock held to guarantee uniqueness of a tablespace checkpoint;Lock of setup of a unqiue tablespace checkpoint in null mode;ROCoordinates fast object reuse;Coordinates flushing of multiple object;PSParallel execution server process reservation and synchronization;首先来看问题1,因为锁是加在数据库对象上的,这些对象可以是表、文件、表空间、并行执行的从属进程、重做线程等等,我们将这些对象统一称为资源。为此,Oracle在SGA中设计了enqueue resource数组,数组中的每个元素代表一个资源,数组的总大小可通过参数_enqueue_resources设置(可通过x$ksqrs和v$resources查看enqueue resources)。Enqueue resource中的每个元素就是一个ksqrs结构,ksqrs结构中的关键成员有:enqueue type:标识锁类型(或称为资源类型),Oracle内部的锁类型非常丰富,表6.2-3给出了部分常见的锁类型。各种internal locks都会在system enqueue中对应一种类型,记录锁和表锁属于user enqueue,分别对应于TX和TM类型;enqueue identification(ID1、ID2):用于标识具体的资源,例如当enqueue type等于TM时,identification存放具体哪个表(ID1等于表的object id),当enqueue type等于TX时,identification存放具体哪个事务(ID1高位的2个字节存放undo segment id,ID1低位的2个字节存放transaction table id,ID2存放wrap);link:双向指针,用于将相同状态的ksqrs结构链接在一起,例如处于空闲状态或者在同一个hash桶中;owners:指向双向链表的头部和尾部,该双向链表存放所有已经持有本资源的锁信息;converters:指向双向链表的头部和尾部,该双向链表存放所有已经持有本资源并等待升级到锁强度更高的锁信息;waiters:指向双向链表的头部和尾部,该双向链表存放所有已经等待本资源的锁信息;图6.2-2 Enqueue Free List与Hash Table 如图6.2-2所示,正常情况下单个ksqrs结构未被使用前通过link双向指针串在一起,组成ksqrs的free list。当需要申请一个资源时,从free list上摘一个ksqrs结构下来,填写enqueue type和enqueue identification,并根据enqueue type和enqueue identification计算hash值,从而算出本krqrs结构归属的hash bucket,并将该ksqrs结构加入到算出的hash bucket中的hash chains中(hash chain中的ksqrs也是通过link双向指针链接在一起的)。hash算法越优秀,hash冲突越小,hash chain的长度越短。可见,处于使用状态的ksqrs是通过hash进行管理的,这样可以快速定位某个资源是否已经加锁(enqueue type和enqueue identification可以唯一标识某个特定的资源)。Hash table的长度(即bucket的数量)可以通过参数_enqueue_hash设置。由于多个用户会并发访问enqueue hash table,所以需要对其进行并发访问保护。系统会申请若干个enqueue hash chains latch(parent latch与child latch,详细情况请回顾“同步与互斥”章节),每个enqueue hash chains latch保护一段bucket(实际上是round-roubin方式)以及这些bucket后面的hash chain。Enqueue hash chains latch的数量由参数_enqueue_hash_chain_latches设置,默认值为cpu_count。假设表t1的object id为1234,现在需要对t1加表锁,那么首先需要申请TM资源。申请资源的大致过程如下:step1:查找enqueue hash table中是否已经有表t1的资源(表资源的类型为TM),对TM、1234(id1=object id)、0(id2=0)计算hash值,从而得到对应的bucket(此处假设为bucket12);step2:申请获得bucket12对应的enqueue hash chains latch;step3:成功获得latch后,查找bucket12的hash chain,看是否已经有表t1的TM资源,如果有则表示不需要创建新的t1资源,释放latch直接退出,否则进入下一步;step4:从ksqrs free list上摘下一个ksqrs结构,将enqueue type设置为TM,将enqueue identification设置为id1=1234,id2=0,然后将该ksqrs结构添加到bucket12的hash chain中;step5:至此完成表t1资源的创建,释放latch,并退出;在上述步骤4中,需要从ksqrs free list上摘下一个空闲的ksqrs结构。Ksqrs free list本身也需要同步与互斥保护,在高并发场景下会有大量频繁的申请与释放,此处就会成为瓶颈。为此,Oracle采用了Lazy策略,即释放资源后对应的ksqrs结构并不立刻归还到ksqrs free list中,而是保留一部分空闲ksqrs结构在chain chain上,这样后继可以直接复用,从而提升性能。至此,我们已经完成enqueue resource的介绍。但enqueue resources只是一个容器,只能给出问题1的部分答案,即解决了如何快速找到某个数据对象(资源)的问题,还需要回答问题1提出的锁模式和问题2。为此,我们需要引入另外一个结构“锁”,“锁”是加在资源上的,即附着在某个ksqrs结构上的。图6.2-3 KSQRS结构及锁对应关系 如图6.2-3所示,每个资源都对应一个ksqrs结构,加在该资源上的所有锁都通过ksqrs结构进行排队:Owners:持有者,即该资源的拥有者,每个锁对应一个拥有者,拥有者不会被阻塞,当有多个拥有者时这些拥有者的锁一定是相容的;Converters:转换者,由拥有者转换而来,表示已经拥有低强度的锁,但在申请变更为更高强度锁时和其它拥有者的锁不相容;Waiters:等待者,和拥有者的锁不相容;当拥有者释放锁时,首先唤醒转换者,即将转换者变更为新的拥有者。当拥有者和转换者都为空时,依次唤醒等待者。如果等待者中有多个相邻的锁是相容的,可以同时唤醒成为拥有者,即如果锁4和锁5是相容的,可以同时成为拥有者。有了上述概念之后,我们首先来看表锁的互斥排队过程。表锁对表对象加锁,所以容纳表锁的ksqrs类型为TM。每个表锁是一个ktqdm结构,申请表锁时首先从ktqdm free list中申请一个ktqdm结构(ktqdm free list由dml allocation latch保护),然后将ktqdm结构附着到对应表的ksqrs结构上。ktqdm结构中关键成员有:sid:锁对应的会话(session);lmode:当前已经持有的锁模式;request:当前正在请求的锁模式;ctime:锁已经持有的时长或者等待的时长;表6.2-4 表锁阻塞时序示例TimeSession1(S1)Session2(S2)Session3(S3)Session4(S4)T1lock table t1 in row exclusive mode;Lock table t1 in row exclusive mode;  T2 Lock table t1 in share row exclusive mode;  T3  Lock table t1 in exclusive mode; T4   Lock table t1 in row exclusive mode;T5Commit;   T6 Commit;  T7  Commit; T8   Commit;图6.2-4 表锁阻塞队列示例 如表6.2-4所示,该表展示了一个针对表t1的时序示例,4个会话(s1、s2、s3、s4)同时对表t1加表锁。图6.2-4给出了T4时刻,表t1上的各表锁之间的阻塞情况。详细过程如下:因为都是对表t1加锁,所以相关的ktqdm结构都附着在同一个ksqrs结构上,ksqrs的类型为TM,id1=t1(实际上是表t1的object_id),表示资源为表t1;T1时刻:s1和s2两个会话同时对表t1加row exclusive锁,这两个锁是相容的,所以都在持有者队列中,通过ktqdm结构中的link链成双向链表,lmode=3表示持有的锁模式为row exclusive;T2时刻:会话s2尝试对表t1加SRX(share row exclusive),即将锁的强度从RX升级为SRX。由于s2的SRX与s1的RX是不相容的,所以s2的ktqdm结构从持有者链表中迁移到转换者链表中,lmode=3表示s2已经持有RX锁,request=5表示s2正在申请SRX锁,此时会话s2阻塞;T3、T4时刻:会话s3和s4分别对表t1加X和RX锁,这两个锁要么和持有者的锁不相容,要么和转换者的锁不相容,所以按照申请的顺序加入到等待者链表中,lmode=0表示尚未持有任何锁,request=6/3表示正在申请的锁模式,此时会话s3和s4阻塞;T5时刻:会话s1提交并释放锁,此时s2从转换者链表迁移到持有者链表中,更新(sid=s2, lmode=5, request=0)表示锁升级为SRX,此时会话s2开始运行;T6时刻:会话s2提交并释放锁,此时持有者和转换者链表都为空,从等待者链表中将s3迁移到持有者链表中,并更新(sid=s3, lmode=6, request=0),由于会话s4的RX和会话s3的X不相容,所以会话s4仍然留在等待者链表中,此时会话s3运行,会话s4继续阻塞;T7时刻:会话s3提交并释放锁,会话s4从等待者链表迁移到持有者链表中,开始运行;至此,我们介绍了表锁的整个运行过程,回答了表锁相关的问题1和问题2,即通过ksqrs结构定位到并发阻塞的资源,通过ksqrs的持有者、转换者、等待者三个链表结合ktqdm结构完成排队、阻塞和唤醒。从中我们可以发现如下关键点:一个会话对同一个表不管加多少次表锁,只会占用一个ktqdm结构;转换者链表中的元素优先级高于等待者链表中的元素,因为转换者中的元素已经持有锁,需要让它们尽快运行以尽快释放锁;从等待者链表向持有者链表迁移时,是按照入链的顺序迁移的,即按照申请的顺序迁移的,体现了FIFO的公平性;下面我们开始介绍记录锁。对于问题1,记录锁是很容易解决的,每条记录的头部有lb标志,且记录锁只有X模式,所以记录锁的重点是解决问题2,即对有记录锁冲突的事务如何进行排队。记录锁的排队机制和表锁的排队机制是类似的,主要区别如下:仍然通过ksqrs enqueue排队,但ksqrs的type为TX,id1和id2等于事务id,即每个事务开启第一次写时会申请一个标识本事的TX类型的ksqrs结构,后继因为和本事务记录锁发生冲突的会话全部附着在该ksqrs结构上;不需要像表锁为每个锁申请一个kstqdm,只需要为每个冲突的事务申请一个ktcxb结构;表6.2-5 记录锁阻塞时序示例TimeSession1(S1)Session2(S2)T1--transaction id=10.1.145Update t1 set c1=1;100 rows updated. T2 --transaction id=10.2.23Update t2 set c1=2;100 rows updated.T3 Update t1 set c1=2 where c2=3;T4Commit; T5 1 rows updated.T6 Commit;图6.2-5 记录锁阻塞队列示例 如表6.2-5所示,该表展示了两个事务(10.1.145和10.2.23)同时修改表t1和表t2中记录的情况,因为同时修改t1中的记录而发生记录锁冲突。图6.2-5展示了在T3时刻TX排队情况。详细过程如下:T1时刻:会话s1开启一个写事务(第一条语句就是更新表t1的全表记录),申请一个ksqrs结构,类型为TX,id1=655361(10*65536+1),id2=145,并加入到ksqrs enqueue hash表中。同时申请一个ktcxb结构,设置sid=s1,lmode=6(记录锁只能是X模式),request=0,并将标识本事务的ktxcb附着在该ksqrs的持有者链表上;T2时刻:会话s2开启一个写事务(第一条语句就是更新表t2的全表记录),申请一个ksqrs结构,类型为TX,id1=655362(10*65536+2),id2=23,并加入到ksqrs enqueue hash表中。同时申请一个ktcxb结构,设置sid=s2,lmode=6,request=0,并将标识本事务的ktxcb附着在该ksqrs的持有者链表上;T3时刻:会话s2更新t1表的单条记录,通过检查该记录的记录头,发现该记录已经被事务10.1.145锁住,申请一个ktcxb结构,设置sid=s2,lmode=0,request=6,并将标识本会话的ktcxb附着在事务10.1.145的ksqrs的等待者链表上,以等待事务10.1.145释放该记录锁,此时会话s2阻塞;T4时刻:事务10.1.145提交,唤醒ksqrs(TX,id1=655361,id2=145)中等待者链表中的所有会话,然后释放ksqrs结构(TX,id1=655361,id2=145)和附着在该ksqrs上的ktcxb结构,此时s2会话激活,继续执行对表t1的记录更新;至此,我们介绍了记录锁的整个运作过程,回答了记录锁相关的问题1和问题2,即在记录头部发现记录冲突,在通过ksqrs的持有者、等待者链表结合ktcxb完成排队、阻塞和激活。从中我们还可以发现如下关键点:每个写事务都会申请一个ksqrs(类型为TX)结构,并持有到事务结束,可见事务本身一种资源;每个写事务都会申请一个或两个ktxcb结构,可见ktcxb结构的数量和修改的记录数无关,只可冲突的事务数相关;所有和事务A有记录冲突的事务都会申请一个ktxcb结构,并将这些ktxcb结构附着在事务A的ksqrs的等待者链表中;TX事务锁除了用于记录锁的排队之外,还用于ITL Entry Shortage时事务的排队。当事务修改block中的数据时,首先需要在该block中占用一个ITL Entry。如果ITL Entry已经被用满,且无法动态扩展ITL时,本事务就需要阻塞等待。此时为本事务申请一个ktxcb结构,然后在本block的ITL中随机选择一个活跃事务,将ktxcb结构附着在该活跃事务的ksqrs结构的等待者链表上。这样当该活跃事务提交时,其占用的ITL Entry就会空出来,唤醒本事务复用该ITL Entry。实际上,Oracle不仅仅将enqueue机制应用于表锁和记录锁,而是将enqueue机制通用化,当系统资源冲突或者不足时都采用enqueue机制进行排队。enqueue机制通用化时,都是通过ksqrs进行排队,只是enqueue type不同。同时不同的资源,用于排队的结构也不同,ktqdm用于表锁,ktcxb用于事务锁,ksqeq、kdnssf、ktatrfil、ktatrfsl、ktatl、ktstusc、ktstusg、ktstuss等等都是用于各种internal locks。不过不管是表锁、事务锁,还是各种internal locks,最终都是通过_enqueue_locks参数设置总lock的数量。enqueue采用数组结构,同时又通过双向指针对数组中的结构进行分类管理。对于大小和属性相同的对象,Oracle一般采用数组这种数据结构进行管理。数组是采用分段方式进行分配和管理的,即Oracle初始只会分配一个容纳固定数量数据单元的内存块,然后在运行过程中动态分配更多的内存块。例如,x$ksqrs数组初始会申请一个较大的内存块,后继不够时再每次申请可容纳32个ksqrs结构的内存块,以此进行动态扩容。死锁Oracle的latch是通过对latch设置level属性在事前规避死锁,而lock的申请顺序和用户语句的执行时序强相关,无法通过事前规定lock的顺序来规避。因此,Oracle采用了事后检测的方法来解决死锁。当会话因为锁等待达到3秒后会醒来,这时会检查等待关系。如果存在循环等待表示存在死锁,否则进行下一个3秒周期的等待。如果检查发现存在死锁,就会触发ORA-60 deadlock detected错误,让应用参与决策。由于是事后超时检查死锁,所以一般是等待时间长的事务先报错。MySQL设计原理事务MySQL数据库支持ANSI SQL定义的Read Uncommitted、Read Committed、Repeatable Read、Serializable四种隔离级别,默认隔离级别为Repeatable Read。MySQL采用的是索引组织表,表中的记录时按照索引键或主键存放的,这就为加断言锁提供了基础。实际上,MySQL就是通过间隙锁锁住记录之间的间隙,从而达到断言锁的目的,防止幻读。各隔离级别下,MySQL的并发控制机制如下:Read Uncommitted:不使用一致性读,允许读取未提交事务的记录,因此会有脏读。只有更改记录或者用户强制lock read才会加锁,且只对记录加record_lock,不会间隙加锁;Read Committed:使用一致性读,且总是读取最新提交的快照数据,允许不可重复读和幻读,在外键检查时对间隙加锁,其它情况只对记录加锁;Repeatable Read:使用一致性读,且在同一个事务中读取的总是相同的历史快照数据,在更改记录或者用户强制lock read时对记录和间隙加锁,这样避免不可重读和幻读(在某些情况下可以只对记录加锁,如唯一索引等);Serializable:不使用一致性读,所有更改和读取操作都会加锁,加锁机制和可重复读一致;可见,MySQL的并发控制机制与“事务”章节介绍的Locking理论是最接近的,同时在Read Committed、Repeatable Read隔离级别下采用了一致性读机制(详细情况请参加“前像数据与回滚”章节),读不加锁,从而最大化地提高并发度。当然在Read Committed、Repeatable Read隔离级别下也可以通过lock read(select ... lock in share mode加共享锁,select ... for update加排它锁)主动对记录加锁,从而在较低隔离级别下也可以解决lost update、write skew等问题。记录锁表6.3-1 记录锁相容矩阵(行为已加锁类型,列为待加锁类型) LOCK_S_GAPLOCK_S_REC_NOT_GAPLOCK_S_ORDINARYLOCK_S_INSERT_INTENTIONLOCK_X_GAPLOCK_X_REC_NOT_GAPLOCK_X_ORDINARYLOCK_X_INSERT_INTENTIONLOCK_S_GAPYYYYYYYYLOCK_S_REC_NOT_GAPYYYYYNNYLOCK_S_ORDINARYYYYYYNNYLOCK_S_INSERT_INTENTIONYYYYNYNYLOCK_X_GAPYYYYYYYYLOCK_X_REC_NOT_GAPYNNYYNNYLOCK_X_ORDINARYYNNYYNNYLOCK_X_INSERT_INTENTIONNYNYNYNY记录锁类型包括共享锁(LOCK_S)和排它锁(LOCK_X)两种类型。MySQL支持对间隙加锁,所以有如下不同的锁算法:LOCK_GAP:间隙锁,仅对间隙加锁,锁住前一条记录和本条记录之间的间隙,但不包括本条记录和前一条记录本身;LOCK_REC_NOT_GAP:记录锁,仅锁住本条记录;LOCK_ORDINARY:Next_Key锁,是LOCK_GAP和LOCK_REC_NOT_GAP的组合,锁住本条记录以及本条记录和前一条记录之间的间隙,但不包括前一条记录;LOCK_INSERT_INTENTION:插入意向锁是一种特殊的间隙锁类型,又称为插入意向间隙锁(insertion intention gap lock),这种锁在插入操作执行前产生。假设已经存在两个索引值4和7,两个事务分别插入记录5和6,每个事务在插入数据前都能在(4, 7)中获得一个插入意向间隙锁,并且由于这两个事务插入的记录不相等而不会互相阻塞。但是,如果间隙(4, 7)之前已经被其它事务加上间隙锁,插入意向间隙锁就会被阻塞,从而防止前事务幻读;可见,MySQL支持两种锁类型,四种锁算法,这样共计可以组合出八种不同的锁,具体相容关系如表6.3-1所示,并从中可以发现如下规律:不管是哪种锁算法,共享锁与共享锁之间都是相容的,即LOCK_S_*和LOCK_S_*是相容的;不管已经持有的锁是哪种类型和算法,待加的LOCK_S_GAP和LOCK_X_GAP都是相容的,即GAP锁(不含插入意向锁)和所有已经持有的锁都是相容的,因为GAP锁主要用于防止将来其它事务的插入操作(避免幻读);LOCK_S_REC_NOT_GAP、LOCK_S_ORDINARY、LOCK_X_REC_NOT_GAP、LOCK_X_ORDINARY之间的不相容主要发生在记录本身的共享与排它、排它与排它的不相容;LOCK_S_INSERT_INTENTION和LOCK_X_INSERT_INTENTION表示即将进行插入操作,所以不相容性主要发生在GAP类的锁上,包括LOCK_S_GAP、LOCK_X_GAP、LOCK_S_ORDINARY和LOCK_X_ORDINARY;表6.3-2 lock_t结构域类型含义trxtrx_t本lock_t归属的事务trx_locksUT_LIST_NODE_T(lock_t)一个事务可能有多个lock_t结构,trx_locks用于将事务的多个lock_t结构链成链表,便于管理type_modeulint组合标志位:0-3bits:0 LOCK_IS、1 LOCK_IX、2 LOCK_S、3 LOCK_X、4   LOCK_AUTO_INC;4bit:LOCK_TABLE   表锁;5bit:LOCK_REC 记录锁;7bit:LOCK_WAIT   本锁处于阻塞等待状态;8bit:LOCK_GAP;9bit:LOCK_REC_NOT_GAP;10bit:LOCK_INSERT_INTENTION;hashhash_node_t用于构建lock_t结构组成的hash表,方便查找indexdict_index_t记录的索引un_memeberlock_rec_t或者lock_table_t具体的表锁结构或记录锁结构lock_bitmapbyte(var)锁位图图6.3-1 记录锁与记录之间的映射关系 和Oracle不同,MySQL是以独立的锁结构lock_t来管理锁信息的。最便捷的方式是为每个事务的每个记录锁申请独立的锁结构,但这样会引入数量庞大的锁结构,严重消耗内存资源,为此不得不采用多粒度锁机制,并进行复杂的锁升级。MySQL在速度和资源之间做了平衡,以每个事务处理的page为单位申请lock_t结构,即如果同一个事务对同一个page上多条记录加相同类型的锁,那么只需要申请一个lock_t结构。下面首先来看lock_t结构中最重要的lock_rec_t和lock_bitmap。如图6.3-1所示:lock_rec_t:对应于一个page,space和page no用于标识针对具体哪个page,nbits用于表达变长变量lock_bitmap的长度,lock_bitmap的字节数等于1+(nbits/8);lock_bitmap:变长,和page中的记录数强相关,MySQL每条记录的ROW HEADER结构中有一个REC_NEW_HEAP_NO(详细情况请参见“空间管理与数据布局”章节),用于对page内每条记录生成唯一的编号。这样lock_bitmap中的每个bit位对应于page中的一条记录,bit位的位置就对应于记录的REC_NEW_HEAP_NO,该bit位为1就表示对应的记录上有锁;可见,MySQL是按照page为单位组织锁结构的。优点是节约了内存资源,不需要引入复杂的锁升级机制。缺点是判断某条记录上是否有锁的效率相对较低,首先找到该page相关的所有lock_t结构(事务、锁类型和算法不同,同一个page会有多个lock_t),遍历这些lock_t结构,并根据记录的REC_NEW_HEAP_NO检查每一个lock_t结构中的lock_bitmap,以核实该记录上是否有锁。除了lock_rec_t和lock_bitmap之外,lock_t结构中的详细情况如表6.3-2所示,其中重要的成员还有:trx:指向本lock_t归属的事务,由此可得到对应的事务结构;trx_locks:双向链表,同一个事务可能申请多个lock_t结构,通过该指针将同一个事务的lock_t链接在一起;type_mode:锁的状态、类型以及算法等信息;hash:用于构建hash链表,MySQL会组建锁的hash表,方便以page为单位找到对应的lock_t结构;了解了锁的基本结构后,下面来看MySQL是如何组织lock_t的。MySQL中主要有两种情况查询锁:情况1:事务需要知道本事务已经持有了哪些锁,阻塞在哪个锁上;情况2:事务在扫描或修改某个page中的记录时,需要知道该记录上是否有锁,以及锁的类型和算法是什么;首先来看情况1,每个事务都会维护一个trx_lock_t结构,该结构包含如下关键成员:wait_lock:一个指向lock_t结构的指针,指向本事务当前等待的锁结构;trx_locks:类型为UT_LIST_BASE_NODE(lock_t),指向链表的指针,结合每个lock_t中的trx_locks将属于本事务的所有lock_t结构链接在一起,构成一个链表;wait_started:锁等待的开始时间;lock_heap:lock_t结构是动态生成的,维护本事务所有动态锁的内存;可见,通过wait_lock和trx_locks,事务将归属于本事务的所有lock管理起来。一个事务只可能阻塞等待在一个锁上,所以wait_lock只是一个指针。下面来看情况2,全局变量lock_sys会维护一个大的hash表(rec_hash)和因为锁等待而阻塞的线程(waiting_threads)。Rec_hash实际上就是按照space和page no对lock_t进行hash管理的大hash表。其中关键的成员有:array:hash表的桶数组;n_cells:hash表的桶数量,即桶数组的长度;sync_obj:互斥量数组,用于保护并发访问hash表;这样根据space和page no算出具体的hash值,从而得到对应page 所在的桶,即array数组的下标。然后遍历该桶对应的哈希链,即由lock_t结构组成的链表,比较lock_t.lock_rec_t结构中的space和page no,从而找到对应的page。由于存在多个事务对同一个page的不同记录加锁,所以同一个page会有多个lock_t结构,需要遍历这些结构。对于每个lock_t结构,比较记录REC_NEW_HEAP_NO对应的位图,从而判断是否有锁。至于锁的类型和算法,则根据lock_t中的type_mode来判断。图6.3-2 lock_t锁布局 如图6.3-2所示,每个事务维护一个trx_lock_t结构,通过该结构总额trx_locks和wait_lock以及每个lock_t的trx_locks指针,将属于某个事务的所有锁结构链接在一起。同时维护一张rec_hash表,将hash值相同的lock_t结构通过hash指针链接在一起,这就可以查询特定page的锁情况。多个用户线程会并发访问hash表,需要同步机制进行并发保护。考虑到并发性,会有多个mutexes(sync_obj),每个mutexes保护一段bucket数组以及后面的哈希链表,提高并发性。表锁表6.3-3 表锁之间的相容关系 LOCK_ISLOCK_IXLOCK_SLOCK_XLOCK_AILOCK_ISYYYNYLOCK_IXYYNNYLOCK_SYNYNNLOCK_XNNNNNLOCK_AIYYNNN表6.3-4 表锁之间的强度关系(Y表示行的强度大于列,N表示列的强度大于行) LOCK_ISLOCK_IXLOCK_SLOCK_XLOCK_AILOCK_ISYNNNNLOCK_IXYYNNNLOCK_SYNYNNLOCK_XYYYYYLOCK_AINNNNYMySQL的表锁和Oracle比较类似,也是通过多粒度锁解决效率问题,支持如下锁类型:LOCK_IS:意向共享锁;LOCK_IX:意向排它锁;LOCK_S:共享锁;LOCK_X:排它锁;LOCK_AUTO_INC:自增长锁,含有自增长列的表才会加该类型的锁;表锁之间的相容性如表6.3-3所示,之间的强度关系如表6.3-4所示。在实现上,表锁也是一个lock_t结构。和记录锁不同的是lock_t中的um_member不同,um_member是一个union结构。当lock_t为记录锁时,um_member为lock_rec_t结构。当lock_t为表锁时,um_member为lock_table_t结构。Lock_table_t结构中的关键成员有:table:指向dict_table_t类型的指针,表示本表锁归属于哪个表;locks:组成lock_t的链表,用于将归属于同一个表的所有lock_t结构链接在一起;图6.3-3 表锁布局 如图6.3-3所示,每个表在缓存中对应一个字典结构dict_table_t。Dict_table_t结构中的locks以及各个lock_t中的locks(实际上是um_member.lock_table_t.locks)将归属于同一个表的所有lock_t结构管理起来。Dict_table_t结构中的autoinc_lock将该表LOCK_AUTO_INC自增长锁独立出来,避免事务频繁地创建和释放该结构。表锁和记录锁都是lock_t结构,不同的是表锁不需位图结构,直接通过type_mode标识具体的锁类型。当然不管是表锁还是记录锁,从事务的角度来看,都是通过trx_locks和wait_lock进行管理的。聚集索引和辅助索引MySQL是索引组织表,索引又分为聚集索引和辅助索引,其加锁原则为:通过主键进行加锁的场景,仅对聚集索引加锁;通过辅助索引进行加锁的场景,先对辅助索引加锁,再对聚集索引加锁;在加锁的过程中,加锁策略和隔离级别、扫描类型、索引的唯一性等强相关。总的来说,规则如下:如果没有任何索引,需要全表扫描(或者覆盖索引扫描),所有记录全部加锁。RC与RR、Serialiable的区别是只在记录上加锁,不在间隙上加锁。当然MySQL出于性能的目的,对于不满足更改条件的记录会调用unlock_row提前释放锁,一定程度上违反了2PL;如果是非唯一索引,在[index first key, index last key)范围内加记录锁,如果是RR或者Serialiable隔离级别,间隙也需要加锁;如果是唯一索引,在[index first key, index last key)范围内加记录锁,如果是等值查询,即使是RR或者Serialiable隔离级别也不需要加间隙锁,因为唯一性已经保障不会出现幻读;隐式锁与显式锁虽然MySQL以page为粒度组织lock_t结构,以计算换空间(无法直接判断某行记录上是否有锁,需要遍历lock_t中的bitmap),一定程度上节约了内存资源。然而lock_t的量级仍然是事务数*page数*锁类型,锁资源的压力仍然非常大。为了节约锁资源,MySQL实现了一种称为隐式锁的延迟加锁机制。其核心思想是锁是非常消耗资源的,能不加锁就不加锁,只有在发生冲突时再加锁。显式锁是明确的锁,对应于lock_t对象,而隐式锁只是逻辑上的“锁”,没有lock_t对象,需要通过其它规则间接地发现该记录上有锁。如何判断某条记录上是否有隐式锁?对于聚集索引来说比较简单,每条记录上都有该记录的事务id(trx_id),如果该事务id对应的事务仍然是活跃的,那么该记录上有隐式锁,否则没有隐式锁。辅助索引比较复杂,每个page上都有一个PAGE_MAX_TRX_ID(该域在PAGE HEADER结构中,详细情况请参考“空间管理与数据布局”章节),用于表示更新本page的最后一个事务id。如果PAGE_MAX_TRX_ID比最小活跃事务id还要小,说明该page上的所有记录都没有隐式锁,否则需要找到对应的主键记录进行更加复杂的判断。图6.3-4 辅助索引与聚集索引的逻辑关系 如图6.3-4所示,现在需要判断辅助索引current_index_rec上是否有隐式索引,需要通过对应的聚集索引来判断。聚集索引结合undo日志可以构造出历史版本,包括聚集索引的历史版本和辅助索引的历史版本。有了这些历史版本之后,辅助索引上的隐式索引判断规则如下:current_trx不是活跃事务(通过current_cluster_rec中的隐藏事务id获得),current_index_rec上没有隐式锁;current_cluster_rec没有历史记录,表示本条记录是current_trx插入的,所以current_index_rec上有隐式锁;current_index_rec=history1_index_rec,但current_index_rec和history1_index_rec的delete flag不同,表示current_index_rec正在被current_trx删除,所以current_index_rec上有隐式锁;current_index_rec=history1_index_rec,且current_index_rec和history1_index_rec的delete flag相同,修改current_index_rec的事务既可能已经提交了,也有可能没有提交。如果current_trx!=history1_trx表示current_trx没有修改current_index_rec,所以没有隐式锁,否则要进一步判断history2;current_index_rec!=history1_index_rec,且current_index_rec和history1_index_rec的delete flag都为0,表示current_trx修改了current_index_rec,所以current_index_rec上有隐式锁;current_index_rec!=history1_index_rec,且current_index_rec的delete flag为1,修改current_index_rec的事务既可能已经提交了,也有可能没有提交。如果current_trx!=history1_trx表示current_trx没有修改current_index_rec,所以没有隐式锁,否则要进一步判断history2;通过上述规则,MySQL就可以通过比较和计算发现辅助索引上是否有隐式锁。在后继事务的加锁过程中,如果发现某条记录有隐式锁,那么以前事务的名义为该记录申请加显式锁。可见,在隐式锁机制下,只有发生锁冲突时才会加锁,为系统节约了大量资源:如果在原事务提交或回滚前,没有其它事务访问对应的记录,实际上所有的隐式锁都不会被转换为显式锁;如果在原事务提交或回滚前,其它事务访问该记录的某些辅助索引,只有被访问到的辅助索引才会被转换为显式锁,其它辅助索引上隐式锁仍然不会被转换;由于隐式锁只能通过规则和事务id进行判断,无法获取锁模式和锁类型等信息,所以隐式锁有如下限制:隐式锁针对的是记录锁,不可能是间隙或Next-Key类型;INSERT操作只加隐式锁,不加显式锁(包括聚集索引);UPDATE、DELETE在查询时,对查询用到的辅助索引和聚集索引加显式锁,其它二级索引使用隐式锁;记录锁的维护MySQL是以page为单位维护lock_t对象的,而page会随着数据的变化而变化,产生分裂、合并等现象。因此,lock_t对象也要随着page的分裂、合并而分裂、合并。分裂、合并的机制和原理基本一致,而分裂又分为左分裂和右分裂,其原理也是一致的,所以下面以右分裂为例来讲述记录锁的分裂维护。假设某page中的记录为R1、R2、R3、R4、R5、R6、R7,那么可以锁定的范围有:(infimum,R1](R1,R2](R2,R3](R3,R4](R4,R5](R5,R6](R6,R7](R7,supremum)此时page需要进行右分裂,分裂点为记录R4,即记录R4~R7需要迁移到一个新的page中。那么需要生成一个新的lock_t对象(right):left lock_t:(infimum,R1](R1,R2](R2,R3](R3,supremum);right lock_t:(infimum,R4](R4,R5](R5,R6](R6,R7](R7,supremum);Right lock_t的supremum继承于原lock_t对象的supremum,同时left lock_t对象的supremum和right lock_t的infimum需要根据分裂前(R3, R4]进行设置,即(R3,supremum)和(infimum,R4]要等效于分裂前的(R3,R4]。死锁MySQL对死锁采用了主动检测机制,其检测原理就是有向循环图。记录锁的hash组织方式为有向循环图的检测提供了充分必要条件。当某事务在加锁时因为锁冲突要等待,就开始进行深度优先的递归遍历,检测是否存在有向循环图。如果存在循环就表示有死锁,寻找一个undo量最小的事务进行回滚。PostgreSQL设计原理事务PostgreSQL数据库支持ANSI SQL定义的Read Uncommitted、Read Committed、Repeatable Read和Serializable四种隔离级别,默认隔离级别为Read Committed。在各隔离级别下PostgreSQL的并发控制机制分别如下:Read Uncommitted:实际上就是Read Committed;Read Committed:使用一致性读,且总是读取最新提交的快照数据,允许不可重复读和幻读;Repeatable Read:实际上是snapshot isolation,使用一致性读,且在同一个事务中读取的总是相同的历史快照数据,允许写倾斜(write skew);Serializable:实际上是serializable snapshot isolation,使用一致性读,并在snapshot isolation基础上加入SIREAD锁和RW-Conflicts机制,解决写倾斜异常,保证可序列化;可见,PostgreSQL的并发控制机制与Oracle和MySQL有很大的不同,通过snapshot isolation和serializable snapshot isolation机制实现Repeatable Read和Serializable。同时PostgreSQL也采用了锁机制,解决表级冲突以及记录级的写冲突,也支持通过在select语句上指定for update或者for share强制加记录排它或者共享锁。因此,PostgreSQL综合运用了乐观控制和悲观控制方法,以达到最优的并发控制效率。记录锁图6.4-1 Tuple结构 正常情况下PostgreSQL直接在记录上设置标志位就可以完成对记录加记录锁,不需要申请独立的内存锁结构,从而提高内存资源利用率和锁效率。如图6.4-1所示,每条记录都有一个HeadTupleHeaderData(详细情况请参考“数据前像与回滚”章节),该头部包含了如下重要信息:x_min:insert本条记录的事务id;x_max:delete/update本条记录的事务id;t_infomask:大量的组合标志位,通过综合这些标志完成记录锁的设置和判断,具体有HEAP_XMAX_KEYSHR_LOCK、HEAP_XMAX_EXCL_LOCK、HEAP_XMAX_LOCK_ONLY、HEAP_XMAX_COMMITTED、HEAP_XMAX_INVALID、HEAP_XMIN_COMMITTED、HEAP_XMIN_INVALID;图6.4-2 记录判断伪代码FOR each row that will be updated by this UPDATEWHILE TRUEIF (row1 is being updated) THENWAIT for the termination of the transaction that update row1IF (status of the terminated transaction is COMMITTED)AND (this transaction is REPEATABLE READ or SERIALIZABLE) THENABORT this transaction /*first-update-win*/ELSEGOTO step (2)END IFELSE IF(row1 has been updated by another concurrent transaction) THENIF (this transaction is READ COMMITTED) THENUPDATE row1ELSEABORT this transaction /*first-update-win */END IFELSEUPDATE row1 /*row1 is not yet modified or has been updated by a terminated transaction */END IFEND WHILEEND FOR了解了锁记录的标志位之后,我们以update语句为例来看PostgreSQL是如何基于锁进行并发控制的。如图6.4-2所示,判断过程要点如下:Step3:如果记录正在被更新,证明记录上有排它锁,有写写冲突,需要阻塞等待;Step5:当前事务被唤醒后,如果对方事务已经提交且隔离级别为Repeatable Read或者Serielizable,表示对方事务已经修改了当前记录,可能会引起Lost Update异常,当前事务必须强制退出,否则跳转到第2步,重新对本记录进行判断;Step11:如果记录已经被更新,更新该记录的事务已经提交,且该事务与当前事务是并发事务(即当前事务启动时该事务尚未提交)。如果当前事务的隔离级别为Read Committed直接修改该记录,否则强制退出当前事务以防止Lost Update异常;Step12:没有任何冲突,直接修改记录;可见,通过记录上的标志位即可判断出是否有冲突。同时PostgreSQL也支持通过select语句指定for update或者for share提前设置标志,解决频繁强制事务退出的问题。当然上述机制仍然存在一个问题,当存在冲突时,如何有效地对阻塞事务进行排队,这些就需要显式地申请记录锁,详细情况请参考后面的“表锁与记录锁”章节。表锁与记录锁表6.4-1 语句与表锁模式的对应关系模式名称模式id语句场景NoLock0 AccessShare1select RowShare2select for update/ for shareRowExclusive3insert/ update/ deleteShareUpdateExclusive4vaccum(non-full), analyze, create index concurrentlyShare5create index(without concurrently)ShareRowExclusive6任何postgresql命令不会自动获得这种锁Exclsuvie7任何postgresql命令不会自动获得这种锁AccessExclusice8alter table, drop table, vaccum full, unqualified lock table表6.4-2 表锁模式相容矩阵(行为已加锁类型,列为待加锁类型) AccessShareRowShareRowExclusiveShareUpdateExclusiveShareShareRowExclusiveExclusiveAccessExclusiveAccessShareYYYYYYNNRowShareYYYYYYNNRowExclusiveYYYYNNNNShareUpdateExclusiveYYYNNNNNShare      NNShareRowExclusiveYYNN NNNExclusiveYYNNNNNNAccessExclusiveYNNNNNNN和Oracle、MySQL一样,PostgreSQL出于效率考虑表锁也采用了多粒度机制,表锁的模式和相容矩阵如表6.4-1和6.4-2所示,不同的是PostgreSQL的VACCUM机制非常厚重,所以在表锁中需要引入相关的锁模式。在实现层面,不管是表锁还是显式的记录锁,都采用类似的机制,相关的结构分别为LOCKTAG、LOCK、PROCLOCKTAG、PROCLOCK、PGPROC、LOCKLOCKTAG、LOCALLOCK。需要注意的是,记录锁和表锁不同,记录锁只有共享锁和排它锁两种模式。表6.4-3 LOCKTAG结构域长度含义locktag_field14锁对象标识符locktag_field24锁对象标识符locktag_field34锁对象标识符locktag_field42锁对象标识符locktag_type1锁对象类型:LOCKTAG_RELATION:对表加锁,DB OID+RELOID;LOCKTAG_RELATION_EXTEND:对表加锁;LOCKTAG_PAGE:对page加锁,DB OID+RELOID+PageNumber;LOCKTAG_TUPLE:对记录加锁,DB OID+RELOID+PageNumber +OffsetNumber;LOCKTAG_TRANSACTION:TransactionId;LOCKTAG_VIRTUALTRASNACTIONID:VirtualTransactionId;LOCKTAG_SPECULATIVE_TOKEN:TransactionId;LOCKTAG_OBJECT:DB OID + CLASS OID + OBJECT OID + SUBID;LOCKTAG_USERLOCK;LOCKTAG_ADVISORY;locktag_lockmethodid1锁方法id:DEFAULT_LOCKMETHOD;USER_LOCKMETHOD;LOCKTAG用于标识某个具体被锁定的资源对象,locktag_type和locktag_lockmethodid分别用于标识锁定对象的类型和方法。例如,当locktag_type等于LOCKTAG_TUPLE时,表示锁定一条记录,即记录锁,此时locktag_field1等库对象ID,locktag_field2等于表对象ID,locktag_field3等于PageNumber,表示哪个Page,locktag_field4等于OffsetNumber,表示page内记录的偏移。可见,通过4个locktag_field就可以唯一确定一条记录。当然有时不需要设置所有的locktag_field,例如,当locktag_type等于LOCKTAG_TRANSACTION时只需要将locktag_field1设置为xid。图6.4-3 LOCK结构及与PGPROC、PROCLOCK间的关系 LOCK对象表示一个具体的锁对象,例如一个记录锁就是一个LOCK对象,一个表锁也是一个LOCK对象。如图6.4-3所示,LOCK对象详细描述了某个对象资源上的锁信息,具体情况如下:tag:类型为LOCKTAG,唯一地标识被锁定的某个资源对象;grantMask:类型为LOCKMASK,实际上就4个字节,通过bitmap标识已经在该资源对象上加了哪些锁模式,例如,如果第1个bit位设置为1表示已经加上AccessShare锁。通过1<<LockMode可以标识加上多个锁模式;waitMask:类型同grantMask,grantMask表示已经加上的锁模式,而waitMask表示正在等待的锁模式;procLocks:对tag资源对象加锁的进程列表,指向PROCLOCK对象,并通过PROCLOCK对象中的locklink指针将所有和本LOCK对象相关的PROCLOCK对象链接在一起;waitProcs:当锁模式不相容时,相关进程就需要阻塞等待,waitProc指向等待的PGPROC对象,并通过PGPROC对象的links指针将所有阻塞在本LOCK对象的PGPROC对象链接在一起;Requested、nRequested:本LOCK对象上各种锁模式被请求的次数,总次数,MAX_LOCKMODES为当前系统支持的锁模式数量;granted、nGranted:本LOCK对象上各种锁模式已经被授予的次数,总次数;图6.4-4 PGPROC结构 通过LOCK对象及其哈希表可以从资源的角度找到任何锁对象,从而确定该资源上的锁情况,这是第一个维度。然而我们还需要从事务或者进程的角度查看锁的情况,这是第二个维度。在进入第二个维度之前,我们首先来看PGPROC结构。PostgreSQL是多进程设计,每个后台进程在共享内存中都有一个PGPROC对象。如图6.4-4所示,PGPROC对象中与锁强相关的信息如下:links:和LOCK对象中的waitProcs指针相对应,用于将阻塞等待在同一个LOCK对象上的PGPROC链成一个链表;waitLock:指向本进程正在阻塞等待的LOCK对象;waitProcLock:指向本进程正在阻塞等待的PROCLOCK对象;waitLockMode:本进程阻塞等待的锁模式;heldLocks:本进程已经持有的锁模式;myProcLocks:本进程拥有的所有PROCLOCK对象,通过分区数组以及PROCLOCK中的procLink指针,将所有属于本进程的PROCLOCK对象链接在一起;图6.4-5 资源对象与进程之间的关系 表6.4-4 PROCLOCK结构域类型含义tagPROCLOCKTAGPROCLOCK对象标识符holdMaskLOCKMASK当前已经持有的锁模式releaseMaskLOCKMASK可以释放的锁模式lockLinkSHM_QUEUE用于将归属于同一个LOCK对象的所有PROCLOCK链接在一起procLinkSHM_QUEUE用于将归属于同一个PGPROC进程的所有PROCLOCK链接在一起表6.4-5 PROCLOCKTAG结构域类型含义myLockLOCK*指向LOCK对象的指针myProcPGPROC*指向PGPROC对象的指针LOCK对象描述了某个具体资源对象的锁情况,PGPROC对象描述了某个具体进程的锁情况。如图6.4-5所示,某个资源可以被多个进程加锁,某个进程也可以对多个资源加锁,所以LOCK对象和PGPROC对象时多对多的关系。PostgreSQL设计了PROCLOCK对象以维护LOCK对象和PGPROC对象之间的对应关系。每个PROCLOCK对象代表一个LOCK对象和一个PGPROC对象的对应关系。详细情况如表6.4-4和6.4-5所示,其中的关键信息如下:tag:唯一确定一个LOCK对象和PGPROC对象的对应关系;holdMask:该进程在该对象上已经持有的锁模式;releaseMask:该进程在该对象上可以被释放的锁模式;lockLink和procLink:分别按照Lock对象维度和PGPROC对象维度将相关的LOCKPROC对象链接在一起;表6.4-6 LOCALLOCK结构域类型含义tagLOCALLOCKTAGLOCALLOCK对象标识符lockLOCK*指向共享内存中对应的LOCK对象proclockPROCLOCK*指向共享内存中对应的PROCLOCK对象hashcodeuint32LOCKTAG hash值的拷贝nLocksint64该锁被本进程持有的总次数numLockOwnersint相关的lock   owner个数maxLockOwnersintlockOwners数组的大小lockOwnersLOCKLOCALOWNER*动态申请的lock   owner数组表6.4-7 LOCALLOCKTAG结构域类型含义lockLOCKTAG标识对应的LOCK对象modeLOCKMODE锁模式LOCK、LOCKPROC、PGPROC等对象都存放在共享内存中,运行时都访问共享内存,同时还要考虑互斥,代价比较高。为此,PostgreSQL的每个后台进程在本地维护了LOCALLOCK对象,更新LOCK、LOCKPROC、PGPROC等对象时同时更新LOCALLOCK对象。这样在访问锁时,如果LOCALLOCK对象已经满足要求,就可以不用访问共享内存,从而提高效率。例如,对同一个锁多次加锁或者释放只属于某个资源的锁。死锁对于死锁,PostgreSQL采用了事前预防和事后检测相结合的方式,具体包括:当进程加锁冲突时,就会进入等待队列。如果在队列中已有其它进程请求本进程已经持有的锁,为了避免死锁,可以将本进程插入到该进程的前面;当释放锁时,会尝试唤醒等待队列中的进程。如果某进程请求的锁与该进程前序进程的锁不相容,那么该进程不会被唤醒;通过上述方式,在尽量保证先请求先处理的原则下,尽可能规避潜在的死锁。然而,上述方法只是进行了简单的规避,并不能彻底解决死锁,完全解决需要通过有向等待图来解决,但成本较高,PostgreSQL将这一过程放在了事后。图6.4-6 死锁检测的触发过程 如图6.4-6所示,当阻塞等待超时后就开始进行死锁检测。不过PostgreSQL在有向循环图中引入了Soft Edge和Hard Edge的概念:Soft Edge:进程A和进程B都在同一个锁的等待队列中。进程A和进程B的锁请求不相容,且进程A在进程B的后面,这时进程A指向进程B的有向边为Soft Edge;Hard Edge:进程A请求的锁和进程B已经持有的锁冲突,这时进程A指向进程B的有向边为Hard Edge;可见,Soft Edge是可以通过重新排队进行规避的,而Hard Edge已经形成,是无法改变的。有了Soft Edge和Hard Edge概念之后,我们来看看PostgreSQL是如何进行死锁检测的:从每一个点出发,沿着有向循环图的有向边行进,如果能够回到起点,说明存在死锁;在遍历过程中将Soft Edge记录下来,如果存在死锁且没有Soft Edge,直接终止本事务;如果有Soft Edge。对于每个Soft Edge,递归枚举它的所有子集,尝试进行调整。调整方法采用拓扑进行排序,并遍历测试,如果通过测试表明可以规避死锁,直接结束。如果调整任何一个Soft Edge都无法解决死锁,终止本事务;SIREAD锁和RW-Conflicts图6.4-7 写倾斜于依赖图 在Serializable隔离级别下,PostgreSQL可以解决所有异常,其采用的方法并不是读写都加断言锁和记录锁,而是采用SSI策略(详细情况请参考“事务”章节)。如图6.4-7所示,当依赖图(dependency graph)中存在循环,表示存在写倾斜异常,需要强制某个事务退出,从而打破循环,保证可序列化。可见,SSI的重点是标识rw关系和检测依赖图中是否有循环,为此PostgreSQL定义了SIREAD锁和RW-Conflicts两种数据结构。为了构建RW-Conflicts,首先需要表示出哪些事务读取了哪些记录,这就是SIREAD锁的作用。当执行DML语句时,CheckTargetForConflictsOut函数会创建SIREAD锁。例如,当事务txid1读取记录tuple1时会创建SIREAD锁{tuple1, {txid1}},之后事务txid2也读取记录tuple1时该SIREAD锁会更新为{tuple1, {txid1, txid2}}。可见,SIREAD锁是以记录为单位跟踪相关事务。然而在高并发下,SIREAD锁的数量会非常大,严重消耗系统资源。为此,PostgreSQL采用锁升级的机制来缓解资源消耗。SIREAD锁有tuple、page、relation三个层次。如果某个page的所有tuple都创建了SIREAD锁,那么升级为page级,即以page为单位创建SIREAD锁,原来属于该page的tuple级SIREAD锁全部释放。Relation级即表级,原理同page级。RW-Conflicts是一个三元组,由读事务、写事务、记录(元组)组成。例如,事务txid1读取了记录tuple1,之后事务txid2更新了记录tuple1,那么就需要创建一个RW-Conflict,{txid1, txid2, {tuple1}}。在执行insert、update、delete命令时,CheckTargetForConflictsIn函数会检查相关SIREAD锁,从而判断是否存在RW-Conflicts。如果存在,就创建RW-Conflicts。表6.4-8 写倾斜检测示例一时间Tx_A(txid_a)Tx_B(txid_b)SIREAD LocksRW-ConflictsT1start transaction isolation level serializable;start transaction isolation level serializable;  T2select * from t1 where id=2;(1 row returned) L1:{pkey_2,{txid_a}}L2:{tuple_2,{txid_a}} T3 select * from t1 where id=1;(1 row returned)L1:{pkey_2,{txid_a}}L2:{tuple_2,{txid_a}}L3:{pkey_1,{txid_b}}L4:{tuple_1,{txid_b}} T4update t1 set val=”++” where id=1;(1 row updated)  C1:{r=txid_b, w=txid_a, {pkey_1, tuple_1}}T5 update t1 set val=”++” where id=2;(1 row updated) C1:{r=txid_b, w=txid_a, {pkey_1, tuple_1}}C2:{r=txid_a, w=txid_b, {pkey_2, tuple_2}}T6commit;(success)   T7 commit;(failed)  表6.4-9 写倾斜检测示例二时间Tx_A(txid_a)Tx_B(txid_b)T1start transaction isolation level serializable;start transaction isolation level serializable;T2select * from t1 where id=2;(1 row returned) T3 select * from t1 where id=1;(1 row returned)T4update t1 set val=”++” where id=1;(1 row updated) T5commit;(success) T6 update t1 set val=”++” where id=2;(failed)表6.4-10 写倾斜检测示例三时间Tx_A(txid_a)Tx_B(txid_b)T1start transaction isolation level serializable;start transaction isolation level serializable;T2select * from t1 where id=2;(1 row returned) T3 select * from t1 where id=1;(1 row returned)T4update t1 set val=”++” where id=1;(1 row updated) T5 update t1 set val=”++” where id=2;(1 row updated)T6commit;(success) T7 select * from t1;(failed)假设表t1,在id列上有主键索引。表6.4-8给出了写倾斜检测的详细过程,具体如下:T2:事务Tx_A查询id为2的记录,由于涉及主键,所以CheckTargetForConflictsOut创建了两个SIREAD Locks,分别为L1和L2;T3:事务Tx_B查询id为1的记录,由于涉及主键,所以CheckTargetForConflictsOut创建了两个SIREAD Locks,分别为L3和L4;T4:事务Tx_A更新id为1的记录,CheckTargetForConflictsIn检测SIREAD Locks,发现存在RW-Conflicts,所以创建RW-Conflicts C1;T5:事务Tx_B更新id为2的记录,CheckTargetForConflictsIn检测SIREAD Locks,发现存在RW-Conflicts,所以创建RW-Conflicts C2。此时依赖图已经存在循环,即写倾斜已经产生,然而事务Tx_A和Tx_B都没有提交,所以CheckTargetForConflictsIn无法基于“first-committer-win”原则决策让哪个事务失败;T6:事务Tx_A提交,PreCommit_CheckForSerializationFailure函数检查发现Tx_A和Tx_B存在写倾斜,但Tx_B事务仍然处于运行状态,所以事务Tx_A提交成功;T7:事务Tx_B提交,PreCommit_CheckForSerializationFailure函数检查发现Tx_A和Tx_B存在写倾斜,但Tx_A事务已经提交,所以事务Tx_B提交失败;从上述过程我们发现SIREAD Locks和RW-Conflicts不能在事务提交后立刻释放,需要存在一段时间,以确保相关事务的写倾斜检测能够正常进行。另外,并不意味着事务的倾斜异常只会发生在提交阶段。事实上,CheckTargetForConflictsIn和CheckTargetForConflictsOut都会进行依赖图检测,只要存在循环,且有一个事务已经提交,就会立刻让当前事务失败,例如表6.4-9和6.4-10。CockroachDB设计原理设计思路CockroachDB是一种基于乐观机制的分布式数据库,其默认的隔离级别是可序列化快照(SS, Serializable Snapshot)。和PostgreSQL相比,CockroachDB不采用锁机制,而是将SS发挥到极致,其采用的并发控制有如下特征:可序列化:执行结果和某种串行执行的结果是等价的;可恢复:对于一系列并发执行的事务,有些事务执行成功,有些事务异常退出,仍然能够保证系统可恢复至一致性状态。原子性保证单个事务是可恢复的,严格的乐观调度策略保证任何事务的组合执行也是可恢复的;无锁:执行期间不会在资源上加锁。如果某事务和可序列化、乐观调度机制相关冲突,通过强制该事务退出来保证正确性;分布式:系统无集中的授时、协调或者其它服务;可序列化图在序列化理论中,冲突的发生条件是两个不同事务中的操作操作了相同的数据,且至少有一个操作是写操作。满足上述条件时,就可以说第二个操作和第一个操作相冲突。冲突有三种类型:读写冲突(RW):第二个操作覆盖了第一个操作读取的结果;写读冲突(WR):第二个操作读取了第一个操作写的结果;写写冲突(WW):第二个操作覆盖了第一个操作写的结果;图6.5-1 可序列化图示例 对于事务执行的任何历史,通过这些冲突可以建立一个可序列化图。如图6.5-1所示,将所有事务链接在一起的有向图有如下部分组成:事务是图中的节点;当某操作和另外一个事务的操作冲突时,就画一个从被冲突事务到冲突事务的有向边;图6.5-2 循环可序列化图示例,该历史不可序列化 执行历史是可序列化的,当且仅当可序列化图是非循环的。图6.5-2中的示例就是不可序列化的。CockroachDB采用时间排序来保证可序列化图是非循环的,方法如下:每个事务启动时都会赋予一个时间戳,此后该事务中的所有语句都使用此时间戳;每个操作都可以独立地判断自己和其它事务的哪个操作冲突,以及被冲突操作的时间戳是什么;允许操作和拥有更早时间戳的其它操作相冲突,但不允许和拥有更晚时间戳的操作相冲突;由于在时间前进方向上不允许存在冲突,所以可序列化图就不存在循环。下面章节我们将介绍CockroachDB是如何检测和防止这些冲突的。WR冲突与MVCCWR冲突采用多版本来解决。CockroachDB不仅仅存储单值,而是存储了基于时间戳的多个版本值。写操作不会覆盖旧值,而是创建一个带新时间戳的新值。图6.5-3 多版本值读示例 如图6.5-3所示,对某key的读操作将返回比读操作时间戳小的最新版本。因此,在CockroachDB中后继事务不会形成WR冲突,因此读操作不会使用更晚的时间戳。RW冲突与时间戳缓存任何读操作的时间戳都会缓存在时间戳缓存中。通过该缓存我们可以查询某个key最近进行了哪些读操作,以及这些读操作的时间戳是怎样的。所有写操作在对key进行写时都需要查询时间戳缓存。如果返回的时间戳大于写操作的时间戳,表明RW和一个更晚的时间戳相冲突。这是不允许的,必须以一个更晚的时间戳重启写操作所在的事务。时间戳缓存是一个区间缓存,也就是说其存储的是key的范围。如果某读操作读取了某段范围内的所有key(例如扫描),那么扫描的这些key都以范围的形式存在时间戳缓存中。时间戳缓存完全缓存在内存中,采用LRU算法。当缓存大小达到设定的限制后,最老的时间戳条目就会被删除。为了处理不在缓存中的key,需要定义“低水位线”,其等价于所有key的最早时间戳。如果写操作查询的key不在时间戳缓存中,就返回低水位线。WW冲突与只写最新版本写操作尝试写某key时,该key的时间戳比操作本身的时间戳还要新,表明WW和一个更晚的时间戳相冲突。为了高正可序列化,必须以一个更晚的时间戳重启写操作所在的事务。通过时间排序,拒绝任何不满足排序要求的冲突,CockroachDB的SS可以保证执行结果是可序列化的。严格调度与可恢复性通过前面章节介绍的冲突规则可以保证执行历史是可序列化的。另一个问题是如何保证两个满足冲突规则的未提交事务是可恢复的。假设两个事务T1和T2,T1的时间戳小于T2的时间戳。T1写了key“A”,之后T2在T1提交前读取key“A”。该冲突是被时间排序规则所允许的。但T2应该从key“A”中读到哪一个值呢?假设忽略掉T1的未提交数据,读取数据的前一个版本。如果T1和T2都成功提交,这将引起WR冲突,且和时间排序规则相冲突,因此不可序列化;假设读取T1的未提交数据。如果T2提交成功,T1回滚了,这和T1的原子性相冲突(T1回滚了,但仍然对数据库的状态产生了影响);上述两种情况都是不允许的。为了维护调户的可恢复性,在T1提交前T2不可以提交。为此,CockroachDB采取了严格的调度策略处理此场景:读操作和覆盖操作只允许作用在已提交数据上,操作永远不允许在未提交数据上实施。为了实现原子性提交,key上的未提交数据都保存在意向记录中(Intent Record)。如图6.5-4所示,在MVCC存储结构中,key上的意向记录可以很容易地被查到。在并发环境中,意向记录意味着存在一个正在运行的并发事务。图6.5-4 意向记录与MVCC 严格调度存在两种场景:读操作遇到一个时间戳更小的意向记录,或者写操作遇到一个意向记录(不管时间戳的大小)。对于这两种场景,CockroachDB有两种选择:如果第二个事务的时间戳更大,该事务可以等待第一个事务提交或回滚完毕,然后再继续执行自己的操作;强制其中一个事务退出;作为一种乐观的系统(无等待),CockroachDB选择了强制退出其中一个事务。决策将哪个事务退出的过程如下:step1:第二个事务(遇到意向记录的那个事务)读取第一个事务的事务记录(CockroachDB为每个活跃事务维护一条事务记录,以表征该事务的提交状态);step2:如果第一个事务已经提交(意向记录还没有来得及清理),第二事务清理该意向记录,即将意向记录中的值当成正常值来处理;step3:如果第一个事务已经回滚,第二事务删除该意向记录,并将意向记录当成不存在处理;step4:如果第一个事务处于运行态(未提交),固定选择第一个或第二个事务都是不合理的。同时还存在两个事务同时处理对方,对于冲突的两个事务,胜利的一方最好是确定性的。为此,每个事务记录都赋予一个优先级,永远强制退出优先级地的那个事务。如果优先级相等,强制退出时间戳大的事务。新事务启动时获取一个随机的优先级,当事务因为冲突而重启时,其新的优先级等于max(random, [导致本事务重启的哪个事务的优先级]-1),最终事务在重启的过程中优先级会不断提升。采用本方法,未提交事务之间的冲突可以通过强制退出其中一个事务而立刻得到解决。因此,严格调度确保了所有的事务执行历史都是可恢复的。优先级已经在概率上解决了导致异常事务的问题,即被异常打败的事务会不断地重启,且在重启的过程中优先级会不断地上升,最终获得胜利。另外,CockroachDB在所有事务中增加了心跳。在运行过程中,活跃事务需要周期性地更新其事务记录中的心跳时间戳。如果其它事务碰到某事务的记录时,该事务的心跳时间戳超时,那么该事务被认为是异常事务,此时强制异常事务退出而不是比较优先级。VoltDB设计原理传统数据库的成本Micheal Stonebraker等人在开源数据库Shore上进行了各种基准测试,以调研传统数据库中各组件的成本。测试环境为桌面系统,刚开始性能大约为640TPS。之后每次删除系统中的一个特征,并重新进行基准测试,直至仅剩下一个非常薄的查询内核,性能为12700TPS。这个内核是单线程、无锁、无恢复功能的全内存数据库。通过分解发现了4个影响性能的最大组件:Logging:跟踪数据结构的所有变化并记录日志,拖慢了性能。如果可恢复性不是必须的,或者可通过集群中其它节点进行恢复,日志就不是必须的;Lock:两阶段锁产生了相当大的负载,因为所有对数据的访问都要经过Lock Manager这个单点组件;Latch:在多线程数据库中,很多数据结构在被访问前都要先加上Latch,通过单线程机制可以避免这个诉求,并获得可观的性能提升;BufferManager:内存数据库不需要通过缓存池访问数据页,消除了访问每条记录的间接成本;表6.6-1 传统数据库各组件指令数占比组件New OrderPaymentBtree keys16.2%10.1%Logging11.9%17.7%Locking16.3%25.2%Latching14.2%12.6%Buffer manager34.6%29.8%others6.8%4.7%图6.6-1 NewOrder下各组件指令占比 图6.6-1和表6.6-1给出了这些挑战对应的性能变化情况(测试模型为TPC-C下NewOrder事务和Payment事务,统计的是运行该事务的CPU指令数)。可见每个组件都占整个系统的10%~35%指令数(整个系统运行一遍NewOrder事务的指令数为1.73M)。“hand-coded optimizations”代表的是对B树进行一系列优化。“useful work”代表的是处理查询的实际工作,只占总工作的1/60。“buffer manager”下面的方框代码的是移除上面所有组件之后的性能,这时仍然支持事务,指令数只有总体的1/15,不过仍然是实际工作的4倍(两者之间的差距主要源于函数调用栈的深度,以及无法完全消除缓存管理和事务相关的所有代码)。基于上述分析,Micheal Stonebraker在设计VoltDB时,期望通过裁减Buffer Manager、Latch和Lock等组件以获得更高的性能。因此,VoltDB是一款仅支持序列化隔离级别的分布式内存数据库。内存数据库可以降低Buffer Manager的成本,仅支持序列化隔离级别可以降低Latch和Lock的成本。本章重点讨论VoltDB的并发控制是如何避免Lock成本的。图6.6-2 串行执行队列 假设只有单颗CPU和DRAM内存,我们应该设计一个怎样的程序,在单位时间内仅可能多地执行命令。这些命令可以是创建、查询或者更新结构化数据。如图6.6-2所示,解决方案之一就是将命令放在一个队列中。然后执行一个循环,不断地从队列中取命令并执行。显而易见的是此方法可以让单颗CPU充分运转起来,当然有几纳秒的时间周期用于从命令队列中取命令和将响应放入响应队列中。在循环中,CPU执行的任务基本上100%都是实际工作,而不是系统调度、锁控制、缓存控制等和实际工作不相关的工作。在VoltDB中,用户的命令就是SQL执行计划、分布式分片上的执行计划、或者存储过程的调用,循环就对应于单个分片上的命令队列。并发控制VoltDB每次只会运行一个命令,命令之间无并行无重叠,从而提供了序列化的隔离性。在单颗CPU上高饱和地运行应用的实际工作。然而服务器上有多颗CPU,如何让多颗CPU都高饱和地运行起来?首先对数据进行分片,然后在每个分片上维护一个命令队列。这也是大部分分布式NoSQL数据库的设计思路:操作需要制定待操作数据的KEY。VoltDB采用的是一致性哈希分片,用户需要为每个表指定分片列。这些分片列和NoSQL存储的KEY非常类似。根据分片列判断SQL语句或者存储过程涉及哪个分片,然后将其路由到对应分片的命令队列上。集群中多个服务器或者服务器上多颗CPU,都可以通过增加分片的方法让各CPU繁忙起来,每颗CPU独立运行某个分片上的命令队列,各自提供ACID语义。可见:在每个分片上串行地执行查询或者修改命令;命令可以是SQL、存储过程、SQL执行计划的某个片段;每个命令都提供ACID;表数据分布在各个分片上;通过增加分片的方法在多CPU、多服务器上获得扩展性;事务VoltDB将存储过程作为单独的事务来执行,SQL语句作为自动提交的事务来执行。单分片事务是在单个分片上直接执行的事务。单分片事务可以是只读事务,也可以是读写事务,每个单分片事务都完全满足ACID。实际上单分片只读事务的执行过程可以进一步优化,即越过SPI,以负载均衡的方式直接路由到分片的某个副本上。VoltDB的副本间是以同步的方式执行读写事务,所以只读事务即使越过SPI,仍然可以读到前面事务的结果。此优化可以提升只读事务的吞度量,降低只读事务的延时,减轻SPI的工作量。图6.6-3 只读事务在分片的副本间负载均衡 图6.6-3以示例的方式展示了优化的正确性,事务A和事务C为读写事务,事务B为只读事务,且应用的发起顺序为事务B先于事务C而后于事务A。事务B放在任何一个副本的序列化命令队列中都是正确的(不影响其它副本的结果)。VoltDB支持事务在多分片上进行读写操作,这样的是称为多分片事务。SPI为单个分片实施序列化工作,MPI为跨分片事务实施序列化共诺。MPI会和相干分片的SPI交互,以将分解后的命令注入到对应分片的命令队列中。图6.6-4 只读事务在分片的副本间负载均衡 图6.6-4示例了MPI执行多分片事务M的过程。SPI#1将事务M序列化在单分片事务C的后面执行,SPI#2将事务M序列化在单分片事务B的前面执行。从全局来看,事务的执行顺序为C、M、B。为了执行多分片SQL,VoltDB的SQL执行计划生成器会将执行计划分解成多个片段,有些片段在多个分片上分布式执行,有些片段对分布式执行的结果进行汇总。多分片写事务在各分片间采用两阶段提交协议。在Prepare阶段,MPI将执行接话片段分发到各个分片执行。如果这些片段在各分片上执行成功,无约束性冲突,MPI通知所有分片进行提交。各分片不会执行命令队列中的任何其它命令,只到收到提交消息。VoltDB中多分片事务的大部分案例是分布式读,要么是读取记录时不知道分片的取值,要么是进行汇聚分析。对于仅含只读工作的多分片事务,用户可以通过标签显式地表达出来,这样上述分布式过程就可以进行优化:SPI可以将命令发给任何某个副本,而不需要在副本间同步;分片执行完读操作后,可以立刻执行命令队列中的其它命令,而不是阻塞在那里等待提交消息;总结与分析并发控制的原则是在保证正确性的前提下尽可能地提高并发性,为此Oracle、MySQL、PostgreSQL、CockroachDB、VoltDB采用了不同的策略以提高并发性。从并发控制算法的用户友好度和ANSI SQL隔离级别匹配度来看。MySQL支持ANSI SQL定义的四种隔离级别,在任何情况下都会加记录级写锁,避免了Dirty Write异常。在Read Committed级别下通过语句级一致性读解决了Dirty Read异常。在Repeatable Read级别下,通过事务级一致性读以及Next_key写锁解决了Fuzzy Read和Phantom异常,但由于读不加锁,仍然存在Lost Update和Write Skew异常。在Serializable级别下,读写都加Next_key锁,可以解决所有异常。PostgreSQL真正意义上仅支持三种隔离级别(Read Uncommitted实际上就是Read Committed),在任何情况下都会加记录级写锁,避免了Dirty Write异常。在Read Committed级别下通过语句级一致性读解决了Dirty Read异常。在Repeatable Read级别下,通过事务级一致性读以及写锁解决了Lost Update、Fuzzy Read、Phantom异常,但由于读不加锁,Lost Update异常只能采取“First-Update-Win”原则,对用户不友好,而Write Skew异常仍然无法解决。在Serializable级别下,通过SSI算法进一步解决Write Skew异常,但解决的方法是一旦发现潜在的Write Skew,就强制某个事务退出,对用户并不友好。Oracle仅支持Read Committed和Serializable两种隔离级别,在任何情况下都会将记录级写锁,避免了Dirty Write异常。在Read Committed级别下通过语句级一致性读解决了Dirty Read异常。在Serializable级别下,通过事务级一致性读和SCN比较,解决了Lost Update、Fuzzy Read和Phantom异常,读不加锁导致Lost Update只能通过报错来解决,对用户不友好,同时读不加锁导致Write Skew异常无法解决。CockroachDB支持Snapshot Isolation和Serializable Snapshot Isolation隔离级别,通过多版本和时间戳排序达到可序列化要求。然而为了可恢复新采用了严格的调度策略,不管是读操作还是写操作一旦遇到比自己跟到且未提交的时间戳,必须强制一个事务退出,对用户不友好。VoltDB仅支持Serializable隔离级别,所有事务都串行执行,不存在任何异常。可见在ANSI SQL隔离级别匹配度上MySQL最高,然后依次是PostgreSQL、Oracle、CockroachDB和VoltDB。MySQL、PostgreSQL、Oracle都支持用户在select语句上指定加锁,这样即使在低隔离级别上也可以选择性地解决Lost Update和Write Skew异常。从并发控制算法的效率上来看,Oracle没有设计独立的锁结构,仅在记录上通过1个字节的lb表达出锁信息,理论上锁资源是无穷的。Enqueue机制对等待的事务进行排队,并区分拥有者和等待者,进行非常精准的唤醒。MySQL和PostgreSQL的锁机制比较类似,正常情况下通过记录上的标志位进行判断(判断规则比较复杂),一旦出现冲突则转换为显式锁。在显式锁方面,MySQL以page为单位组织锁资源,在空间和时间上做了权衡。PostgreSQL采取了记录、page、表多粒度的方式组织锁资源。在冲突对事务进行排队时,两者相对Oracle都比较粗糙。和MySQL不同的是,PostgreSQL在SSI方面又引入了SIREAD锁和RW-Conflicts,对所有读操作、读写操作都要进行跟踪记录,并进行检索判断,成本非常高。CockroachDB需要对所有记录的读操作维护时间戳,成本较高。当然由于采用的是乐观控制,在低冲突场景下效率相对较高,在中高冲突下由于要频繁地重做,效率是极低的。VoltDB采用的是串行执行策略,效率非常高。但场景首先,需要以存储过程为事务执行单位,减少应用和数据库之间的来回交互,同时负载要有非常好的可切分性,每颗CPU负责一个分片,分片之间无相关性。可见,在效率上Oracle是最高的,MySQL和PostgreSQL相当。CockroachDB和VoltDB引入了新思路,但场景相对受限。Oracle、MySQL、PostgreSQL采用了锁机制,存在死锁的情况,三者都采用有向循环图的检测方法。Oracle认为死锁检测的代价较大,只有在锁等待超时后才会检测死锁。MySQL在发生锁等待时提前进行死锁检测,提前解决死锁问题。PostgreSQL也采用了锁等待超时后进行检测的策略,但在事前和事后都做了一些小的优化,尽可能地避免死锁。PDF版本下载地址:http://blog.itpub.net/69912723/viewspace-2725664/
文章
机器学习/深度学习  ·  SQL  ·  缓存  ·  Oracle  ·  算法  ·  关系型数据库  ·  MySQL  ·  数据库  ·  PostgreSQL  ·  索引
2023-02-23
云数据库行业动态【2023年2月版】
作者:NineData 工程师,让每个人用好数据和云本文整理2023年2月份最新数据库厂商、数据库领域的行业动态、以及各家云数据库厂商的产品动态。阅读此文你将了解:数据库厂商、数据库领域的行业动态,重磅发布、技术突破等;常见数据库最新产品动态、新功能、版本升级,软件优化等;大佬推荐的技术内容、经验分享、推荐阅读等重要更新阿里云RDS SQLServer 发布Serverless公测版去年4月份,阿里云RDS MySQL Serverless 发布公测版本,当时我们同事第一时间做了“开箱实测”并有初步结论:阿里云 RDS MySQL Serverless版本的升/降配速度非常快,约10秒完成压力检测与变配,升配时性能表现非常平稳,降配时性能比较平稳(详见 实测阿里云RDS Serverless )。不到一年,阿里云推出SQLServer 的Serverless 公测版本,且此版本是支持高可用的(MySQL Serverless 至今只支持基础版)。至此,阿里云在开源和商业数据库均有Serverless 形态产品,离自研的云原生数据库Serverless 发布应该不远了。召开数据库迁移工具能力要求会议2月22日下午,中国通信标准化协会大数据技术标准推进委员会线上召开《数据库迁移工具能力要求》标准第一次讨论会。根据通告,本次会议主要讨论数据库迁移工具能力域、适用范围以及适用对象,并确定了数据库应用迁移工具的定义及要求。就在前两天,国家最高领导人强调基础软件要早日实现自主可控,数据库作为基础软件三驾马车之一理应走在前面。数仓巨头Teradata退出中国市场近日,数仓巨头Teradata宣布退出在中国,后续将进入中国公司关闭程序。据相关媒体报道,目前公司有1200多名员工,外企的补偿方案应该可以让这些大厂“毕业生”休息几个月。就在不久前Gartner 2022报告中,Teradata与 AWS、Oracle等企业一同被评为“领导者”,可见其在行业内依旧有很大影响力。巅峰期Teradata一度是数仓领域明星产品,MPP架构也影响了之后重要的数仓产品,如Greenplum、Redshift、BigQuery。可由于它主要用于数仓领域,所以去TD不像去O那么困难,导致这几年在国内业务不仅没有增长,反而一直下降,离开中国或许时间问题。TD的离开也说明国产基础软件、信创等有了重要进步。阿里云Serverless数据库在金融企业的生产实践金融科技企业微财引入阿里云瑶池数据库Serverless备份解决方案,微财也成为金融行业首个大规模应用云数据库RDS Serverless的企业。云数据库经过十几年的发展,第一代代表产品RDS,它的特点是重管控。第二代产品是以AWS Aurora为代表,它在架构上实现了存算分离,比较明显地增强数据库的扩展性和性能。而Serverless是云数据库重要发展趋势,虽然此案例是Serverless的RDS作为一个灾备节点,但相信未来会在越来越多的重要场景普遍的使用,真正实现数据库的“按量付费”。InfluxDB 完成 5100万美元E轮融资近日,时序数据库 InfluxDB 完成 5100万美元融资,随着此次融资的结束,InfluxDB 母公司的股权融资总额为 1.71 亿美元。时间序列数据(Time Series DataBase TSDB)是专门用于处理和时间相关数据的数据库,早期比较常见使用场景是IT运维、工业监控系统中。近几年,随着实时数仓的兴起,TSDB也有了更多的应用场景,人们也愈发发现其重要性,InfluxDB 一直是TSDB领域的强有竞争力的领导者产品。Uber和Oracle达成7年全面战略合作2023年2月13日,Oracle 和Uber 建立为期七年的战略云合作伙伴关系,本次交易对于甲骨文云业务来说是一个巨大的胜利,赢得了一块价值超过10亿美元的蛋糕。此外,Oracle 公司将成为 Uber 的全球商业客户,以 Uber 作为全球员工出行和外卖的首选平台。云计算的浪潮浩浩荡荡,Oracle 大有奋起直追的趋势,未来其可能不再是一个简单的数据库公司。而如果以云计算估值企业,Oracle资本价值可能会有变化。中国数据复制第一股——英方软件22023年1月19日,英方软件成功登陆科创板,从而成为中国数据复制第一股,首日涨幅达176.77%,当前股价106元。根据其官网介绍,公司主要产品是数据集成方面。IDC数据,2021年公司以10.2%的市占率位居我国数据复制与保护纯软件市场第三,国产数据复制纯软件市场第一。Apache Hudi 商业公司宣布2500万美元融资Apache Hudi 背后商业公司Onehouse宣布2500万美元A轮融资。Hudi最早是由出行巨头Uber公司开源的数据湖解决方案,它能够基于HDFS上管理大型数据集,并且支持对数据进行插入、更新、增量消费等操作。另据介绍,Onehouse是一家位于美国加州的托管数据湖公司。2022年下半年以来,全球数据库创业企业都面临着融资规模和次数都收缩的情况下,数据湖产品还能进行此规模的A轮融资,可见资本对基础软件依旧很有信心。2022 数据库发展研究报告进由官方背景的通信院发布的这则报告,主要针对数据库市场规模、数据库形态、企业数量、从业人员、学术影响以及资本情况等六个方面介绍。该报告说,到了2026 年,中国数据库市场总规模将达到 930 亿元,市场年复合增长率 (CAGR)为 24.9%,国内从业人员不足2万人,那人均有500万的产值。笔者作为从业人员,清楚的明白这个人均的数字是远远达不到的,显然这里的差额是被国外厂家给赚走了,国产数据库以及从业者当自强,像手机、新能源汽车行业学习。2022中国数据库报告近日,云和恩墨发布2022年中国数据库报告,此报告主要包括国产数据库类型、数据库流行的趋势、学术论文、商业市场以及数据库未来发展等几个维度。其中年度最流行的三个数据库,分别是TiDB、OceanBase、 openGauss。另外,作者认为数据库技术未来趋势是HTAP、云原生、serverless、内存技术等。该报告也分析了,在数据库学术论文发表数量上,国内呈逐年递增的趋势。产品动态阿里云阿里云 PolarDB MySQL 新增group by相关参数和模式名称为空时语句的优化处理。阿里云 AnalyticDB MySQL 新增多个函数,多个数据库的外表,同时优化了CBO。阿里云 PolarDB PostgreSQL引擎新增分区上创建索引、新插件的Truncate等功能。阿里云 PolarDB-X 修复DDL、系统函数、分区数等缺陷。阿里云 PolarDB-X 日志节点修复CDC、事务日志和多流数据路由等问题。阿里云 PolarDB O和PG引擎都新增pgtap插件,以及时空方面的能力。阿里云 Redis对离线全量key的分析进行功能优化。阿里云 Redis、Tair新增支持创建按量付费的云盘版实例。阿里云 RDS 新增新版本的性能洞察,该版本基于MySQL的performance_schema能力汇聚SQL信息。阿里云 RDS 计算包支持抵扣ARM架构实例的计算资源,新增内核版本发布。阿里云 MongoDB 4.2版本新增云盘存储,支持ESSD PL1云盘。部分地域新增独享型云盘版规格。阿里云 DAS(据库自治服务)新增新版本的性能洞察,支持RDS和PolarDB。腾讯云腾讯云数据库MySQL新增备份落冷,生成的备份文件进行存储类型转换。腾讯云数据库PostgreSQL新增支持逻辑复制槽故障转移。火山引擎火山引擎 云数据库MySQL新增更新数据库名、账号名和账号密码的创建规则功能。火山引擎 云数据库MySQL 新增支持通过 VPN 进行私网域名的公网解析。火山引擎 云数据库 SQL Server 新增支持变更实例配置、计费和实例规格。火山引擎 DTS GA了MongoDB不同版本的测试,Redsi的支持进入邀测阶段,同时优化了DDL、任务进度查看、链接等问题。火山引擎 云数据库PostgreSQL新增支持只读实例支持弹性公网 IP 功能、监控告警功能、数据备份进度查看。AzureAzure SQL数据库发布Serverless Hyperscale公共版;Azure SQL 数据库 Serverless 和超大规模实例有效结合;Azure PostgreSQL 数据库使用加密密钥对数据进行存储加密服务GA;Azure Databricks 实时的Serverless GA了。AWS[AWS] Amazon RDS for PostgreSQL 现已支持 tcn 扩展,tcn(触发更改通知)是一个函数,针对指定表中的数据更改生成 NOTIFY 事件;[AWS] Amazon OpenSearch Serverless 正式上线;[AWS] RDS for MariaDB 支持实施 SSL/TLS 连接;[AWS] Aurora 支持 PostgreSQL 14.6、13.9、12.13、11.18;[AWS] ElastiCache 支持 Memcached 1.6.17。GCPGCP AlloyDB for PostgreSQL 新增持续备份和恢复预览版本,该功能支持从可配置的时间窗口内的任何时刻恢复数据;GCP Cloud Spanner的区域性终端节点功能已被移至未来版本;GCP 云Bigtable 数据库新增支持Changestream特性,另修复了一些BUG。GCP Spanner 新增自动完成和验证DDL语句功能;GCP Bigtable 修复了元数据和google-gax使用等问题;GCP Bigtable修复了batch endpoint的缺陷,更新了部分依赖关系;GCP Spanner 表大小的统计信息功能GA;GCP DMS新增支持将Oracle载迁移到云数据库 PostgreSQL。华为云华为云 GaussDB(for MySQL)支持重启代理实例,需要先开通读写分离功能。PostgreSQLPostgre SQL 2022年PostgreSQL中国技术大会,将在3月初的杭州举办;PostgreSQL 发布增量物化视图插件 pg_ivm 1.5 发布。DorisDoris Doris 在2月15日发布1.2.2Release,1.2.2将作为1.2的LTS迭代版本。NavicatNavicat支持OceanBase和Redis。starRocksstarRocks 发布V2.5版本,新增支持更丰富的数据格式、查询缓存、物化视图等功能NebulaGraphNebulaGraph 近日,国产分布式图数据库 NebulaGraph发布了企业版 v3.4.0。作者:来自NineData 的工程师。NineData向企业、开发者提供高效、安全的数据库SQL开发、数据库备份、数据复制/迁移/集成、数据对比等功能,是一个SaaS服务开箱即用,可以快速提升企业SQL开发效率,保障企业数据安全。来源:微信公众号:云数据库技术
文章
SQL  ·  Oracle  ·  关系型数据库  ·  MySQL  ·  Serverless  ·  分布式数据库  ·  数据库  ·  PostgreSQL  ·  时序数据库  ·  RDS
2023-02-24
云数据库行业动态【2023年3月版】
本文整理2023年3月份最新数据库厂商、数据库领域的行业动态、以及各家云数据库厂商的产品动态。本文主要整理了数据库领域的最新动态:数据库厂商的重磅事件;各数据库的产品更新。一、重磅事件▋阿里云RDS 三款产品均支持Serverless上个月阿里云发布SQLServer Serverless 公测版本,本周发布RDS PostgreSQL Serverless。至此,从去年4月份以来,历时近一年阿里云RDS三款产品均有 Serverless 版本。All In Serverless 已不再是口号。另外,阿里云 RDS Serverless 已有在金融企业生产环境中落地案例,从相关介绍的文章看,目前Serverless形态的 RDS 主要用于灾备场景。▋OCI MySQL Database Service 推出 Read ReplicaOracle 云上 MySQL 服务发布 Read Replicas with Load Balancer功能。根据文章介绍,用户可以基于已有的MySQL实例,创建只读实例(官方介绍最多可创建18个),从而实现MySQL的读写分离,进而支持更高的系统负载。应用系统既可以连接到Load Balancer上,也可以直接连接到只读副本上。架构图:▋蚂蚁金服首个云原生时序数据库 CeresDB 1.0 正式发布CeresDB 是一款计算存储分离架构的分布式时序数据库,其存储层可以基于 OceanBase KV、OSS 等。以上是官方对CeresDB介绍,时序数据库是一种针对时序数据高度优化的垂直型数据库,在制造业、金融业、社交媒体上都有广泛应用,近年发展迅猛。去年蚂蚁金服出身的时许数据库创业Greptime DB ,获得数百万美元的融资,看来资本和大企业对时许数据库都非常看好。▋阿里云瑶池数据库峰会将在京成功举办3月24日,在北京召开的阿里云瑶池数据库峰会上,阿里云宣布将云原生数据库 PolarDB 和云原生数据仓库 AnalyticDB 打通融合;同时推出全新多模数据库 Lindorm AI 引擎,支持对非结构化数据进行智能分析和处理,从而打造 AIGC(生成式 AI)应用的数据基础设施。阿里云数据库产品事业部负责人李飞飞表示,未来的数据库将只有一种生态,就是云原生数据库,它会提供分布式一体化的能力,而“分库分表”、“集中式”这些概念都将过时。▋OceanBase第一次开发者大会3月25日,首届OceanBase开发者大会在北京举行。 大会发布了OceanBase 4.1版本,公布两大友好工具,升级文档易用性,统一企业版和社区版代码分支,全面呈现了OceanBase打造极致的开发者友好数据库的成果。 过去13年,OceanBase以极强的稳定性、可扩展性和低成本,成为分布式数据库领域的典型实践,并持续加大科研投入,突破技术边界,让分布式技术不断升级的同时越来越好用,研发单机分布式一体化架构,让分布式数据库走向通用;攻坚HTAP能力,让一份数据既能做交易又能做分析,实现低延时、低成本。▋全球企业数据库市场规模将达到 2842 亿美元,年增长率为 12.5%近日,企业数据库市场报告发布的一份新的市场研究报告,该报告称“到 2032 年,企业数据库市场收入预计将达到 2842 亿美元”、“从 2023 年到 2032 年,企业数据库市场的年中增长率可能高达 12.5%”。虽然该报告并没有介绍中国市场,但是国内增长率应该是远超12.5%,在目前国家GDP只有5%左右增长的情况下,数据库行业的这个增长率对广大数据库从业者还是很友好的,至少是几倍大盘速度。▋MongoDB公司公布2023财年业绩2023财年全年总收入为12.84 亿美元,同比增长 47%。其中订阅收入为12.351 亿美元,同比增长 47%;服务收入为4890 万美元,同比增长 54%。全年毛利润为9.347 亿美元,毛利率为 73%,而去年同期为 70%。其中 Atlas 收入同比增长 50%,占第四季度总收入的 65%。截至2023 年 1 月 31 日,拥有超过 40800 名客户。▋内存数据库厂商 DragonflyDB 获得2100万美元融资2023年3月21日,内存数据库初创公司 DragonflyDB Inc 宣布获得 2100万美元融资,同时推出其数据库的最新版本 Dragonfly 1.0,该版本增加了几个新的可靠性和数据管理功能,据称其速度比 Redis 快 25 倍。。目前使用最广泛的内存数据库是开源的 Redis ,它的成功是踏着Memcache过来的,现在从打江山进入守江山角色。▋NineData 发布数据库开发工具界的 ChatGPT2023年最火产品当属AIGC,玖章算术公司旗下的 NineData发布了数据库工具届的ChatGPT。NineData 内置强大的AI生成能力,用户通过自然语言提问,轻松完成库表生成、测试数据构建、数据查询变更及性能优化等常见的数据库开发、数据分析及日常运维工作。NineData 的官网地址:https://www.ninedata.cloud/,提供企业级数据库 SQL 开发工具,数据复制、对比、备份等产品,无需下载,直接使用。▋Amazon Aurora Serverless v2在中国区域上线经在国外发布近2年后,Aurora Serverless v2正式在国内上线,目前支持北京和宁夏两个区域。Aurora Serverless v2 具有自动缩放和支付即用的特点,用户可以根据其实际需求进行容量调整,真正实现了数据库的“按量付费”,据官方介绍是该特性可帮助客户节省高达90%的成本。当前Aurora 支持 MySQL和PostgreSQL 两个版本。▋造车“新势力”遇见数据库“新势力”虽然这是有浓厚PR气息的通告,但正如文中所说,新能源汽车、分布式数据库,两个看似风牛马不相及的事物,确实有共同之处。比如两款产品都有清晰的定位,理想家庭奶爸用车首选,大空间、丰富的配置、可油可电等特性,把中国人用车场景研究的透透的;OceanBase国产分布式数据库的重要产品,在去O背景下,以服务故障自动恢复、数据零丢失、国产自主可控等特性去打用户。从目前两款产品在市场表现看,国货当自强,已不是一句口号了。▋GaussDB数据库社区上线GaussDB 是重要的国产数据库之一,也是墨天轮国产数据库排行榜长期占据在前10 的产品之一。在2019年开源后,广大数据库爱好者可以更深入的倒腾它。此次,华为云发布GaussDB数据库社区,可以看出官方将对这款产品的重视,相信未来GaussDB肯定有更好的发展。▋NineData 发布SQL Dev的企业多人协同功能NineData 最新发布了企业级多人协作能力,SQL 窗口和 SQL 任务分别接入了开发规范和审批流程;同时发布了智能 SQL,提供自然语言转换 SQL 的能力。其中企业协同功能,主要解决权限安全管控、数据库安全变更、高效SQL开发。可以直接登录 NineData 官网(https://www.ninedata.cloud/),申请免费测试数据源使用。二、产品动态▋阿里云云数据库MongoDB 分片集群架构支持添加只读节点;云数据库PolarDB 新增弹性跨机并行查询功能等功能;RDS 新增支持在创建RDS实例时指定实例端口;DAS 新增多个数据库的实例会话的API接口;RDS PostgreSQL 内核新增支持多款插件;RDS MySQL 新增8.0、5.7的高可用版实例升级为集群版功能;云数据库MongoDB金融云华东2(上海)地域新增多个可用区并支持部署多可用区实例;DTS新增全量数据校验支持按表行数进行校验和MySQL系列数据库实例间触发器的同步或迁移任务。▋腾讯云云数据库MySQL优化购买逻辑,支持一键导入配置,筛选规格更加快捷;云数据库SQL Server 支持查询和下载阻塞及死锁事件;云数据PostgreSQL 支持控制台查看备份空间、支持手动备份;TDSQL新增支持实例级独立 IP 地址;DTS新增批量重命名任务名称、目标端增加kafka、数据下云等功能;云数据库MySQL备份周期支持自定义以周和月的维度来进行备份时间设置;云数据库SQLServer新增单节点新增云盘类型和双节点云盘版类型。▋火山引擎DTS新增支持全量一致性迁移、过滤源端数据、查看预检查项详情功能;Redis发布标签管理、创建实例时绑定白名单、修改最大连接数、单节点实例购买、购买相同配置的实例等功能;MySQL新增事件中心、恢复新实例时支持添加标签信息等功能;PolarDB PostgreSQL版增强跨机并行查询能力;云数据库MongoDB在多个架构中新增支持 32 核 64GiB 节点规格;云数据库 SQL Server 版上线。▋Doris2023年3月20日发布1.2.3 Release 版本;▋GreeplumnVMware 3月17日正式发布 Greenplum 7.0 Beta.2;▋Azure数据库迁移服务(经典版)SQL Server场景将于2026年3月15日下架;▋GCP云数据库提供更小规格的只读实例,只读实例不再需要与其主实例相同或更多的CPU和内存限制;云数据库spnnaer支持ARRAY_FILTER ARRAY_TRANSFORM函数;Spanner for PostgreSQL新增支持JSONB数组数据类型;云数据库spanner支持UNRECOGNIZED类型和延迟解码BYTES列;AlloyDB for PostgreSQL 新增支持16个地域;SQL 新增支持通过API或gcloud获取实例详细信息的功能。▋NineData新增智能 SQL 功能,快速生成 SQL 语句,被称为数据库开发工具界的ChatGPT;发布了企业级多人协作能力,新增 SQL 窗口规范预检,SQL 任务全新改版,新增 SQL 规范预检、流程审核功能,新增 SQL 开发规范和审批流程功能。新增单点登录 SSO(Single Sign-On)新增 AWS-美国西部(加利福尼亚北部)地域▋GreatSQL发布8.0.25-17版本,修复InnoDB并行查询bug。▋AWS云数据库MySQL支持106个新的数据库标签;云数据库BigTable优化了数据恢复时,目标集群没有足够的存储时的处理逻辑;云数据库Spanner推出细粒度的访问控制,结合IAM权限和数据库角色等功能;Amazon RDS PostgreSQL新增支持14.7、13.10、12.14和11.19等版本;Amazon DynamoDB 新增支持表删除保护功能;RDS for Oracle新增支持19C和21C。▋TIDB发布小版本6.5.1,新增全栈支持 IPv6 地址、定期清理过期的缓存等功能,修复部分BUG。▋Navicat支持 OceanBase 全线数据库产品。▋华为云云数据库 GaussDB 支持购买时设置读写分离以及新增数据库创建规则。▋GreatSQL万里数据库团队开源数据校验&修复工具gt-checksum。▋Databend数仓平台Databend v1.0 Release 正式发布。▋AntDB亚信发布AntDB企业社区版 V7.2.0,用户可以免费下载使用。▋HudiApache Hudi 发布 0.13.0 版本,新增包括变更数据捕获等功能。*作者:NineData 工程师,致力于让每个人用好数据和云云数据库行业动态持续更新中,每月底准时整理并分享。欢迎数据库领域的技术文章,行业资讯,相关内容可以投递到公众号「云数据库技术」。
文章
SQL  ·  NoSQL  ·  关系型数据库  ·  MySQL  ·  Serverless  ·  分布式数据库  ·  数据库  ·  PostgreSQL  ·  OceanBase  ·  RDS
2023-03-27
“2023金三银四”又来了,关于面试,你需要知道的这些事
  一年一度的“金三银四”又到来了,本以为疫情放开后求职环境会变好一些,没想到反倒是比之前更差了,最近也有许多读者通过各种方式跟博主沟通,询问关于面试的一些问题,刚好趁着这段时间有些空闲,整理了一下去年跳槽的一些经验,希望能够帮助到有需要的朋友。  博主先做下简单的自我介绍: 坐标深圳,22年底跳槽(骑驴找马),在近一个多月时间内面试了:南方电网,格力,顺丰,KLook,维信金科,富德保险、联创杰等20多家公司,收到南方电网,KLook,格力,维信金科等10几家公司Offer(还有几家走到2,3轮面试因为收到心仪offer而没有继续参加后续面试),最后选择了一家岗位匹配度和薪资比较符合个人预期的游戏公司,现在主要负责游戏数据中台开发。  下面是之前收到的一些录取意向,因为Boss沟通信息有时效性,很多录取信息找不回来了,现在很多公司都是要你确定入职后,才会发实际的Offer,在未确定之前,很多都是口头Offer(口头Offer没有法律效应,大家面试时要慎重考虑)。  废话不多说,下面就聊聊个人的面试经验,主要可以分为两部分即:面试准备,面试复盘。  面试准备: 主要是关于简历的编写,投递,面试题目准备,与Hr沟通技巧等相关知识。  面试复盘: 主要是面试过程中的问题发现,经验总结面试准备  主要大纲如下:简历编写  一般简历包含的模块(只是个人看法,仅供参考):个人信息、教育背景、求职意向、专业技能、工作经历、项目经历,自我评价七大模块  注意:涉及到时间的,如工作经历,项目经历,要按照时间逆序写,即先写最近发生的,再写以前发生的,面试官更加关注的是你最近的一个状态  个人信息模块: 简历中需要在个人信息中明显体现:自己的工作年限和联系方式  教育背景模块:  有工作年限的朋友在教育背景处无需编写一大段学习过的课程名称,而是应该简洁地写出自己在工作或者学校中非常突出的特点,比如获得过蓝桥杯XX奖状,国家励志奖学金。  如果是应届生的话,可以抽选几个与岗位要求匹配的相关科目写上即可(尽量写自己成绩比较好的,有些岗位可能要提供成绩证明)  求职意向模块:  薪资和入职时间写【面议】,一般不建议直接写明,给自己预留空间,薪资实际上是根据面试情况浮动的,不要给自己固定死,表现好就适当提高自己的预期  专业技能模块:  技巧1:要具体不要抽象,一定要避免跟大多数网上教程一样写精通XX技术,熟悉XX技术,单纯从文字谁也看不出来你是真是假,反倒可能给后续面试留下隐患。 推荐的方式:结合STAR原则,简要介绍,如: 反例:精通/熟悉SpringCloud技术栈 正例:熟悉SpringCloud技术栈的使用,曾使用它们独立完成XX项目基础搭建,开发,并定位开发中使用产生的一系列问题。 这样两者一对比下来,显然下面的描述面试更加具体,专业技能描写要尽量避免抽象,这样也方便后续面试官进行具体的面试问题询问。   技巧2:突出优点, 如在工作获得过年度优秀个人(最好写明有多少个开发者),个人有开源项目,博客等(前提是有价值的,不是个人随记那种价值意义不大的) 正例: 有个人开源项目,获得XXStar,地址XXX。 有个人博客,在主流XX平台有XX粉丝数,是XX平台签约作者。   技巧3:烂熟于心, 对于自己编写的技能一定是比较熟悉的,不要看网上教程就乱写, 面试官大多数是会根据这个进行询问,自己不熟悉的写上去反倒是扣分项。  工作经历模块:  这个模块一定要有(如果是应届生就写实习经历), 许多人可能会觉的是不是跟【项目经历模块】功能重叠了,其实不是,面试官通过这个模块可以一眼看到你的整个工作生涯,而项目经历一般细节篇幅较长,很难让人直观对面试者整个工作经历有一个全面的认知  包含信息: 公司名称,部门,时间,在公司的职责,业绩正例: XX公司,IT部门(20xx.08-23xx.09) 职责:作为XX组长/开发者,负责XX架构技术,技术文档编写,CodeReview等工作 业绩:负责的XX项目如期交付,达到XX目标,为公司带来了XX  项目经验模块:  注意,简历编写的目的是为了让面试官看懂,应尽量具体不要抽象,用行业通用专业术语而不是大量描述词,写完后反过来问自己或者给朋友参考,通过第三视角观察:如果是别人提交这样的简历,自己是否能够看懂。  编写一般包含以下信息:公司名称(项目始末时间)、项目名称,项目简介、技术栈、个人职责,项目难点,工作成绩  示例: XXX公司 IT部 XX项目(项目起始时间-项目结束时间) 公司简介(可选,如果是比较知名或者和应聘岗位有契合度的建议写):XX公司,国内做XX项目Topx,主营XX业务 项目简介:XX项目是为了解决XX问题,通过XX技术/算法实时计算/处理数据,然后进行XX数据分析,并可视化输出XX指定各类型统计信息,给XX业务提供数据支撑。 技术描述:JDK8,SpringCloud,Spring,Mybatis,PostgreSQL,Redis,Promethues,Grafana等 项目职责: 1、负责与产品进行需求沟通,探讨技术可行性 2、负责项目的整体架构设计,设计文档编写 3、负责XX核心模块开发,项目进度跟踪,阶段性复盘 负责功能(可以结合STAR法则描述具体负责的开发模块): 1、使用XXX技术进行实时数据接入,通过XX算法/方式对数据进行XX维度统计,输出XXX数据,完成XXX可视化展示。 项目难点(后面面试官基本都会问你是如何解决的,所以对于写上去的一定要能够自洽): 1、项目的架构设计,服务拆分和技术选型 2、保证数据接入服务的可靠性,高可用性和大数据量下XX数据实时处理的稳定性 工作业绩: 1、XX项目对实时数据提供了可靠,准确的数据处理(准确率达到了xx),为XX系统可视化展示各类型指标,运营进行各决策制定提供了数据支撑,给公司XX带来了XX帮助。   自我评价  还是一句话,要具体不要抽象,不要写努力,刻苦,不懂可以学之类的,要写有证据证明你的观点,否则不如不写。  正例:自驱动力强,对自己职业规划有清晰认识,在业务时间会自己钻研新技术,并通过编写博客和开源项目XXX进行输出等(这样就跟你前面举例的专业技能相呼应)。简历编写时常见的疑问  简历是不是一定要一页?  个人觉的视情况而定。不知道这个结论是从哪里冒出来的,确实看到有一些面试官是如此说,但是,对于IT专业,一页简历很难将你的个人特点展现出来。  除非你是行业内非常nb的 ,直接将名字或者个人开源项目放到简历就能让人脑海浮现的人物外,不然,还是不用太纠结这个问题。所以,简历的篇幅尽量简洁的前提下,不要超过4页最好,打印的时候单页打印,方便面试官查看。  一般写多少个项目经验好呢?  考虑到前面的模块篇幅+面试官时间,阅读体验等因素,一般推荐编写2-3个项目经历即可,因此要挑选最近,最具有代表性的项目。  如何对自己的简历熟记于心?  最好的方式就是以面试官的视角看自己的简历,然后提出问题,用专门的文档整理出来,不但可以加深自己的记忆,还可以在下一次跳槽时使用。 比如:你在简历的专业技能模块中提到自己熟练使用SpringCloud技术栈,并且独立进行项目搭建和开发。 那么可能面试官视角可能就会问你,你使用这个技术搭建过什么项目,为什么要选择这个技术栈,使用XX技术栈代替不行?在进行技术栈选型时有做过什么调研,你们最终选用的是哪个版本,搭建过程有没有出现过什么问题等等。 考虑到面试官可能会提问这一系列的问题,因此你必须提前做好应对准备,不要回答说没有参考,然后自己定的 ,即使真实情况确实如此,你也要说出选型的依据,不然面试官就会对你产生质疑,毕竟如果以后让你负责一个项目,你没有依据的选型,后续项目如果出现问题,你是否考虑过应对方案,没有责任心的人面试官的印象分总是很低的。 简历投递平台和规则  有哪些简历投递平台?  Boss:岗位和专业程度相比最高,回复较快,建议作为主要投递网站  拉勾:岗位数量一般,回复较慢,使用体验是在Boss外最好的  猎聘:猎头较多,一般你活跃后他们打电话给你推荐岗位,在应用上岗位数量一般  前程无忧51Job:岗位数据量较少,旧岗位比较多,可以用于参考,回复较少  智联招聘:外包居多,备用参考  脉脉:实际岗位少,贩卖焦虑和广告居多,不过猎头也不少  投递简历时的注意事项?   简历命名:姓名-岗位名称-工作年限,方便HR望名知意   简历统一以PDF方式投递,防止在某些平台查看时出现布局错乱   投递分先后顺序,不要上来就海投: 自己有意向的岗位先收藏,先尝试面试一些意向比较低的岗位,这样一来可以让自己先熟悉面试流程和进入面试状态,熟悉了之后再投递有意向的岗位把握性更大与HR沟通术语   要自定义打招呼术语:不要使用招聘平台默认招呼语,突出自己的特点。 试想一下,HR每天要查看成百上千个面试者的招呼,如果你的工作经历和学历不是非常突出,你如何吸引HR的注意力,打招呼将是你可以掌握主动权的一个时机,千篇一律很容易被埋没。示例: HR您好,刚刚仔细阅读了您发布的岗位要求,发现与我较匹配,希望能与您进一步沟通,下面是简单的个人介绍: 先前就职于XXX,有X年工作经验,XX年毕业于XX大学-XX专业(学信网可查),曾主导过XX等项目,负责核心的XX架构设计和开发,对线上问题定位,调优有个人的理解。 在校/工作期间获得过XX奖,成绩一直处于前TopX,有XXX开源项目/博客,访问地址如下: 对贵司的该岗位很感兴趣,希望能够得到您的回复,谢谢。   最好能够添加HR微信(就说可能有时候不注意看消息,如果方便的话加个微信): 有时候很多岗位信息,很容易遗漏,通过微信沟通可以更加方便,许多面试平台的沟通信息存储都是有时间限制的,过了时间就找不回来了。  再者说,量变引起质变,当你添加到一定量之后,以后的面试都可以直接从微信中找了HR了,熟悉的肯定更有优势一些。面试预约的细节  如何合理预约面试时间&地点?  在接受面试之前一定要提前做好计划,因为很多时候面试邀请都是并行的,你接收了这个面试时,之前的面试可能就有回复准备进行下一阶段面试,如果不合理安排很容易出现冲突或者面试频繁的现象,频繁的面试是非常耗费精力,而且会影响面试的成功机率。  合理的面试时间:一天一面或者三天两面,要给自己预留出面试复盘的时间,不要以为紧凑的面试能够提高成功机率。  同时,在接受面试之前,尽量了解一整个面试的流程,如果有多轮面试,可以沟通看看第一轮面试是否可以远程(这样可以减少出行机率,在自己熟悉的地方面试氛围也会更加轻松些)  如何合理协调多个面试?  可以使用敬业签,番茄Todo等app记录每日面试的安排,合理调整时间面试复盘  复盘的目的在于发现面试中自己未注意/查觉的问题,常言道:旁观者清, 复盘就是通过第三视角来观察自己,更能帮助我们全面了解自己,增强面试自信,下面是一些复盘的技巧。  一、面试流程复盘: 可以借助思维导图等工具将对应的面试流程归纳总结起来(如果后续你收到多个offer时,也有助帮助你挑选,毕竟很多时候第一感觉就是对的),内容如下:  具体内容有:时间,地点,公司,岗位,面试时间,面试官信息,是否拿到offer/大概什么时候知道面试情况,面试感受  面试中了解到的公司,岗位信息(大概需要面试几轮,可能的时间,为了最大程度在多个offer出现时,横/纵向对比拿到心仪的offer)  二、面试问题复盘  面试中涉及到的知识点(适当的情况下可以录音方便复盘)  主要的内容有:面试中回答准确的问题,面试中回答比较模糊的问题,面试中未回答出来的问题  三、面试中自己提问问题的回复  针对提问面试官的问题:尽可能通过面试官的的回答了解到自己的面试情况,不是直接问,通过委婉的方式,如:面试官您觉的本轮面试我哪些方面可能需要更加深入或者加强呢?  针对HR的问题:主要了解薪资,福利,公积金等问题,特别是五险一金的档次和比例(但这些问题一般是留到最后一轮,如果前面几轮的话,可以主要询问关于公司的文化方面的问题)  这个是建议每次面试都要记录下来:随着面试场次增多,很多时候不同公司的回复可能容易造成记忆混乱,对于自己权衡挑选Offer影响不好。面试题目  因为面试题目较多的原因,现在还有部分在答案梳理中,所以此处会给出部分面试的题目,后续会同步更新答案,想要第一时间获取最新面试题目,欢迎关注博主在GitHub开源的面试项目:IT知识小屋  简历可能提问问题大纲:  面试题目大纲:  部分题目列举:  如何进行自我介绍?  您是如何进行项目架构设计的、技术选型的呢?  您说您对线上问题处理、性能调优和线程并发有自己的理解,请问能简单介绍一下具体是什么?体现在哪里呢?  您们是如何进行项目复盘的呢?复盘后会输出什么?  您离职的原因是因为什么?  你的预期薪资是多少?能谈谈上一份薪资是多少?预期薪资的来由是什么?  你大概什么时候能够到岗?  在编程中使用了哪些代码规范?如何进行复用?  你开发的项目中都是使用SpringCloud这一套框架进行开发的?能简单谈谈你们搭建的流程?  简单说说SpringBoot、SpringMVC、Spring Framework的区别  Spring熟悉?能简单说说其中使用了哪些设计模式?  有阅读过Spring源码?能够谈谈你阅读过哪些Spring源码?  既然你研究过Spring源码,能够谈谈Spring实例加载的一个过程?它是如何解决循环依赖的呢?  你们项目中使用到了SpringCloud框架的哪些组件,能够简单介绍是如何使用它们的?  能简单介绍下JVM的布局是怎样的呢?java是如何进行垃圾回收的?  在项目中有通过Gc日志回收问题分析的经验?你进行问题排查的流程是什么呢?有使用到哪些工具呢?  在实际的开发中,遇到过OOM问题?你是如何排查的?  有进行过JVM调优?有哪些方法进行JVM调优呢?  JVM有哪些核心指标,合理范围是多少?  你有具体的调优案例?可以讲讲具体过程?  你都用过哪些数据库呢?它们之间有什么区别?为什么项目中要选型这个数据库?  对于SQL你有什么方式进行优化呢?  如果走了索引,查询还是慢,该如何处理?  你们的接口、数据库设计是如何进行评审的?评审后会输出什么东西呢?  Redis你熟悉?有哪些场景下使用到了Redis?Redis是单线程为什么性能这么高?  你有使用过哪些消息队列?什么场景下使用?如何保证消息不丢失?如何防止重复消费?  你了解多线程?在什么场景下使用了多线程,有遇到什么问题?  你了解分布式事务、分布式锁、分布式缓存?  项目中有使用到单元测试?如何编写的?  设计模式你了解哪些,谈谈它们在JDK源码中的使用?  如何避免重复入库问题  什么是接口幂等性?  如何避免库存超卖问题  谈谈你对集合的认识/常用到集合的特点  HashMap和ConcurrentHashMap的区别  Synchronized和Lock的区别  介绍下线程池的各个参数和作用  为什么线程池中队列要使用阻塞队列  线程池何时超时?  如果数据量非常大,怎么保证接收接口的稳定性  使用Redis进行库存处理,如果存在一个订单多个商品,怎么保证Redis执行时的原子性,涉及到多个商品库存扣减。  redis如何查询数据预热,如何进行数据预热,怎么知道哪些数据需要预热?  在数据量非常大的时候,使用redis存在瓶颈,有没有更好方案?  Redis中的大Key和大Value如何处理?  介绍下最近项目的一个技术栈以及项目上有亮点的地方整体谈谈  谈谈项目的业务场景具体是什么?  队列中堆积大量数据如何快速写到数据库  列式数据库跟传统数据库或者es,查询方面索引有什么区别?  多个服务之间有会话,如何保证会话的一致性?  介绍一下你觉得自己做得比较好的项目,拿出来分享下。  在项目中,遇到过哪些比较难处理的技术或者业务问题  你们项目中有没有遇到过如并发度等方面问题的难点  在提供技术方案时,有没有选项报告和性能测试?  你做的项目中,你觉的哪些是你考虑得有欠缺的,后面有时间后,重新回顾是觉的可以优化的地方。  你们系统是使用什么垃圾回收器  JVM你有做过哪些调优?  元空间内存占满后你有分析它是怎么一个使用情况?  Spring中一个对象注入有几种方式,它们有什么区别  AOP是如何实现的?  类加载器如何避免类的重复加载即有多个实现如何选择加载哪个类?  为什么双重锁单例要使用volatile关键字  你觉的自己的开发效率如何?  个人对于工作强度要求如何?  对于想SpringCloud这一套,开发久了微服务也来越多,之间的功能会产生一些交集,想微服务之间的职责划分,一般都有哪些原则。  有做过一个服务/领域的划分,指定一些领域内部的一些能力,是否有做过?  平常对于自己架构能力的提升,你有哪些渠道、研究过哪些网站,系统?  微服务里面中,分布式事务、分布式ID、微服务发现和配置,RPC通信,分布式锁,哪些比较熟,有读过一些源代码?  雪花算法有什么问题?在某些情况下会产生重复的ID  Spring循环依赖了解?  声明式事务失效的场景有哪些  为什么HashMap不一上来就树化?  树化阈值为什么是8  栈帧中都包含哪些信息  lambda中调用局部变量,局部变量为什么定义成final  过滤器和拦截器的区别  Http通信和Socket通信在使用角度来说主要的区别体现在哪些方面?  Http从1.0、1.1到2.0到3.0中间有许多改动,这些具体的改动是哪些?  JVM在8、11、13的垃圾回收期算法的调整,有具体了解过?  MySQL主从节点部署,涉及到全量和增量同步,它们的大概流程是怎么样的?  Redis一般在使用的时候会考虑哪些问题?  Http协议,客户端操作服务端时需要考虑哪些问题呢?  如何对一个接口开启跨域访问?  有几种方法可以对接口进行限流?  由于文章篇幅限制,部分面试题目的介绍就简单到此,想要第一时间获取最新面试题目,欢迎关注博主在GitHub开源的面试项目:IT知识小屋心里话  如果大家有任何疑问,欢迎关注/私信博主,博主会在看到消息第一时间进行回复。   最后,祝愿大家能够找到符合心愿的Offer。相关推荐  面试知识开源项目:IT知识小屋,面试真题、面试避坑、996公司、算法、电子书籍等内容干货第一时间分享  实用工具开源项目:轮子之王,拿来即用的常用开发工具(Github,Gitee累计280+star)  代码自动生成开源项目:IT脚手架,一键生成项目基础结构(通用依赖)+数据表实体+controller+service等层级代码
文章
设计模式  ·  NoSQL  ·  算法  ·  Java  ·  测试技术  ·  Redis  ·  数据库  ·  索引  ·  微服务  ·  Spring
2023-03-27
【数据库设计与实现】第6章:并发控制
并发控制设计原则事务的并发控制首先要保证并发执行的正确性,满足可序列化要求,即并发执行的结果和某种串行执行的结果是一致的,然后在满足正确性的前提下尽可能地获得最高的并发度。当然在某些业务场景下,可以适当牺牲部分正确性(即接受某些异常),从而获得更高的并发性能。并发控制大体分为悲观算法和乐观算法,为了尽可能深入了解各种算法的优缺点,本章在Oracle、MySQL的基础上增加了PostgreSQL、CockroachDB和VoltDB。Oracle、MySQL、PostgreSQL采用了悲观控制策略,同时通过MVCC进一步提高并发性,而PostgreSQL在此基础上实现了Serializable Snapshot Isolation。CockroachDB完全采用了乐观控制,是乐观控制的开源和商业化实现。VoltDB在并发控制策略上做了突破新创新,舍弃了悲观控制和乐观控制,采用了全串行化的执行策略。在设计和实现并发控制时,有如下几点需要考虑:并发控制算法的用户友好度和正确性,与ANSI SQL隔离级别的匹配度;并发控制算法的效率;并发控制算法的死锁策略;Oracle设计原理事务Oracle数据库支持ANSI SQL定义的Read Committed、Serializable隔离级别以及一个自定义的Read Only隔离级别,且默认Read Committed隔离级别。不支持ANSI SQL定义的Read Uncommitted和Repeatable Read隔离级别,主要基于如下考虑:Read Uncommitted:脏读主要有两个作用,其一是读不加锁,降低读操作的成本,以提高并发度。其二是可以读到最新的未提交数据。Oracle采用了多版本设计,读语句天然不会对记录加锁,同时读取最新脏数据的应用场景也比较少,基于上述考虑,Oracle不支持Read Uncommitted隔离级别;Repeatable Read:Repeatable Read和Serializable的主要区别是读断言锁是长期的还是短期的(详细情况请回顾“事务”章节的“Locking”小节),而Oracle在记录上没有设计读锁,所以两者没有区别,因此Oracle只提供了Serializable隔离级别,不支持Repeatable Read隔离级别;Oracle在事务的并发控制上综合运用了锁机制和多版本机制(MVCC),在锁机制中仅在记录上设计了行级写锁,没有设计行级读锁,读导致的相关异常通过多版本机制和用户加锁来解决(select ... for update)。在Read Committed隔离级别下,记录被修改时会在该记录上加上写锁且一直持续到事务提交或回滚,而读取记录则通过多版本机制读取已经提交的最新记录(多版本一致性读的原理,请回顾“数据前像与回滚”章节的“一致性读”小节)。为了能够读到最新的已提交数据,在每次查询语句开始前会获取当前的最新scn,该scn之前的最新已提交记录都可以被读取。这样既可以保证读到最新的已提交事务的数据,又保证了语句执行过程中的一致性。在Serializable隔离级别下,记录被修改时会在该记录上加上写锁且一直持续到事务提交或回滚。多版本机制和Read Committed下有所不同,Read Committed在每条查询语句开始之前都会获取当前最新的scn,而Serializable在事务开始前获取当前最新的scn。整个事务运行期间保持该scn不变,从而解决了不可重复读和幻读异常。然而只有写加锁,读不加锁,从而存在如下异常:Lost Update:r1[x=100]r2[x=100]w2[x=120]c2w1[x=130]c1是“事务”章节中的示例,事务T2对x增加的20将丢失。Oracle是通过报错来解决Lost Update异常的。当事务修改某条记录时,发现该记录的当前提交scn大于本事务的开始scn,说明该记录在本事务运行期间被其它事务修改并提交过,此时已经无法达成可序列化,报“ORA-08177: can’t serialize access for this transaction”错误,本次事务执行失败;Write Skew:r1[x=50]r1[y=50]r2[x=50]r2[y=50]w1[y=-40]w2[x=-40]c1c2是“事务”章节中的示例,需满足x+y为正数的约束,从单个事务T1和T2来看都是满足该约束的,但执行成功后不再满足该约束;解决上述异常的方式是使用select ... for update,即应用通过语句要求数据库在读操作时在记录上加锁(原则上此处加读锁即可,但Oracle没有记录级读锁,所以此处加的仍然是记录级写锁,一定程度上影响并发性),从而解决上述两种异常。实际上,在Read Committed隔离级别下,应该也可以通过select ... for update强制读语句加上写锁以达成可序列化的效果,缺点是降低了并发性。在Read Only隔离级别下,多版本机制和在Serializable隔离级别下是一样的,不同的是Read Only隔离级别下不允许执行DML写语句。Read Only对于分析型等只读场景是非常有意义的,既可以读到一致性的数据,同时又不阻塞正常的写事务。记录锁图6.2-1 记录级锁示意图 在上节我们知道Oracle只有记录级写锁,没有记录级读锁,即完全是通过记录级写锁达成事务的并发控制。图6.2-1给出了Oracle记录级写锁的示意图,在每行记录的头部都有一个字节的lb字段,记录本条记录被ITL中的哪个事务给锁定了。如果某条记录的lb指向ITL中的事务A,且该事务处于活跃态,那么该记录就被事务A锁住了,即事务A在该记录上加了记录级写锁。此处引出了Oracle的一个重要的设计理念,锁就是数据的一部分(占用1个字节),存在于block(data block、index block)中。这样的设计有如下优势:锁资源轻量且无限大:不需要在独立的内存区域中设计锁结构,锁就在数据中,随着block在内存和持久设备中换入换出,锁资源无限大,所以Oracle不需要设计多层次的锁粒度,并根据锁记录的数目在不同锁粒度间升级;易于传输:锁是记录的一部分,可以随着block进行传输,这一点在Oracle RAC中体现得非常明显,当block在数据库实例间传输时锁信息自然也就传输过去了;表锁当我们在做DDL语句时需要对操作的表加表锁,从而防止其他用户同时对该表做DDL操作。在更改表结构时还需要防止此时有其它事务正在更改本表中的记录,为此需要逐行检查本表的记录上是否有锁。如果表中的记录非常多,逐行检查表上记录是否有锁非常消耗资源,可能还涉及block的读入与写出,导致性能进一步恶化。为了解决此问题,可以在表上引入新的锁类型,以表明其所属的行上有锁,这就是意向锁。意向锁指如果对某个节点加意向锁,则说明该节点的下层节点正在被加锁。对任一节点加锁,必须先对上层节点加意向锁。对应到表和记录,对表中的任何记录加记录锁前,必须先对该表加意向锁,然后再对该记录加记录锁。这样DDL对表加锁时,不需要再逐行检查表中每条记录上的锁标志了,直接判断表上是否有意向锁即可,系统效率得以提升。意向锁有如下锁类型:意向共享锁(Intent Share Lock,IS锁):如果对记录加S锁,需要先对表加IS锁,以表示该表的记录准备(意向)加S锁;意向排它锁(Intent Exclusive Lock,IX锁):如果对记录加X锁,需要先对表加IX锁,以表示该表的记录准备(意向)加X锁;表上有基本的S锁和X锁,意向锁又引入了IS锁和IX锁,这样可以组合出新的S+IS、S+IX、X+IS、X+IX四种锁。但实际上只有S+IX有意义,其它三种组合都没有使锁的强度得以增强(即:S+IS=S,X+IS=X,X+IX=X,等于指强度相等)。这样我们引入了一种新的锁类型:共享意向排它锁(Shared Intent Exclusive Lock,SIX锁)。事务对某表加SIX锁,表示该事务要读取整个表(所以要对该表加S锁),同时会更新表中的部分记录(所以要对该表加IX锁)。意向锁封锁的策略:加锁:申请封锁时,应按照自上而下的次序进行;解锁:释放锁时,应按照自下而上的次序进行;可见,数据库表上的锁类型有S、X、IS、IX、SIX五种。Oracle的表锁分别有S、X、RS、RX、SRX,与S、X、IS、IX、SIX一一对应。需要注意的是Oracle在记录上只提供X锁,所以与RS(通过select ... for update语句获得)对应的记录锁也是X锁(该行实际上海没有被修改),这与理论上的IS锁有所区别的。表6.2-1 表锁相容矩阵 SXRSRXSRXSYNYNNXNNNNNRSYNYYYRXNNYYNSRXNNYNN表6.2-2 语句与表锁的对应关系锁锁语句场景NULL1select ... from table_nameRS2select ... from table_name for update(9.2.0.5之前版本)lock table table_name in row share modeRX3insert into table_nameupdate table_namedelete from table_nameselect ... from table_name for update(9.2.0.5及后继版本)lock table table_name in row exclusive modeS4create index ...lock table table_name in share mode外键上没有索引SRX5lock table table_name in share row exclusive mode外键约束定义成on   delete cascadeX6alter table ...drop table ...drop index ...truncate table ...lock table table_name in exclusive mode表6.2-1为Oracle表锁的相容矩阵,Y表示相容,N表示不相容,需要阻塞等待。表6.2-2给出了语句与表锁之间的对应关系示例,锁给出了字符和数值两种表达方式。当Oracle执行select ... for update、insert、update、delete等DML语句时,会在操作的表上自动加上表级RX锁。当执行alter table、drop table等DDL语句时,会在操作的表上自动加上表级X锁。另一方面,应用程序或者操作人员也可以通过lock table语句指定需要获得某种类型的表锁。最后再介绍一下Oracle的breakable parse locks(分析锁)。Oracle会在share pool中缓存分析和优化过的SQL语句和PL/SQL程序,这样再次执行这些相同的SQL或PL/SQL程序时,不必再进行解析、编译、生成执行计划,直接使用缓存的执行计划。缓存的执行计划对所涉数据库表是有依赖的,即当表结构发生变更时,缓存的所涉的执行计划需要及时失效。分析锁就是为了解决及时通知问题的,当缓存执行计划时,会在所涉数据库对象上加上分析锁。该分析锁会一直持有,直到对应的执行计划失效。分析锁不会产生任何阻塞,当表结构发生变更时,会及时通知对缓存的相关执行计划失效。Enqueue在上面章节我们知道Oracle有记录级X锁,有多种模式的表锁。通过这些锁在保证正确性的前提下,提供了最大的事务并发度。但从实现层面来看,我们还有两个关键问题尚未解决:问题1:如何高效地知道某个数据库对象上已经加了锁,加了什么模式的锁;问题2:当发生冲突时如何对事务排队,持有者释放锁时如何及时唤醒阻塞事务并保证公平性;表6.2-3 部分常见enqueue type大类类型场景User enqueuesTXAllocating an ITL entry in order to begin a transaction;Lock held by a transaction to allow other transactions to wait for it;Lock held on a particular row by a transaction to prevent other transactions from modifying it;Lock held on an index during a split to prevent other operations on it;TMSynchronizes accesses to an object;ULLock used by user applications(通过DBMS_LOCK.REQUEST加锁);System enqueuesSTSynchronizes space management activities in dictionary-managed tablespace;CICoordinates cross-instance function invocations;TTSerializes DDL operations on tablespace;USLock held to perform DDL on the undo segment;CFSynchronizes accesses to the controlfile;TCLock held to guarantee uniqueness of a tablespace checkpoint;Lock of setup of a unqiue tablespace checkpoint in null mode;ROCoordinates fast object reuse;Coordinates flushing of multiple object;PSParallel execution server process reservation and synchronization;首先来看问题1,因为锁是加在数据库对象上的,这些对象可以是表、文件、表空间、并行执行的从属进程、重做线程等等,我们将这些对象统一称为资源。为此,Oracle在SGA中设计了enqueue resource数组,数组中的每个元素代表一个资源,数组的总大小可通过参数_enqueue_resources设置(可通过x$ksqrs和v$resources查看enqueue resources)。Enqueue resource中的每个元素就是一个ksqrs结构,ksqrs结构中的关键成员有:enqueue type:标识锁类型(或称为资源类型),Oracle内部的锁类型非常丰富,表6.2-3给出了部分常见的锁类型。各种internal locks都会在system enqueue中对应一种类型,记录锁和表锁属于user enqueue,分别对应于TX和TM类型;enqueue identification(ID1、ID2):用于标识具体的资源,例如当enqueue type等于TM时,identification存放具体哪个表(ID1等于表的object id),当enqueue type等于TX时,identification存放具体哪个事务(ID1高位的2个字节存放undo segment id,ID1低位的2个字节存放transaction table id,ID2存放wrap);link:双向指针,用于将相同状态的ksqrs结构链接在一起,例如处于空闲状态或者在同一个hash桶中;owners:指向双向链表的头部和尾部,该双向链表存放所有已经持有本资源的锁信息;converters:指向双向链表的头部和尾部,该双向链表存放所有已经持有本资源并等待升级到锁强度更高的锁信息;waiters:指向双向链表的头部和尾部,该双向链表存放所有已经等待本资源的锁信息;图6.2-2 Enqueue Free List与Hash Table 如图6.2-2所示,正常情况下单个ksqrs结构未被使用前通过link双向指针串在一起,组成ksqrs的free list。当需要申请一个资源时,从free list上摘一个ksqrs结构下来,填写enqueue type和enqueue identification,并根据enqueue type和enqueue identification计算hash值,从而算出本krqrs结构归属的hash bucket,并将该ksqrs结构加入到算出的hash bucket中的hash chains中(hash chain中的ksqrs也是通过link双向指针链接在一起的)。hash算法越优秀,hash冲突越小,hash chain的长度越短。可见,处于使用状态的ksqrs是通过hash进行管理的,这样可以快速定位某个资源是否已经加锁(enqueue type和enqueue identification可以唯一标识某个特定的资源)。Hash table的长度(即bucket的数量)可以通过参数_enqueue_hash设置。由于多个用户会并发访问enqueue hash table,所以需要对其进行并发访问保护。系统会申请若干个enqueue hash chains latch(parent latch与child latch,详细情况请回顾“同步与互斥”章节),每个enqueue hash chains latch保护一段bucket(实际上是round-roubin方式)以及这些bucket后面的hash chain。Enqueue hash chains latch的数量由参数_enqueue_hash_chain_latches设置,默认值为cpu_count。假设表t1的object id为1234,现在需要对t1加表锁,那么首先需要申请TM资源。申请资源的大致过程如下:step1:查找enqueue hash table中是否已经有表t1的资源(表资源的类型为TM),对TM、1234(id1=object id)、0(id2=0)计算hash值,从而得到对应的bucket(此处假设为bucket12);step2:申请获得bucket12对应的enqueue hash chains latch;step3:成功获得latch后,查找bucket12的hash chain,看是否已经有表t1的TM资源,如果有则表示不需要创建新的t1资源,释放latch直接退出,否则进入下一步;step4:从ksqrs free list上摘下一个ksqrs结构,将enqueue type设置为TM,将enqueue identification设置为id1=1234,id2=0,然后将该ksqrs结构添加到bucket12的hash chain中;step5:至此完成表t1资源的创建,释放latch,并退出;在上述步骤4中,需要从ksqrs free list上摘下一个空闲的ksqrs结构。Ksqrs free list本身也需要同步与互斥保护,在高并发场景下会有大量频繁的申请与释放,此处就会成为瓶颈。为此,Oracle采用了Lazy策略,即释放资源后对应的ksqrs结构并不立刻归还到ksqrs free list中,而是保留一部分空闲ksqrs结构在chain chain上,这样后继可以直接复用,从而提升性能。至此,我们已经完成enqueue resource的介绍。但enqueue resources只是一个容器,只能给出问题1的部分答案,即解决了如何快速找到某个数据对象(资源)的问题,还需要回答问题1提出的锁模式和问题2。为此,我们需要引入另外一个结构“锁”,“锁”是加在资源上的,即附着在某个ksqrs结构上的。图6.2-3 KSQRS结构及锁对应关系 如图6.2-3所示,每个资源都对应一个ksqrs结构,加在该资源上的所有锁都通过ksqrs结构进行排队:Owners:持有者,即该资源的拥有者,每个锁对应一个拥有者,拥有者不会被阻塞,当有多个拥有者时这些拥有者的锁一定是相容的;Converters:转换者,由拥有者转换而来,表示已经拥有低强度的锁,但在申请变更为更高强度锁时和其它拥有者的锁不相容;Waiters:等待者,和拥有者的锁不相容;当拥有者释放锁时,首先唤醒转换者,即将转换者变更为新的拥有者。当拥有者和转换者都为空时,依次唤醒等待者。如果等待者中有多个相邻的锁是相容的,可以同时唤醒成为拥有者,即如果锁4和锁5是相容的,可以同时成为拥有者。有了上述概念之后,我们首先来看表锁的互斥排队过程。表锁对表对象加锁,所以容纳表锁的ksqrs类型为TM。每个表锁是一个ktqdm结构,申请表锁时首先从ktqdm free list中申请一个ktqdm结构(ktqdm free list由dml allocation latch保护),然后将ktqdm结构附着到对应表的ksqrs结构上。ktqdm结构中关键成员有:sid:锁对应的会话(session);lmode:当前已经持有的锁模式;request:当前正在请求的锁模式;ctime:锁已经持有的时长或者等待的时长;表6.2-4 表锁阻塞时序示例TimeSession1(S1)Session2(S2)Session3(S3)Session4(S4)T1lock table t1 in row exclusive mode;Lock table t1 in row exclusive mode;  T2 Lock table t1 in share row exclusive mode;  T3  Lock table t1 in exclusive mode; T4   Lock table t1 in row exclusive mode;T5Commit;   T6 Commit;  T7  Commit; T8   Commit;图6.2-4 表锁阻塞队列示例 如表6.2-4所示,该表展示了一个针对表t1的时序示例,4个会话(s1、s2、s3、s4)同时对表t1加表锁。图6.2-4给出了T4时刻,表t1上的各表锁之间的阻塞情况。详细过程如下:因为都是对表t1加锁,所以相关的ktqdm结构都附着在同一个ksqrs结构上,ksqrs的类型为TM,id1=t1(实际上是表t1的object_id),表示资源为表t1;T1时刻:s1和s2两个会话同时对表t1加row exclusive锁,这两个锁是相容的,所以都在持有者队列中,通过ktqdm结构中的link链成双向链表,lmode=3表示持有的锁模式为row exclusive;T2时刻:会话s2尝试对表t1加SRX(share row exclusive),即将锁的强度从RX升级为SRX。由于s2的SRX与s1的RX是不相容的,所以s2的ktqdm结构从持有者链表中迁移到转换者链表中,lmode=3表示s2已经持有RX锁,request=5表示s2正在申请SRX锁,此时会话s2阻塞;T3、T4时刻:会话s3和s4分别对表t1加X和RX锁,这两个锁要么和持有者的锁不相容,要么和转换者的锁不相容,所以按照申请的顺序加入到等待者链表中,lmode=0表示尚未持有任何锁,request=6/3表示正在申请的锁模式,此时会话s3和s4阻塞;T5时刻:会话s1提交并释放锁,此时s2从转换者链表迁移到持有者链表中,更新(sid=s2, lmode=5, request=0)表示锁升级为SRX,此时会话s2开始运行;T6时刻:会话s2提交并释放锁,此时持有者和转换者链表都为空,从等待者链表中将s3迁移到持有者链表中,并更新(sid=s3, lmode=6, request=0),由于会话s4的RX和会话s3的X不相容,所以会话s4仍然留在等待者链表中,此时会话s3运行,会话s4继续阻塞;T7时刻:会话s3提交并释放锁,会话s4从等待者链表迁移到持有者链表中,开始运行;至此,我们介绍了表锁的整个运行过程,回答了表锁相关的问题1和问题2,即通过ksqrs结构定位到并发阻塞的资源,通过ksqrs的持有者、转换者、等待者三个链表结合ktqdm结构完成排队、阻塞和唤醒。从中我们可以发现如下关键点:一个会话对同一个表不管加多少次表锁,只会占用一个ktqdm结构;转换者链表中的元素优先级高于等待者链表中的元素,因为转换者中的元素已经持有锁,需要让它们尽快运行以尽快释放锁;从等待者链表向持有者链表迁移时,是按照入链的顺序迁移的,即按照申请的顺序迁移的,体现了FIFO的公平性;下面我们开始介绍记录锁。对于问题1,记录锁是很容易解决的,每条记录的头部有lb标志,且记录锁只有X模式,所以记录锁的重点是解决问题2,即对有记录锁冲突的事务如何进行排队。记录锁的排队机制和表锁的排队机制是类似的,主要区别如下:仍然通过ksqrs enqueue排队,但ksqrs的type为TX,id1和id2等于事务id,即每个事务开启第一次写时会申请一个标识本事的TX类型的ksqrs结构,后继因为和本事务记录锁发生冲突的会话全部附着在该ksqrs结构上;不需要像表锁为每个锁申请一个kstqdm,只需要为每个冲突的事务申请一个ktcxb结构;表6.2-5 记录锁阻塞时序示例TimeSession1(S1)Session2(S2)T1--transaction id=10.1.145Update t1 set c1=1;100 rows updated. T2 --transaction id=10.2.23Update t2 set c1=2;100 rows updated.T3 Update t1 set c1=2 where c2=3;T4Commit; T5 1 rows updated.T6 Commit;图6.2-5 记录锁阻塞队列示例 如表6.2-5所示,该表展示了两个事务(10.1.145和10.2.23)同时修改表t1和表t2中记录的情况,因为同时修改t1中的记录而发生记录锁冲突。图6.2-5展示了在T3时刻TX排队情况。详细过程如下:T1时刻:会话s1开启一个写事务(第一条语句就是更新表t1的全表记录),申请一个ksqrs结构,类型为TX,id1=655361(10*65536+1),id2=145,并加入到ksqrs enqueue hash表中。同时申请一个ktcxb结构,设置sid=s1,lmode=6(记录锁只能是X模式),request=0,并将标识本事务的ktxcb附着在该ksqrs的持有者链表上;T2时刻:会话s2开启一个写事务(第一条语句就是更新表t2的全表记录),申请一个ksqrs结构,类型为TX,id1=655362(10*65536+2),id2=23,并加入到ksqrs enqueue hash表中。同时申请一个ktcxb结构,设置sid=s2,lmode=6,request=0,并将标识本事务的ktxcb附着在该ksqrs的持有者链表上;T3时刻:会话s2更新t1表的单条记录,通过检查该记录的记录头,发现该记录已经被事务10.1.145锁住,申请一个ktcxb结构,设置sid=s2,lmode=0,request=6,并将标识本会话的ktcxb附着在事务10.1.145的ksqrs的等待者链表上,以等待事务10.1.145释放该记录锁,此时会话s2阻塞;T4时刻:事务10.1.145提交,唤醒ksqrs(TX,id1=655361,id2=145)中等待者链表中的所有会话,然后释放ksqrs结构(TX,id1=655361,id2=145)和附着在该ksqrs上的ktcxb结构,此时s2会话激活,继续执行对表t1的记录更新;至此,我们介绍了记录锁的整个运作过程,回答了记录锁相关的问题1和问题2,即在记录头部发现记录冲突,在通过ksqrs的持有者、等待者链表结合ktcxb完成排队、阻塞和激活。从中我们还可以发现如下关键点:每个写事务都会申请一个ksqrs(类型为TX)结构,并持有到事务结束,可见事务本身一种资源;每个写事务都会申请一个或两个ktxcb结构,可见ktcxb结构的数量和修改的记录数无关,只可冲突的事务数相关;所有和事务A有记录冲突的事务都会申请一个ktxcb结构,并将这些ktxcb结构附着在事务A的ksqrs的等待者链表中;TX事务锁除了用于记录锁的排队之外,还用于ITL Entry Shortage时事务的排队。当事务修改block中的数据时,首先需要在该block中占用一个ITL Entry。如果ITL Entry已经被用满,且无法动态扩展ITL时,本事务就需要阻塞等待。此时为本事务申请一个ktxcb结构,然后在本block的ITL中随机选择一个活跃事务,将ktxcb结构附着在该活跃事务的ksqrs结构的等待者链表上。这样当该活跃事务提交时,其占用的ITL Entry就会空出来,唤醒本事务复用该ITL Entry。实际上,Oracle不仅仅将enqueue机制应用于表锁和记录锁,而是将enqueue机制通用化,当系统资源冲突或者不足时都采用enqueue机制进行排队。enqueue机制通用化时,都是通过ksqrs进行排队,只是enqueue type不同。同时不同的资源,用于排队的结构也不同,ktqdm用于表锁,ktcxb用于事务锁,ksqeq、kdnssf、ktatrfil、ktatrfsl、ktatl、ktstusc、ktstusg、ktstuss等等都是用于各种internal locks。不过不管是表锁、事务锁,还是各种internal locks,最终都是通过_enqueue_locks参数设置总lock的数量。enqueue采用数组结构,同时又通过双向指针对数组中的结构进行分类管理。对于大小和属性相同的对象,Oracle一般采用数组这种数据结构进行管理。数组是采用分段方式进行分配和管理的,即Oracle初始只会分配一个容纳固定数量数据单元的内存块,然后在运行过程中动态分配更多的内存块。例如,x$ksqrs数组初始会申请一个较大的内存块,后继不够时再每次申请可容纳32个ksqrs结构的内存块,以此进行动态扩容。死锁Oracle的latch是通过对latch设置level属性在事前规避死锁,而lock的申请顺序和用户语句的执行时序强相关,无法通过事前规定lock的顺序来规避。因此,Oracle采用了事后检测的方法来解决死锁。当会话因为锁等待达到3秒后会醒来,这时会检查等待关系。如果存在循环等待表示存在死锁,否则进行下一个3秒周期的等待。如果检查发现存在死锁,就会触发ORA-60 deadlock detected错误,让应用参与决策。由于是事后超时检查死锁,所以一般是等待时间长的事务先报错。MySQL设计原理事务MySQL数据库支持ANSI SQL定义的Read Uncommitted、Read Committed、Repeatable Read、Serializable四种隔离级别,默认隔离级别为Repeatable Read。MySQL采用的是索引组织表,表中的记录时按照索引键或主键存放的,这就为加断言锁提供了基础。实际上,MySQL就是通过间隙锁锁住记录之间的间隙,从而达到断言锁的目的,防止幻读。各隔离级别下,MySQL的并发控制机制如下:Read Uncommitted:不使用一致性读,允许读取未提交事务的记录,因此会有脏读。只有更改记录或者用户强制lock read才会加锁,且只对记录加record_lock,不会间隙加锁;Read Committed:使用一致性读,且总是读取最新提交的快照数据,允许不可重复读和幻读,在外键检查时对间隙加锁,其它情况只对记录加锁;Repeatable Read:使用一致性读,且在同一个事务中读取的总是相同的历史快照数据,在更改记录或者用户强制lock read时对记录和间隙加锁,这样避免不可重读和幻读(在某些情况下可以只对记录加锁,如唯一索引等);Serializable:不使用一致性读,所有更改和读取操作都会加锁,加锁机制和可重复读一致;可见,MySQL的并发控制机制与“事务”章节介绍的Locking理论是最接近的,同时在Read Committed、Repeatable Read隔离级别下采用了一致性读机制(详细情况请参加“前像数据与回滚”章节),读不加锁,从而最大化地提高并发度。当然在Read Committed、Repeatable Read隔离级别下也可以通过lock read(select ... lock in share mode加共享锁,select ... for update加排它锁)主动对记录加锁,从而在较低隔离级别下也可以解决lost update、write skew等问题。记录锁表6.3-1 记录锁相容矩阵(行为已加锁类型,列为待加锁类型) LOCK_S_GAPLOCK_S_REC_NOT_GAPLOCK_S_ORDINARYLOCK_S_INSERT_INTENTIONLOCK_X_GAPLOCK_X_REC_NOT_GAPLOCK_X_ORDINARYLOCK_X_INSERT_INTENTIONLOCK_S_GAPYYYYYYYYLOCK_S_REC_NOT_GAPYYYYYNNYLOCK_S_ORDINARYYYYYYNNYLOCK_S_INSERT_INTENTIONYYYYNYNYLOCK_X_GAPYYYYYYYYLOCK_X_REC_NOT_GAPYNNYYNNYLOCK_X_ORDINARYYNNYYNNYLOCK_X_INSERT_INTENTIONNYNYNYNY记录锁类型包括共享锁(LOCK_S)和排它锁(LOCK_X)两种类型。MySQL支持对间隙加锁,所以有如下不同的锁算法:LOCK_GAP:间隙锁,仅对间隙加锁,锁住前一条记录和本条记录之间的间隙,但不包括本条记录和前一条记录本身;LOCK_REC_NOT_GAP:记录锁,仅锁住本条记录;LOCK_ORDINARY:Next_Key锁,是LOCK_GAP和LOCK_REC_NOT_GAP的组合,锁住本条记录以及本条记录和前一条记录之间的间隙,但不包括前一条记录;LOCK_INSERT_INTENTION:插入意向锁是一种特殊的间隙锁类型,又称为插入意向间隙锁(insertion intention gap lock),这种锁在插入操作执行前产生。假设已经存在两个索引值4和7,两个事务分别插入记录5和6,每个事务在插入数据前都能在(4, 7)中获得一个插入意向间隙锁,并且由于这两个事务插入的记录不相等而不会互相阻塞。但是,如果间隙(4, 7)之前已经被其它事务加上间隙锁,插入意向间隙锁就会被阻塞,从而防止前事务幻读;可见,MySQL支持两种锁类型,四种锁算法,这样共计可以组合出八种不同的锁,具体相容关系如表6.3-1所示,并从中可以发现如下规律:不管是哪种锁算法,共享锁与共享锁之间都是相容的,即LOCK_S_*和LOCK_S_*是相容的;不管已经持有的锁是哪种类型和算法,待加的LOCK_S_GAP和LOCK_X_GAP都是相容的,即GAP锁(不含插入意向锁)和所有已经持有的锁都是相容的,因为GAP锁主要用于防止将来其它事务的插入操作(避免幻读);LOCK_S_REC_NOT_GAP、LOCK_S_ORDINARY、LOCK_X_REC_NOT_GAP、LOCK_X_ORDINARY之间的不相容主要发生在记录本身的共享与排它、排它与排它的不相容;LOCK_S_INSERT_INTENTION和LOCK_X_INSERT_INTENTION表示即将进行插入操作,所以不相容性主要发生在GAP类的锁上,包括LOCK_S_GAP、LOCK_X_GAP、LOCK_S_ORDINARY和LOCK_X_ORDINARY;表6.3-2 lock_t结构域类型含义trxtrx_t本lock_t归属的事务trx_locksUT_LIST_NODE_T(lock_t)一个事务可能有多个lock_t结构,trx_locks用于将事务的多个lock_t结构链成链表,便于管理type_modeulint组合标志位:0-3bits:0 LOCK_IS、1 LOCK_IX、2 LOCK_S、3 LOCK_X、4   LOCK_AUTO_INC;4bit:LOCK_TABLE   表锁;5bit:LOCK_REC 记录锁;7bit:LOCK_WAIT   本锁处于阻塞等待状态;8bit:LOCK_GAP;9bit:LOCK_REC_NOT_GAP;10bit:LOCK_INSERT_INTENTION;hashhash_node_t用于构建lock_t结构组成的hash表,方便查找indexdict_index_t记录的索引un_memeberlock_rec_t或者lock_table_t具体的表锁结构或记录锁结构lock_bitmapbyte(var)锁位图图6.3-1 记录锁与记录之间的映射关系 和Oracle不同,MySQL是以独立的锁结构lock_t来管理锁信息的。最便捷的方式是为每个事务的每个记录锁申请独立的锁结构,但这样会引入数量庞大的锁结构,严重消耗内存资源,为此不得不采用多粒度锁机制,并进行复杂的锁升级。MySQL在速度和资源之间做了平衡,以每个事务处理的page为单位申请lock_t结构,即如果同一个事务对同一个page上多条记录加相同类型的锁,那么只需要申请一个lock_t结构。下面首先来看lock_t结构中最重要的lock_rec_t和lock_bitmap。如图6.3-1所示:lock_rec_t:对应于一个page,space和page no用于标识针对具体哪个page,nbits用于表达变长变量lock_bitmap的长度,lock_bitmap的字节数等于1+(nbits/8);lock_bitmap:变长,和page中的记录数强相关,MySQL每条记录的ROW HEADER结构中有一个REC_NEW_HEAP_NO(详细情况请参见“空间管理与数据布局”章节),用于对page内每条记录生成唯一的编号。这样lock_bitmap中的每个bit位对应于page中的一条记录,bit位的位置就对应于记录的REC_NEW_HEAP_NO,该bit位为1就表示对应的记录上有锁;可见,MySQL是按照page为单位组织锁结构的。优点是节约了内存资源,不需要引入复杂的锁升级机制。缺点是判断某条记录上是否有锁的效率相对较低,首先找到该page相关的所有lock_t结构(事务、锁类型和算法不同,同一个page会有多个lock_t),遍历这些lock_t结构,并根据记录的REC_NEW_HEAP_NO检查每一个lock_t结构中的lock_bitmap,以核实该记录上是否有锁。除了lock_rec_t和lock_bitmap之外,lock_t结构中的详细情况如表6.3-2所示,其中重要的成员还有:trx:指向本lock_t归属的事务,由此可得到对应的事务结构;trx_locks:双向链表,同一个事务可能申请多个lock_t结构,通过该指针将同一个事务的lock_t链接在一起;type_mode:锁的状态、类型以及算法等信息;hash:用于构建hash链表,MySQL会组建锁的hash表,方便以page为单位找到对应的lock_t结构;了解了锁的基本结构后,下面来看MySQL是如何组织lock_t的。MySQL中主要有两种情况查询锁:情况1:事务需要知道本事务已经持有了哪些锁,阻塞在哪个锁上;情况2:事务在扫描或修改某个page中的记录时,需要知道该记录上是否有锁,以及锁的类型和算法是什么;首先来看情况1,每个事务都会维护一个trx_lock_t结构,该结构包含如下关键成员:wait_lock:一个指向lock_t结构的指针,指向本事务当前等待的锁结构;trx_locks:类型为UT_LIST_BASE_NODE(lock_t),指向链表的指针,结合每个lock_t中的trx_locks将属于本事务的所有lock_t结构链接在一起,构成一个链表;wait_started:锁等待的开始时间;lock_heap:lock_t结构是动态生成的,维护本事务所有动态锁的内存;可见,通过wait_lock和trx_locks,事务将归属于本事务的所有lock管理起来。一个事务只可能阻塞等待在一个锁上,所以wait_lock只是一个指针。下面来看情况2,全局变量lock_sys会维护一个大的hash表(rec_hash)和因为锁等待而阻塞的线程(waiting_threads)。Rec_hash实际上就是按照space和page no对lock_t进行hash管理的大hash表。其中关键的成员有:array:hash表的桶数组;n_cells:hash表的桶数量,即桶数组的长度;sync_obj:互斥量数组,用于保护并发访问hash表;这样根据space和page no算出具体的hash值,从而得到对应page 所在的桶,即array数组的下标。然后遍历该桶对应的哈希链,即由lock_t结构组成的链表,比较lock_t.lock_rec_t结构中的space和page no,从而找到对应的page。由于存在多个事务对同一个page的不同记录加锁,所以同一个page会有多个lock_t结构,需要遍历这些结构。对于每个lock_t结构,比较记录REC_NEW_HEAP_NO对应的位图,从而判断是否有锁。至于锁的类型和算法,则根据lock_t中的type_mode来判断。图6.3-2 lock_t锁布局 如图6.3-2所示,每个事务维护一个trx_lock_t结构,通过该结构总额trx_locks和wait_lock以及每个lock_t的trx_locks指针,将属于某个事务的所有锁结构链接在一起。同时维护一张rec_hash表,将hash值相同的lock_t结构通过hash指针链接在一起,这就可以查询特定page的锁情况。多个用户线程会并发访问hash表,需要同步机制进行并发保护。考虑到并发性,会有多个mutexes(sync_obj),每个mutexes保护一段bucket数组以及后面的哈希链表,提高并发性。表锁表6.3-3 表锁之间的相容关系 LOCK_ISLOCK_IXLOCK_SLOCK_XLOCK_AILOCK_ISYYYNYLOCK_IXYYNNYLOCK_SYNYNNLOCK_XNNNNNLOCK_AIYYNNN表6.3-4 表锁之间的强度关系(Y表示行的强度大于列,N表示列的强度大于行) LOCK_ISLOCK_IXLOCK_SLOCK_XLOCK_AILOCK_ISYNNNNLOCK_IXYYNNNLOCK_SYNYNNLOCK_XYYYYYLOCK_AINNNNYMySQL的表锁和Oracle比较类似,也是通过多粒度锁解决效率问题,支持如下锁类型:LOCK_IS:意向共享锁;LOCK_IX:意向排它锁;LOCK_S:共享锁;LOCK_X:排它锁;LOCK_AUTO_INC:自增长锁,含有自增长列的表才会加该类型的锁;表锁之间的相容性如表6.3-3所示,之间的强度关系如表6.3-4所示。在实现上,表锁也是一个lock_t结构。和记录锁不同的是lock_t中的um_member不同,um_member是一个union结构。当lock_t为记录锁时,um_member为lock_rec_t结构。当lock_t为表锁时,um_member为lock_table_t结构。Lock_table_t结构中的关键成员有:table:指向dict_table_t类型的指针,表示本表锁归属于哪个表;locks:组成lock_t的链表,用于将归属于同一个表的所有lock_t结构链接在一起;图6.3-3 表锁布局 如图6.3-3所示,每个表在缓存中对应一个字典结构dict_table_t。Dict_table_t结构中的locks以及各个lock_t中的locks(实际上是um_member.lock_table_t.locks)将归属于同一个表的所有lock_t结构管理起来。Dict_table_t结构中的autoinc_lock将该表LOCK_AUTO_INC自增长锁独立出来,避免事务频繁地创建和释放该结构。表锁和记录锁都是lock_t结构,不同的是表锁不需位图结构,直接通过type_mode标识具体的锁类型。当然不管是表锁还是记录锁,从事务的角度来看,都是通过trx_locks和wait_lock进行管理的。聚集索引和辅助索引MySQL是索引组织表,索引又分为聚集索引和辅助索引,其加锁原则为:通过主键进行加锁的场景,仅对聚集索引加锁;通过辅助索引进行加锁的场景,先对辅助索引加锁,再对聚集索引加锁;在加锁的过程中,加锁策略和隔离级别、扫描类型、索引的唯一性等强相关。总的来说,规则如下:如果没有任何索引,需要全表扫描(或者覆盖索引扫描),所有记录全部加锁。RC与RR、Serialiable的区别是只在记录上加锁,不在间隙上加锁。当然MySQL出于性能的目的,对于不满足更改条件的记录会调用unlock_row提前释放锁,一定程度上违反了2PL;如果是非唯一索引,在[index first key, index last key)范围内加记录锁,如果是RR或者Serialiable隔离级别,间隙也需要加锁;如果是唯一索引,在[index first key, index last key)范围内加记录锁,如果是等值查询,即使是RR或者Serialiable隔离级别也不需要加间隙锁,因为唯一性已经保障不会出现幻读;隐式锁与显式锁虽然MySQL以page为粒度组织lock_t结构,以计算换空间(无法直接判断某行记录上是否有锁,需要遍历lock_t中的bitmap),一定程度上节约了内存资源。然而lock_t的量级仍然是事务数*page数*锁类型,锁资源的压力仍然非常大。为了节约锁资源,MySQL实现了一种称为隐式锁的延迟加锁机制。其核心思想是锁是非常消耗资源的,能不加锁就不加锁,只有在发生冲突时再加锁。显式锁是明确的锁,对应于lock_t对象,而隐式锁只是逻辑上的“锁”,没有lock_t对象,需要通过其它规则间接地发现该记录上有锁。如何判断某条记录上是否有隐式锁?对于聚集索引来说比较简单,每条记录上都有该记录的事务id(trx_id),如果该事务id对应的事务仍然是活跃的,那么该记录上有隐式锁,否则没有隐式锁。辅助索引比较复杂,每个page上都有一个PAGE_MAX_TRX_ID(该域在PAGE HEADER结构中,详细情况请参考“空间管理与数据布局”章节),用于表示更新本page的最后一个事务id。如果PAGE_MAX_TRX_ID比最小活跃事务id还要小,说明该page上的所有记录都没有隐式锁,否则需要找到对应的主键记录进行更加复杂的判断。图6.3-4 辅助索引与聚集索引的逻辑关系 如图6.3-4所示,现在需要判断辅助索引current_index_rec上是否有隐式索引,需要通过对应的聚集索引来判断。聚集索引结合undo日志可以构造出历史版本,包括聚集索引的历史版本和辅助索引的历史版本。有了这些历史版本之后,辅助索引上的隐式索引判断规则如下:current_trx不是活跃事务(通过current_cluster_rec中的隐藏事务id获得),current_index_rec上没有隐式锁;current_cluster_rec没有历史记录,表示本条记录是current_trx插入的,所以current_index_rec上有隐式锁;current_index_rec=history1_index_rec,但current_index_rec和history1_index_rec的delete flag不同,表示current_index_rec正在被current_trx删除,所以current_index_rec上有隐式锁;current_index_rec=history1_index_rec,且current_index_rec和history1_index_rec的delete flag相同,修改current_index_rec的事务既可能已经提交了,也有可能没有提交。如果current_trx!=history1_trx表示current_trx没有修改current_index_rec,所以没有隐式锁,否则要进一步判断history2;current_index_rec!=history1_index_rec,且current_index_rec和history1_index_rec的delete flag都为0,表示current_trx修改了current_index_rec,所以current_index_rec上有隐式锁;current_index_rec!=history1_index_rec,且current_index_rec的delete flag为1,修改current_index_rec的事务既可能已经提交了,也有可能没有提交。如果current_trx!=history1_trx表示current_trx没有修改current_index_rec,所以没有隐式锁,否则要进一步判断history2;通过上述规则,MySQL就可以通过比较和计算发现辅助索引上是否有隐式锁。在后继事务的加锁过程中,如果发现某条记录有隐式锁,那么以前事务的名义为该记录申请加显式锁。可见,在隐式锁机制下,只有发生锁冲突时才会加锁,为系统节约了大量资源:如果在原事务提交或回滚前,没有其它事务访问对应的记录,实际上所有的隐式锁都不会被转换为显式锁;如果在原事务提交或回滚前,其它事务访问该记录的某些辅助索引,只有被访问到的辅助索引才会被转换为显式锁,其它辅助索引上隐式锁仍然不会被转换;由于隐式锁只能通过规则和事务id进行判断,无法获取锁模式和锁类型等信息,所以隐式锁有如下限制:隐式锁针对的是记录锁,不可能是间隙或Next-Key类型;INSERT操作只加隐式锁,不加显式锁(包括聚集索引);UPDATE、DELETE在查询时,对查询用到的辅助索引和聚集索引加显式锁,其它二级索引使用隐式锁;记录锁的维护MySQL是以page为单位维护lock_t对象的,而page会随着数据的变化而变化,产生分裂、合并等现象。因此,lock_t对象也要随着page的分裂、合并而分裂、合并。分裂、合并的机制和原理基本一致,而分裂又分为左分裂和右分裂,其原理也是一致的,所以下面以右分裂为例来讲述记录锁的分裂维护。假设某page中的记录为R1、R2、R3、R4、R5、R6、R7,那么可以锁定的范围有:(infimum,R1](R1,R2](R2,R3](R3,R4](R4,R5](R5,R6](R6,R7](R7,supremum)此时page需要进行右分裂,分裂点为记录R4,即记录R4~R7需要迁移到一个新的page中。那么需要生成一个新的lock_t对象(right):left lock_t:(infimum,R1](R1,R2](R2,R3](R3,supremum);right lock_t:(infimum,R4](R4,R5](R5,R6](R6,R7](R7,supremum);Right lock_t的supremum继承于原lock_t对象的supremum,同时left lock_t对象的supremum和right lock_t的infimum需要根据分裂前(R3, R4]进行设置,即(R3,supremum)和(infimum,R4]要等效于分裂前的(R3,R4]。死锁MySQL对死锁采用了主动检测机制,其检测原理就是有向循环图。记录锁的hash组织方式为有向循环图的检测提供了充分必要条件。当某事务在加锁时因为锁冲突要等待,就开始进行深度优先的递归遍历,检测是否存在有向循环图。如果存在循环就表示有死锁,寻找一个undo量最小的事务进行回滚。PostgreSQL设计原理事务PostgreSQL数据库支持ANSI SQL定义的Read Uncommitted、Read Committed、Repeatable Read和Serializable四种隔离级别,默认隔离级别为Read Committed。在各隔离级别下PostgreSQL的并发控制机制分别如下:Read Uncommitted:实际上就是Read Committed;Read Committed:使用一致性读,且总是读取最新提交的快照数据,允许不可重复读和幻读;Repeatable Read:实际上是snapshot isolation,使用一致性读,且在同一个事务中读取的总是相同的历史快照数据,允许写倾斜(write skew);Serializable:实际上是serializable snapshot isolation,使用一致性读,并在snapshot isolation基础上加入SIREAD锁和RW-Conflicts机制,解决写倾斜异常,保证可序列化;可见,PostgreSQL的并发控制机制与Oracle和MySQL有很大的不同,通过snapshot isolation和serializable snapshot isolation机制实现Repeatable Read和Serializable。同时PostgreSQL也采用了锁机制,解决表级冲突以及记录级的写冲突,也支持通过在select语句上指定for update或者for share强制加记录排它或者共享锁。因此,PostgreSQL综合运用了乐观控制和悲观控制方法,以达到最优的并发控制效率。记录锁图6.4-1 Tuple结构 正常情况下PostgreSQL直接在记录上设置标志位就可以完成对记录加记录锁,不需要申请独立的内存锁结构,从而提高内存资源利用率和锁效率。如图6.4-1所示,每条记录都有一个HeadTupleHeaderData(详细情况请参考“数据前像与回滚”章节),该头部包含了如下重要信息:x_min:insert本条记录的事务id;x_max:delete/update本条记录的事务id;t_infomask:大量的组合标志位,通过综合这些标志完成记录锁的设置和判断,具体有HEAP_XMAX_KEYSHR_LOCK、HEAP_XMAX_EXCL_LOCK、HEAP_XMAX_LOCK_ONLY、HEAP_XMAX_COMMITTED、HEAP_XMAX_INVALID、HEAP_XMIN_COMMITTED、HEAP_XMIN_INVALID;图6.4-2 记录判断伪代码FOR each row that will be updated by this UPDATEWHILE TRUEIF (row1 is being updated) THENWAIT for the termination of the transaction that update row1IF (status of the terminated transaction is COMMITTED)AND (this transaction is REPEATABLE READ or SERIALIZABLE) THENABORT this transaction /*first-update-win*/ELSEGOTO step (2)END IFELSE IF(row1 has been updated by another concurrent transaction) THENIF (this transaction is READ COMMITTED) THENUPDATE row1ELSEABORT this transaction /*first-update-win */END IFELSEUPDATE row1 /*row1 is not yet modified or has been updated by a terminated transaction */END IFEND WHILEEND FOR了解了锁记录的标志位之后,我们以update语句为例来看PostgreSQL是如何基于锁进行并发控制的。如图6.4-2所示,判断过程要点如下:Step3:如果记录正在被更新,证明记录上有排它锁,有写写冲突,需要阻塞等待;Step5:当前事务被唤醒后,如果对方事务已经提交且隔离级别为Repeatable Read或者Serielizable,表示对方事务已经修改了当前记录,可能会引起Lost Update异常,当前事务必须强制退出,否则跳转到第2步,重新对本记录进行判断;Step11:如果记录已经被更新,更新该记录的事务已经提交,且该事务与当前事务是并发事务(即当前事务启动时该事务尚未提交)。如果当前事务的隔离级别为Read Committed直接修改该记录,否则强制退出当前事务以防止Lost Update异常;Step12:没有任何冲突,直接修改记录;可见,通过记录上的标志位即可判断出是否有冲突。同时PostgreSQL也支持通过select语句指定for update或者for share提前设置标志,解决频繁强制事务退出的问题。当然上述机制仍然存在一个问题,当存在冲突时,如何有效地对阻塞事务进行排队,这些就需要显式地申请记录锁,详细情况请参考后面的“表锁与记录锁”章节。表锁与记录锁表6.4-1 语句与表锁模式的对应关系模式名称模式id语句场景NoLock0 AccessShare1select RowShare2select for update/ for shareRowExclusive3insert/ update/ deleteShareUpdateExclusive4vaccum(non-full), analyze, create index concurrentlyShare5create index(without concurrently)ShareRowExclusive6任何postgresql命令不会自动获得这种锁Exclsuvie7任何postgresql命令不会自动获得这种锁AccessExclusice8alter table, drop table, vaccum full, unqualified lock table表6.4-2 表锁模式相容矩阵(行为已加锁类型,列为待加锁类型) AccessShareRowShareRowExclusiveShareUpdateExclusiveShareShareRowExclusiveExclusiveAccessExclusiveAccessShareYYYYYYNNRowShareYYYYYYNNRowExclusiveYYYYNNNNShareUpdateExclusiveYYYNNNNNShare      NNShareRowExclusiveYYNN NNNExclusiveYYNNNNNNAccessExclusiveYNNNNNNN和Oracle、MySQL一样,PostgreSQL出于效率考虑表锁也采用了多粒度机制,表锁的模式和相容矩阵如表6.4-1和6.4-2所示,不同的是PostgreSQL的VACCUM机制非常厚重,所以在表锁中需要引入相关的锁模式。在实现层面,不管是表锁还是显式的记录锁,都采用类似的机制,相关的结构分别为LOCKTAG、LOCK、PROCLOCKTAG、PROCLOCK、PGPROC、LOCKLOCKTAG、LOCALLOCK。需要注意的是,记录锁和表锁不同,记录锁只有共享锁和排它锁两种模式。表6.4-3 LOCKTAG结构域长度含义locktag_field14锁对象标识符locktag_field24锁对象标识符locktag_field34锁对象标识符locktag_field42锁对象标识符locktag_type1锁对象类型:LOCKTAG_RELATION:对表加锁,DB OID+RELOID;LOCKTAG_RELATION_EXTEND:对表加锁;LOCKTAG_PAGE:对page加锁,DB OID+RELOID+PageNumber;LOCKTAG_TUPLE:对记录加锁,DB OID+RELOID+PageNumber +OffsetNumber;LOCKTAG_TRANSACTION:TransactionId;LOCKTAG_VIRTUALTRASNACTIONID:VirtualTransactionId;LOCKTAG_SPECULATIVE_TOKEN:TransactionId;LOCKTAG_OBJECT:DB OID + CLASS OID + OBJECT OID + SUBID;LOCKTAG_USERLOCK;LOCKTAG_ADVISORY;locktag_lockmethodid1锁方法id:DEFAULT_LOCKMETHOD;USER_LOCKMETHOD;LOCKTAG用于标识某个具体被锁定的资源对象,locktag_type和locktag_lockmethodid分别用于标识锁定对象的类型和方法。例如,当locktag_type等于LOCKTAG_TUPLE时,表示锁定一条记录,即记录锁,此时locktag_field1等库对象ID,locktag_field2等于表对象ID,locktag_field3等于PageNumber,表示哪个Page,locktag_field4等于OffsetNumber,表示page内记录的偏移。可见,通过4个locktag_field就可以唯一确定一条记录。当然有时不需要设置所有的locktag_field,例如,当locktag_type等于LOCKTAG_TRANSACTION时只需要将locktag_field1设置为xid。图6.4-3 LOCK结构及与PGPROC、PROCLOCK间的关系 LOCK对象表示一个具体的锁对象,例如一个记录锁就是一个LOCK对象,一个表锁也是一个LOCK对象。如图6.4-3所示,LOCK对象详细描述了某个对象资源上的锁信息,具体情况如下:tag:类型为LOCKTAG,唯一地标识被锁定的某个资源对象;grantMask:类型为LOCKMASK,实际上就4个字节,通过bitmap标识已经在该资源对象上加了哪些锁模式,例如,如果第1个bit位设置为1表示已经加上AccessShare锁。通过1<<LockMode可以标识加上多个锁模式;waitMask:类型同grantMask,grantMask表示已经加上的锁模式,而waitMask表示正在等待的锁模式;procLocks:对tag资源对象加锁的进程列表,指向PROCLOCK对象,并通过PROCLOCK对象中的locklink指针将所有和本LOCK对象相关的PROCLOCK对象链接在一起;waitProcs:当锁模式不相容时,相关进程就需要阻塞等待,waitProc指向等待的PGPROC对象,并通过PGPROC对象的links指针将所有阻塞在本LOCK对象的PGPROC对象链接在一起;Requested、nRequested:本LOCK对象上各种锁模式被请求的次数,总次数,MAX_LOCKMODES为当前系统支持的锁模式数量;granted、nGranted:本LOCK对象上各种锁模式已经被授予的次数,总次数;图6.4-4 PGPROC结构 通过LOCK对象及其哈希表可以从资源的角度找到任何锁对象,从而确定该资源上的锁情况,这是第一个维度。然而我们还需要从事务或者进程的角度查看锁的情况,这是第二个维度。在进入第二个维度之前,我们首先来看PGPROC结构。PostgreSQL是多进程设计,每个后台进程在共享内存中都有一个PGPROC对象。如图6.4-4所示,PGPROC对象中与锁强相关的信息如下:links:和LOCK对象中的waitProcs指针相对应,用于将阻塞等待在同一个LOCK对象上的PGPROC链成一个链表;waitLock:指向本进程正在阻塞等待的LOCK对象;waitProcLock:指向本进程正在阻塞等待的PROCLOCK对象;waitLockMode:本进程阻塞等待的锁模式;heldLocks:本进程已经持有的锁模式;myProcLocks:本进程拥有的所有PROCLOCK对象,通过分区数组以及PROCLOCK中的procLink指针,将所有属于本进程的PROCLOCK对象链接在一起;图6.4-5 资源对象与进程之间的关系 表6.4-4 PROCLOCK结构域类型含义tagPROCLOCKTAGPROCLOCK对象标识符holdMaskLOCKMASK当前已经持有的锁模式releaseMaskLOCKMASK可以释放的锁模式lockLinkSHM_QUEUE用于将归属于同一个LOCK对象的所有PROCLOCK链接在一起procLinkSHM_QUEUE用于将归属于同一个PGPROC进程的所有PROCLOCK链接在一起表6.4-5 PROCLOCKTAG结构域类型含义myLockLOCK*指向LOCK对象的指针myProcPGPROC*指向PGPROC对象的指针LOCK对象描述了某个具体资源对象的锁情况,PGPROC对象描述了某个具体进程的锁情况。如图6.4-5所示,某个资源可以被多个进程加锁,某个进程也可以对多个资源加锁,所以LOCK对象和PGPROC对象时多对多的关系。PostgreSQL设计了PROCLOCK对象以维护LOCK对象和PGPROC对象之间的对应关系。每个PROCLOCK对象代表一个LOCK对象和一个PGPROC对象的对应关系。详细情况如表6.4-4和6.4-5所示,其中的关键信息如下:tag:唯一确定一个LOCK对象和PGPROC对象的对应关系;holdMask:该进程在该对象上已经持有的锁模式;releaseMask:该进程在该对象上可以被释放的锁模式;lockLink和procLink:分别按照Lock对象维度和PGPROC对象维度将相关的LOCKPROC对象链接在一起;表6.4-6 LOCALLOCK结构域类型含义tagLOCALLOCKTAGLOCALLOCK对象标识符lockLOCK*指向共享内存中对应的LOCK对象proclockPROCLOCK*指向共享内存中对应的PROCLOCK对象hashcodeuint32LOCKTAG hash值的拷贝nLocksint64该锁被本进程持有的总次数numLockOwnersint相关的lock   owner个数maxLockOwnersintlockOwners数组的大小lockOwnersLOCKLOCALOWNER*动态申请的lock   owner数组表6.4-7 LOCALLOCKTAG结构域类型含义lockLOCKTAG标识对应的LOCK对象modeLOCKMODE锁模式LOCK、LOCKPROC、PGPROC等对象都存放在共享内存中,运行时都访问共享内存,同时还要考虑互斥,代价比较高。为此,PostgreSQL的每个后台进程在本地维护了LOCALLOCK对象,更新LOCK、LOCKPROC、PGPROC等对象时同时更新LOCALLOCK对象。这样在访问锁时,如果LOCALLOCK对象已经满足要求,就可以不用访问共享内存,从而提高效率。例如,对同一个锁多次加锁或者释放只属于某个资源的锁。死锁对于死锁,PostgreSQL采用了事前预防和事后检测相结合的方式,具体包括:当进程加锁冲突时,就会进入等待队列。如果在队列中已有其它进程请求本进程已经持有的锁,为了避免死锁,可以将本进程插入到该进程的前面;当释放锁时,会尝试唤醒等待队列中的进程。如果某进程请求的锁与该进程前序进程的锁不相容,那么该进程不会被唤醒;通过上述方式,在尽量保证先请求先处理的原则下,尽可能规避潜在的死锁。然而,上述方法只是进行了简单的规避,并不能彻底解决死锁,完全解决需要通过有向等待图来解决,但成本较高,PostgreSQL将这一过程放在了事后。图6.4-6 死锁检测的触发过程 如图6.4-6所示,当阻塞等待超时后就开始进行死锁检测。不过PostgreSQL在有向循环图中引入了Soft Edge和Hard Edge的概念:Soft Edge:进程A和进程B都在同一个锁的等待队列中。进程A和进程B的锁请求不相容,且进程A在进程B的后面,这时进程A指向进程B的有向边为Soft Edge;Hard Edge:进程A请求的锁和进程B已经持有的锁冲突,这时进程A指向进程B的有向边为Hard Edge;可见,Soft Edge是可以通过重新排队进行规避的,而Hard Edge已经形成,是无法改变的。有了Soft Edge和Hard Edge概念之后,我们来看看PostgreSQL是如何进行死锁检测的:从每一个点出发,沿着有向循环图的有向边行进,如果能够回到起点,说明存在死锁;在遍历过程中将Soft Edge记录下来,如果存在死锁且没有Soft Edge,直接终止本事务;如果有Soft Edge。对于每个Soft Edge,递归枚举它的所有子集,尝试进行调整。调整方法采用拓扑进行排序,并遍历测试,如果通过测试表明可以规避死锁,直接结束。如果调整任何一个Soft Edge都无法解决死锁,终止本事务;SIREAD锁和RW-Conflicts图6.4-7 写倾斜于依赖图 在Serializable隔离级别下,PostgreSQL可以解决所有异常,其采用的方法并不是读写都加断言锁和记录锁,而是采用SSI策略(详细情况请参考“事务”章节)。如图6.4-7所示,当依赖图(dependency graph)中存在循环,表示存在写倾斜异常,需要强制某个事务退出,从而打破循环,保证可序列化。可见,SSI的重点是标识rw关系和检测依赖图中是否有循环,为此PostgreSQL定义了SIREAD锁和RW-Conflicts两种数据结构。为了构建RW-Conflicts,首先需要表示出哪些事务读取了哪些记录,这就是SIREAD锁的作用。当执行DML语句时,CheckTargetForConflictsOut函数会创建SIREAD锁。例如,当事务txid1读取记录tuple1时会创建SIREAD锁{tuple1, {txid1}},之后事务txid2也读取记录tuple1时该SIREAD锁会更新为{tuple1, {txid1, txid2}}。可见,SIREAD锁是以记录为单位跟踪相关事务。然而在高并发下,SIREAD锁的数量会非常大,严重消耗系统资源。为此,PostgreSQL采用锁升级的机制来缓解资源消耗。SIREAD锁有tuple、page、relation三个层次。如果某个page的所有tuple都创建了SIREAD锁,那么升级为page级,即以page为单位创建SIREAD锁,原来属于该page的tuple级SIREAD锁全部释放。Relation级即表级,原理同page级。RW-Conflicts是一个三元组,由读事务、写事务、记录(元组)组成。例如,事务txid1读取了记录tuple1,之后事务txid2更新了记录tuple1,那么就需要创建一个RW-Conflict,{txid1, txid2, {tuple1}}。在执行insert、update、delete命令时,CheckTargetForConflictsIn函数会检查相关SIREAD锁,从而判断是否存在RW-Conflicts。如果存在,就创建RW-Conflicts。表6.4-8 写倾斜检测示例一时间Tx_A(txid_a)Tx_B(txid_b)SIREAD LocksRW-ConflictsT1start transaction isolation level serializable;start transaction isolation level serializable;  T2select * from t1 where id=2;(1 row returned) L1:{pkey_2,{txid_a}}L2:{tuple_2,{txid_a}} T3 select * from t1 where id=1;(1 row returned)L1:{pkey_2,{txid_a}}L2:{tuple_2,{txid_a}}L3:{pkey_1,{txid_b}}L4:{tuple_1,{txid_b}} T4update t1 set val=”++” where id=1;(1 row updated)  C1:{r=txid_b, w=txid_a, {pkey_1, tuple_1}}T5 update t1 set val=”++” where id=2;(1 row updated) C1:{r=txid_b, w=txid_a, {pkey_1, tuple_1}}C2:{r=txid_a, w=txid_b, {pkey_2, tuple_2}}T6commit;(success)   T7 commit;(failed)  表6.4-9 写倾斜检测示例二时间Tx_A(txid_a)Tx_B(txid_b)T1start transaction isolation level serializable;start transaction isolation level serializable;T2select * from t1 where id=2;(1 row returned) T3 select * from t1 where id=1;(1 row returned)T4update t1 set val=”++” where id=1;(1 row updated) T5commit;(success) T6 update t1 set val=”++” where id=2;(failed)表6.4-10 写倾斜检测示例三时间Tx_A(txid_a)Tx_B(txid_b)T1start transaction isolation level serializable;start transaction isolation level serializable;T2select * from t1 where id=2;(1 row returned) T3 select * from t1 where id=1;(1 row returned)T4update t1 set val=”++” where id=1;(1 row updated) T5 update t1 set val=”++” where id=2;(1 row updated)T6commit;(success) T7 select * from t1;(failed)假设表t1,在id列上有主键索引。表6.4-8给出了写倾斜检测的详细过程,具体如下:T2:事务Tx_A查询id为2的记录,由于涉及主键,所以CheckTargetForConflictsOut创建了两个SIREAD Locks,分别为L1和L2;T3:事务Tx_B查询id为1的记录,由于涉及主键,所以CheckTargetForConflictsOut创建了两个SIREAD Locks,分别为L3和L4;T4:事务Tx_A更新id为1的记录,CheckTargetForConflictsIn检测SIREAD Locks,发现存在RW-Conflicts,所以创建RW-Conflicts C1;T5:事务Tx_B更新id为2的记录,CheckTargetForConflictsIn检测SIREAD Locks,发现存在RW-Conflicts,所以创建RW-Conflicts C2。此时依赖图已经存在循环,即写倾斜已经产生,然而事务Tx_A和Tx_B都没有提交,所以CheckTargetForConflictsIn无法基于“first-committer-win”原则决策让哪个事务失败;T6:事务Tx_A提交,PreCommit_CheckForSerializationFailure函数检查发现Tx_A和Tx_B存在写倾斜,但Tx_B事务仍然处于运行状态,所以事务Tx_A提交成功;T7:事务Tx_B提交,PreCommit_CheckForSerializationFailure函数检查发现Tx_A和Tx_B存在写倾斜,但Tx_A事务已经提交,所以事务Tx_B提交失败;从上述过程我们发现SIREAD Locks和RW-Conflicts不能在事务提交后立刻释放,需要存在一段时间,以确保相关事务的写倾斜检测能够正常进行。另外,并不意味着事务的倾斜异常只会发生在提交阶段。事实上,CheckTargetForConflictsIn和CheckTargetForConflictsOut都会进行依赖图检测,只要存在循环,且有一个事务已经提交,就会立刻让当前事务失败,例如表6.4-9和6.4-10。CockroachDB设计原理设计思路CockroachDB是一种基于乐观机制的分布式数据库,其默认的隔离级别是可序列化快照(SS, Serializable Snapshot)。和PostgreSQL相比,CockroachDB不采用锁机制,而是将SS发挥到极致,其采用的并发控制有如下特征:可序列化:执行结果和某种串行执行的结果是等价的;可恢复:对于一系列并发执行的事务,有些事务执行成功,有些事务异常退出,仍然能够保证系统可恢复至一致性状态。原子性保证单个事务是可恢复的,严格的乐观调度策略保证任何事务的组合执行也是可恢复的;无锁:执行期间不会在资源上加锁。如果某事务和可序列化、乐观调度机制相关冲突,通过强制该事务退出来保证正确性;分布式:系统无集中的授时、协调或者其它服务;可序列化图在序列化理论中,冲突的发生条件是两个不同事务中的操作操作了相同的数据,且至少有一个操作是写操作。满足上述条件时,就可以说第二个操作和第一个操作相冲突。冲突有三种类型:读写冲突(RW):第二个操作覆盖了第一个操作读取的结果;写读冲突(WR):第二个操作读取了第一个操作写的结果;写写冲突(WW):第二个操作覆盖了第一个操作写的结果;图6.5-1 可序列化图示例 对于事务执行的任何历史,通过这些冲突可以建立一个可序列化图。如图6.5-1所示,将所有事务链接在一起的有向图有如下部分组成:事务是图中的节点;当某操作和另外一个事务的操作冲突时,就画一个从被冲突事务到冲突事务的有向边;图6.5-2 循环可序列化图示例,该历史不可序列化 执行历史是可序列化的,当且仅当可序列化图是非循环的。图6.5-2中的示例就是不可序列化的。CockroachDB采用时间排序来保证可序列化图是非循环的,方法如下:每个事务启动时都会赋予一个时间戳,此后该事务中的所有语句都使用此时间戳;每个操作都可以独立地判断自己和其它事务的哪个操作冲突,以及被冲突操作的时间戳是什么;允许操作和拥有更早时间戳的其它操作相冲突,但不允许和拥有更晚时间戳的操作相冲突;由于在时间前进方向上不允许存在冲突,所以可序列化图就不存在循环。下面章节我们将介绍CockroachDB是如何检测和防止这些冲突的。WR冲突与MVCCWR冲突采用多版本来解决。CockroachDB不仅仅存储单值,而是存储了基于时间戳的多个版本值。写操作不会覆盖旧值,而是创建一个带新时间戳的新值。图6.5-3 多版本值读示例 如图6.5-3所示,对某key的读操作将返回比读操作时间戳小的最新版本。因此,在CockroachDB中后继事务不会形成WR冲突,因此读操作不会使用更晚的时间戳。RW冲突与时间戳缓存任何读操作的时间戳都会缓存在时间戳缓存中。通过该缓存我们可以查询某个key最近进行了哪些读操作,以及这些读操作的时间戳是怎样的。所有写操作在对key进行写时都需要查询时间戳缓存。如果返回的时间戳大于写操作的时间戳,表明RW和一个更晚的时间戳相冲突。这是不允许的,必须以一个更晚的时间戳重启写操作所在的事务。时间戳缓存是一个区间缓存,也就是说其存储的是key的范围。如果某读操作读取了某段范围内的所有key(例如扫描),那么扫描的这些key都以范围的形式存在时间戳缓存中。时间戳缓存完全缓存在内存中,采用LRU算法。当缓存大小达到设定的限制后,最老的时间戳条目就会被删除。为了处理不在缓存中的key,需要定义“低水位线”,其等价于所有key的最早时间戳。如果写操作查询的key不在时间戳缓存中,就返回低水位线。WW冲突与只写最新版本写操作尝试写某key时,该key的时间戳比操作本身的时间戳还要新,表明WW和一个更晚的时间戳相冲突。为了高正可序列化,必须以一个更晚的时间戳重启写操作所在的事务。通过时间排序,拒绝任何不满足排序要求的冲突,CockroachDB的SS可以保证执行结果是可序列化的。严格调度与可恢复性通过前面章节介绍的冲突规则可以保证执行历史是可序列化的。另一个问题是如何保证两个满足冲突规则的未提交事务是可恢复的。假设两个事务T1和T2,T1的时间戳小于T2的时间戳。T1写了key“A”,之后T2在T1提交前读取key“A”。该冲突是被时间排序规则所允许的。但T2应该从key“A”中读到哪一个值呢?假设忽略掉T1的未提交数据,读取数据的前一个版本。如果T1和T2都成功提交,这将引起WR冲突,且和时间排序规则相冲突,因此不可序列化;假设读取T1的未提交数据。如果T2提交成功,T1回滚了,这和T1的原子性相冲突(T1回滚了,但仍然对数据库的状态产生了影响);上述两种情况都是不允许的。为了维护调户的可恢复性,在T1提交前T2不可以提交。为此,CockroachDB采取了严格的调度策略处理此场景:读操作和覆盖操作只允许作用在已提交数据上,操作永远不允许在未提交数据上实施。为了实现原子性提交,key上的未提交数据都保存在意向记录中(Intent Record)。如图6.5-4所示,在MVCC存储结构中,key上的意向记录可以很容易地被查到。在并发环境中,意向记录意味着存在一个正在运行的并发事务。图6.5-4 意向记录与MVCC 严格调度存在两种场景:读操作遇到一个时间戳更小的意向记录,或者写操作遇到一个意向记录(不管时间戳的大小)。对于这两种场景,CockroachDB有两种选择:如果第二个事务的时间戳更大,该事务可以等待第一个事务提交或回滚完毕,然后再继续执行自己的操作;强制其中一个事务退出;作为一种乐观的系统(无等待),CockroachDB选择了强制退出其中一个事务。决策将哪个事务退出的过程如下:step1:第二个事务(遇到意向记录的那个事务)读取第一个事务的事务记录(CockroachDB为每个活跃事务维护一条事务记录,以表征该事务的提交状态);step2:如果第一个事务已经提交(意向记录还没有来得及清理),第二事务清理该意向记录,即将意向记录中的值当成正常值来处理;step3:如果第一个事务已经回滚,第二事务删除该意向记录,并将意向记录当成不存在处理;step4:如果第一个事务处于运行态(未提交),固定选择第一个或第二个事务都是不合理的。同时还存在两个事务同时处理对方,对于冲突的两个事务,胜利的一方最好是确定性的。为此,每个事务记录都赋予一个优先级,永远强制退出优先级地的那个事务。如果优先级相等,强制退出时间戳大的事务。新事务启动时获取一个随机的优先级,当事务因为冲突而重启时,其新的优先级等于max(random, [导致本事务重启的哪个事务的优先级]-1),最终事务在重启的过程中优先级会不断提升。采用本方法,未提交事务之间的冲突可以通过强制退出其中一个事务而立刻得到解决。因此,严格调度确保了所有的事务执行历史都是可恢复的。优先级已经在概率上解决了导致异常事务的问题,即被异常打败的事务会不断地重启,且在重启的过程中优先级会不断地上升,最终获得胜利。另外,CockroachDB在所有事务中增加了心跳。在运行过程中,活跃事务需要周期性地更新其事务记录中的心跳时间戳。如果其它事务碰到某事务的记录时,该事务的心跳时间戳超时,那么该事务被认为是异常事务,此时强制异常事务退出而不是比较优先级。VoltDB设计原理传统数据库的成本Micheal Stonebraker等人在开源数据库Shore上进行了各种基准测试,以调研传统数据库中各组件的成本。测试环境为桌面系统,刚开始性能大约为640TPS。之后每次删除系统中的一个特征,并重新进行基准测试,直至仅剩下一个非常薄的查询内核,性能为12700TPS。这个内核是单线程、无锁、无恢复功能的全内存数据库。通过分解发现了4个影响性能的最大组件:Logging:跟踪数据结构的所有变化并记录日志,拖慢了性能。如果可恢复性不是必须的,或者可通过集群中其它节点进行恢复,日志就不是必须的;Lock:两阶段锁产生了相当大的负载,因为所有对数据的访问都要经过Lock Manager这个单点组件;Latch:在多线程数据库中,很多数据结构在被访问前都要先加上Latch,通过单线程机制可以避免这个诉求,并获得可观的性能提升;BufferManager:内存数据库不需要通过缓存池访问数据页,消除了访问每条记录的间接成本;表6.6-1 传统数据库各组件指令数占比组件New OrderPaymentBtree keys16.2%10.1%Logging11.9%17.7%Locking16.3%25.2%Latching14.2%12.6%Buffer manager34.6%29.8%others6.8%4.7%图6.6-1 NewOrder下各组件指令占比 图6.6-1和表6.6-1给出了这些挑战对应的性能变化情况(测试模型为TPC-C下NewOrder事务和Payment事务,统计的是运行该事务的CPU指令数)。可见每个组件都占整个系统的10%~35%指令数(整个系统运行一遍NewOrder事务的指令数为1.73M)。“hand-coded optimizations”代表的是对B树进行一系列优化。“useful work”代表的是处理查询的实际工作,只占总工作的1/60。“buffer manager”下面的方框代码的是移除上面所有组件之后的性能,这时仍然支持事务,指令数只有总体的1/15,不过仍然是实际工作的4倍(两者之间的差距主要源于函数调用栈的深度,以及无法完全消除缓存管理和事务相关的所有代码)。基于上述分析,Micheal Stonebraker在设计VoltDB时,期望通过裁减Buffer Manager、Latch和Lock等组件以获得更高的性能。因此,VoltDB是一款仅支持序列化隔离级别的分布式内存数据库。内存数据库可以降低Buffer Manager的成本,仅支持序列化隔离级别可以降低Latch和Lock的成本。本章重点讨论VoltDB的并发控制是如何避免Lock成本的。图6.6-2 串行执行队列 假设只有单颗CPU和DRAM内存,我们应该设计一个怎样的程序,在单位时间内仅可能多地执行命令。这些命令可以是创建、查询或者更新结构化数据。如图6.6-2所示,解决方案之一就是将命令放在一个队列中。然后执行一个循环,不断地从队列中取命令并执行。显而易见的是此方法可以让单颗CPU充分运转起来,当然有几纳秒的时间周期用于从命令队列中取命令和将响应放入响应队列中。在循环中,CPU执行的任务基本上100%都是实际工作,而不是系统调度、锁控制、缓存控制等和实际工作不相关的工作。在VoltDB中,用户的命令就是SQL执行计划、分布式分片上的执行计划、或者存储过程的调用,循环就对应于单个分片上的命令队列。并发控制VoltDB每次只会运行一个命令,命令之间无并行无重叠,从而提供了序列化的隔离性。在单颗CPU上高饱和地运行应用的实际工作。然而服务器上有多颗CPU,如何让多颗CPU都高饱和地运行起来?首先对数据进行分片,然后在每个分片上维护一个命令队列。这也是大部分分布式NoSQL数据库的设计思路:操作需要制定待操作数据的KEY。VoltDB采用的是一致性哈希分片,用户需要为每个表指定分片列。这些分片列和NoSQL存储的KEY非常类似。根据分片列判断SQL语句或者存储过程涉及哪个分片,然后将其路由到对应分片的命令队列上。集群中多个服务器或者服务器上多颗CPU,都可以通过增加分片的方法让各CPU繁忙起来,每颗CPU独立运行某个分片上的命令队列,各自提供ACID语义。可见:在每个分片上串行地执行查询或者修改命令;命令可以是SQL、存储过程、SQL执行计划的某个片段;每个命令都提供ACID;表数据分布在各个分片上;通过增加分片的方法在多CPU、多服务器上获得扩展性;事务VoltDB将存储过程作为单独的事务来执行,SQL语句作为自动提交的事务来执行。单分片事务是在单个分片上直接执行的事务。单分片事务可以是只读事务,也可以是读写事务,每个单分片事务都完全满足ACID。实际上单分片只读事务的执行过程可以进一步优化,即越过SPI,以负载均衡的方式直接路由到分片的某个副本上。VoltDB的副本间是以同步的方式执行读写事务,所以只读事务即使越过SPI,仍然可以读到前面事务的结果。此优化可以提升只读事务的吞度量,降低只读事务的延时,减轻SPI的工作量。图6.6-3 只读事务在分片的副本间负载均衡 图6.6-3以示例的方式展示了优化的正确性,事务A和事务C为读写事务,事务B为只读事务,且应用的发起顺序为事务B先于事务C而后于事务A。事务B放在任何一个副本的序列化命令队列中都是正确的(不影响其它副本的结果)。VoltDB支持事务在多分片上进行读写操作,这样的是称为多分片事务。SPI为单个分片实施序列化工作,MPI为跨分片事务实施序列化共诺。MPI会和相干分片的SPI交互,以将分解后的命令注入到对应分片的命令队列中。图6.6-4 只读事务在分片的副本间负载均衡 图6.6-4示例了MPI执行多分片事务M的过程。SPI#1将事务M序列化在单分片事务C的后面执行,SPI#2将事务M序列化在单分片事务B的前面执行。从全局来看,事务的执行顺序为C、M、B。为了执行多分片SQL,VoltDB的SQL执行计划生成器会将执行计划分解成多个片段,有些片段在多个分片上分布式执行,有些片段对分布式执行的结果进行汇总。多分片写事务在各分片间采用两阶段提交协议。在Prepare阶段,MPI将执行接话片段分发到各个分片执行。如果这些片段在各分片上执行成功,无约束性冲突,MPI通知所有分片进行提交。各分片不会执行命令队列中的任何其它命令,只到收到提交消息。VoltDB中多分片事务的大部分案例是分布式读,要么是读取记录时不知道分片的取值,要么是进行汇聚分析。对于仅含只读工作的多分片事务,用户可以通过标签显式地表达出来,这样上述分布式过程就可以进行优化:SPI可以将命令发给任何某个副本,而不需要在副本间同步;分片执行完读操作后,可以立刻执行命令队列中的其它命令,而不是阻塞在那里等待提交消息;总结与分析并发控制的原则是在保证正确性的前提下尽可能地提高并发性,为此Oracle、MySQL、PostgreSQL、CockroachDB、VoltDB采用了不同的策略以提高并发性。从并发控制算法的用户友好度和ANSI SQL隔离级别匹配度来看。MySQL支持ANSI SQL定义的四种隔离级别,在任何情况下都会加记录级写锁,避免了Dirty Write异常。在Read Committed级别下通过语句级一致性读解决了Dirty Read异常。在Repeatable Read级别下,通过事务级一致性读以及Next_key写锁解决了Fuzzy Read和Phantom异常,但由于读不加锁,仍然存在Lost Update和Write Skew异常。在Serializable级别下,读写都加Next_key锁,可以解决所有异常。PostgreSQL真正意义上仅支持三种隔离级别(Read Uncommitted实际上就是Read Committed),在任何情况下都会加记录级写锁,避免了Dirty Write异常。在Read Committed级别下通过语句级一致性读解决了Dirty Read异常。在Repeatable Read级别下,通过事务级一致性读以及写锁解决了Lost Update、Fuzzy Read、Phantom异常,但由于读不加锁,Lost Update异常只能采取“First-Update-Win”原则,对用户不友好,而Write Skew异常仍然无法解决。在Serializable级别下,通过SSI算法进一步解决Write Skew异常,但解决的方法是一旦发现潜在的Write Skew,就强制某个事务退出,对用户并不友好。Oracle仅支持Read Committed和Serializable两种隔离级别,在任何情况下都会将记录级写锁,避免了Dirty Write异常。在Read Committed级别下通过语句级一致性读解决了Dirty Read异常。在Serializable级别下,通过事务级一致性读和SCN比较,解决了Lost Update、Fuzzy Read和Phantom异常,读不加锁导致Lost Update只能通过报错来解决,对用户不友好,同时读不加锁导致Write Skew异常无法解决。CockroachDB支持Snapshot Isolation和Serializable Snapshot Isolation隔离级别,通过多版本和时间戳排序达到可序列化要求。然而为了可恢复新采用了严格的调度策略,不管是读操作还是写操作一旦遇到比自己跟到且未提交的时间戳,必须强制一个事务退出,对用户不友好。VoltDB仅支持Serializable隔离级别,所有事务都串行执行,不存在任何异常。可见在ANSI SQL隔离级别匹配度上MySQL最高,然后依次是PostgreSQL、Oracle、CockroachDB和VoltDB。MySQL、PostgreSQL、Oracle都支持用户在select语句上指定加锁,这样即使在低隔离级别上也可以选择性地解决Lost Update和Write Skew异常。从并发控制算法的效率上来看,Oracle没有设计独立的锁结构,仅在记录上通过1个字节的lb表达出锁信息,理论上锁资源是无穷的。Enqueue机制对等待的事务进行排队,并区分拥有者和等待者,进行非常精准的唤醒。MySQL和PostgreSQL的锁机制比较类似,正常情况下通过记录上的标志位进行判断(判断规则比较复杂),一旦出现冲突则转换为显式锁。在显式锁方面,MySQL以page为单位组织锁资源,在空间和时间上做了权衡。PostgreSQL采取了记录、page、表多粒度的方式组织锁资源。在冲突对事务进行排队时,两者相对Oracle都比较粗糙。和MySQL不同的是,PostgreSQL在SSI方面又引入了SIREAD锁和RW-Conflicts,对所有读操作、读写操作都要进行跟踪记录,并进行检索判断,成本非常高。CockroachDB需要对所有记录的读操作维护时间戳,成本较高。当然由于采用的是乐观控制,在低冲突场景下效率相对较高,在中高冲突下由于要频繁地重做,效率是极低的。VoltDB采用的是串行执行策略,效率非常高。但场景首先,需要以存储过程为事务执行单位,减少应用和数据库之间的来回交互,同时负载要有非常好的可切分性,每颗CPU负责一个分片,分片之间无相关性。可见,在效率上Oracle是最高的,MySQL和PostgreSQL相当。CockroachDB和VoltDB引入了新思路,但场景相对受限。Oracle、MySQL、PostgreSQL采用了锁机制,存在死锁的情况,三者都采用有向循环图的检测方法。Oracle认为死锁检测的代价较大,只有在锁等待超时后才会检测死锁。MySQL在发生锁等待时提前进行死锁检测,提前解决死锁问题。PostgreSQL也采用了锁等待超时后进行检测的策略,但在事前和事后都做了一些小的优化,尽可能地避免死锁。PDF版本下载地址:http://blog.itpub.net/69912723/viewspace-2725664/
文章
机器学习/深度学习  ·  SQL  ·  缓存  ·  Oracle  ·  算法  ·  关系型数据库  ·  MySQL  ·  数据库  ·  PostgreSQL  ·  索引
2023-03-27
【数据库设计与实现】第6章:并发控制
并发控制设计原则事务的并发控制首先要保证并发执行的正确性,满足可序列化要求,即并发执行的结果和某种串行执行的结果是一致的,然后在满足正确性的前提下尽可能地获得最高的并发度。当然在某些业务场景下,可以适当牺牲部分正确性(即接受某些异常),从而获得更高的并发性能。并发控制大体分为悲观算法和乐观算法,为了尽可能深入了解各种算法的优缺点,本章在Oracle、MySQL的基础上增加了PostgreSQL、CockroachDB和VoltDB。Oracle、MySQL、PostgreSQL采用了悲观控制策略,同时通过MVCC进一步提高并发性,而PostgreSQL在此基础上实现了Serializable Snapshot Isolation。CockroachDB完全采用了乐观控制,是乐观控制的开源和商业化实现。VoltDB在并发控制策略上做了突破新创新,舍弃了悲观控制和乐观控制,采用了全串行化的执行策略。在设计和实现并发控制时,有如下几点需要考虑:并发控制算法的用户友好度和正确性,与ANSI SQL隔离级别的匹配度;并发控制算法的效率;并发控制算法的死锁策略;Oracle设计原理事务Oracle数据库支持ANSI SQL定义的Read Committed、Serializable隔离级别以及一个自定义的Read Only隔离级别,且默认Read Committed隔离级别。不支持ANSI SQL定义的Read Uncommitted和Repeatable Read隔离级别,主要基于如下考虑:Read Uncommitted:脏读主要有两个作用,其一是读不加锁,降低读操作的成本,以提高并发度。其二是可以读到最新的未提交数据。Oracle采用了多版本设计,读语句天然不会对记录加锁,同时读取最新脏数据的应用场景也比较少,基于上述考虑,Oracle不支持Read Uncommitted隔离级别;Repeatable Read:Repeatable Read和Serializable的主要区别是读断言锁是长期的还是短期的(详细情况请回顾“事务”章节的“Locking”小节),而Oracle在记录上没有设计读锁,所以两者没有区别,因此Oracle只提供了Serializable隔离级别,不支持Repeatable Read隔离级别;Oracle在事务的并发控制上综合运用了锁机制和多版本机制(MVCC),在锁机制中仅在记录上设计了行级写锁,没有设计行级读锁,读导致的相关异常通过多版本机制和用户加锁来解决(select ... for update)。在Read Committed隔离级别下,记录被修改时会在该记录上加上写锁且一直持续到事务提交或回滚,而读取记录则通过多版本机制读取已经提交的最新记录(多版本一致性读的原理,请回顾“数据前像与回滚”章节的“一致性读”小节)。为了能够读到最新的已提交数据,在每次查询语句开始前会获取当前的最新scn,该scn之前的最新已提交记录都可以被读取。这样既可以保证读到最新的已提交事务的数据,又保证了语句执行过程中的一致性。在Serializable隔离级别下,记录被修改时会在该记录上加上写锁且一直持续到事务提交或回滚。多版本机制和Read Committed下有所不同,Read Committed在每条查询语句开始之前都会获取当前最新的scn,而Serializable在事务开始前获取当前最新的scn。整个事务运行期间保持该scn不变,从而解决了不可重复读和幻读异常。然而只有写加锁,读不加锁,从而存在如下异常:Lost Update:r1[x=100]r2[x=100]w2[x=120]c2w1[x=130]c1是“事务”章节中的示例,事务T2对x增加的20将丢失。Oracle是通过报错来解决Lost Update异常的。当事务修改某条记录时,发现该记录的当前提交scn大于本事务的开始scn,说明该记录在本事务运行期间被其它事务修改并提交过,此时已经无法达成可序列化,报“ORA-08177: can’t serialize access for this transaction”错误,本次事务执行失败;Write Skew:r1[x=50]r1[y=50]r2[x=50]r2[y=50]w1[y=-40]w2[x=-40]c1c2是“事务”章节中的示例,需满足x+y为正数的约束,从单个事务T1和T2来看都是满足该约束的,但执行成功后不再满足该约束;解决上述异常的方式是使用select ... for update,即应用通过语句要求数据库在读操作时在记录上加锁(原则上此处加读锁即可,但Oracle没有记录级读锁,所以此处加的仍然是记录级写锁,一定程度上影响并发性),从而解决上述两种异常。实际上,在Read Committed隔离级别下,应该也可以通过select ... for update强制读语句加上写锁以达成可序列化的效果,缺点是降低了并发性。在Read Only隔离级别下,多版本机制和在Serializable隔离级别下是一样的,不同的是Read Only隔离级别下不允许执行DML写语句。Read Only对于分析型等只读场景是非常有意义的,既可以读到一致性的数据,同时又不阻塞正常的写事务。记录锁图6.2-1 记录级锁示意图 在上节我们知道Oracle只有记录级写锁,没有记录级读锁,即完全是通过记录级写锁达成事务的并发控制。图6.2-1给出了Oracle记录级写锁的示意图,在每行记录的头部都有一个字节的lb字段,记录本条记录被ITL中的哪个事务给锁定了。如果某条记录的lb指向ITL中的事务A,且该事务处于活跃态,那么该记录就被事务A锁住了,即事务A在该记录上加了记录级写锁。此处引出了Oracle的一个重要的设计理念,锁就是数据的一部分(占用1个字节),存在于block(data block、index block)中。这样的设计有如下优势:锁资源轻量且无限大:不需要在独立的内存区域中设计锁结构,锁就在数据中,随着block在内存和持久设备中换入换出,锁资源无限大,所以Oracle不需要设计多层次的锁粒度,并根据锁记录的数目在不同锁粒度间升级;易于传输:锁是记录的一部分,可以随着block进行传输,这一点在Oracle RAC中体现得非常明显,当block在数据库实例间传输时锁信息自然也就传输过去了;表锁当我们在做DDL语句时需要对操作的表加表锁,从而防止其他用户同时对该表做DDL操作。在更改表结构时还需要防止此时有其它事务正在更改本表中的记录,为此需要逐行检查本表的记录上是否有锁。如果表中的记录非常多,逐行检查表上记录是否有锁非常消耗资源,可能还涉及block的读入与写出,导致性能进一步恶化。为了解决此问题,可以在表上引入新的锁类型,以表明其所属的行上有锁,这就是意向锁。意向锁指如果对某个节点加意向锁,则说明该节点的下层节点正在被加锁。对任一节点加锁,必须先对上层节点加意向锁。对应到表和记录,对表中的任何记录加记录锁前,必须先对该表加意向锁,然后再对该记录加记录锁。这样DDL对表加锁时,不需要再逐行检查表中每条记录上的锁标志了,直接判断表上是否有意向锁即可,系统效率得以提升。意向锁有如下锁类型:意向共享锁(Intent Share Lock,IS锁):如果对记录加S锁,需要先对表加IS锁,以表示该表的记录准备(意向)加S锁;意向排它锁(Intent Exclusive Lock,IX锁):如果对记录加X锁,需要先对表加IX锁,以表示该表的记录准备(意向)加X锁;表上有基本的S锁和X锁,意向锁又引入了IS锁和IX锁,这样可以组合出新的S+IS、S+IX、X+IS、X+IX四种锁。但实际上只有S+IX有意义,其它三种组合都没有使锁的强度得以增强(即:S+IS=S,X+IS=X,X+IX=X,等于指强度相等)。这样我们引入了一种新的锁类型:共享意向排它锁(Shared Intent Exclusive Lock,SIX锁)。事务对某表加SIX锁,表示该事务要读取整个表(所以要对该表加S锁),同时会更新表中的部分记录(所以要对该表加IX锁)。意向锁封锁的策略:加锁:申请封锁时,应按照自上而下的次序进行;解锁:释放锁时,应按照自下而上的次序进行;可见,数据库表上的锁类型有S、X、IS、IX、SIX五种。Oracle的表锁分别有S、X、RS、RX、SRX,与S、X、IS、IX、SIX一一对应。需要注意的是Oracle在记录上只提供X锁,所以与RS(通过select ... for update语句获得)对应的记录锁也是X锁(该行实际上海没有被修改),这与理论上的IS锁有所区别的。表6.2-1 表锁相容矩阵 SXRSRXSRXSYNYNNXNNNNNRSYNYYYRXNNYYNSRXNNYNN表6.2-2 语句与表锁的对应关系锁锁语句场景NULL1select ... from table_nameRS2select ... from table_name for update(9.2.0.5之前版本)lock table table_name in row share modeRX3insert into table_nameupdate table_namedelete from table_nameselect ... from table_name for update(9.2.0.5及后继版本)lock table table_name in row exclusive modeS4create index ...lock table table_name in share mode外键上没有索引SRX5lock table table_name in share row exclusive mode外键约束定义成on   delete cascadeX6alter table ...drop table ...drop index ...truncate table ...lock table table_name in exclusive mode表6.2-1为Oracle表锁的相容矩阵,Y表示相容,N表示不相容,需要阻塞等待。表6.2-2给出了语句与表锁之间的对应关系示例,锁给出了字符和数值两种表达方式。当Oracle执行select ... for update、insert、update、delete等DML语句时,会在操作的表上自动加上表级RX锁。当执行alter table、drop table等DDL语句时,会在操作的表上自动加上表级X锁。另一方面,应用程序或者操作人员也可以通过lock table语句指定需要获得某种类型的表锁。最后再介绍一下Oracle的breakable parse locks(分析锁)。Oracle会在share pool中缓存分析和优化过的SQL语句和PL/SQL程序,这样再次执行这些相同的SQL或PL/SQL程序时,不必再进行解析、编译、生成执行计划,直接使用缓存的执行计划。缓存的执行计划对所涉数据库表是有依赖的,即当表结构发生变更时,缓存的所涉的执行计划需要及时失效。分析锁就是为了解决及时通知问题的,当缓存执行计划时,会在所涉数据库对象上加上分析锁。该分析锁会一直持有,直到对应的执行计划失效。分析锁不会产生任何阻塞,当表结构发生变更时,会及时通知对缓存的相关执行计划失效。Enqueue在上面章节我们知道Oracle有记录级X锁,有多种模式的表锁。通过这些锁在保证正确性的前提下,提供了最大的事务并发度。但从实现层面来看,我们还有两个关键问题尚未解决:问题1:如何高效地知道某个数据库对象上已经加了锁,加了什么模式的锁;问题2:当发生冲突时如何对事务排队,持有者释放锁时如何及时唤醒阻塞事务并保证公平性;表6.2-3 部分常见enqueue type大类类型场景User enqueuesTXAllocating an ITL entry in order to begin a transaction;Lock held by a transaction to allow other transactions to wait for it;Lock held on a particular row by a transaction to prevent other transactions from modifying it;Lock held on an index during a split to prevent other operations on it;TMSynchronizes accesses to an object;ULLock used by user applications(通过DBMS_LOCK.REQUEST加锁);System enqueuesSTSynchronizes space management activities in dictionary-managed tablespace;CICoordinates cross-instance function invocations;TTSerializes DDL operations on tablespace;USLock held to perform DDL on the undo segment;CFSynchronizes accesses to the controlfile;TCLock held to guarantee uniqueness of a tablespace checkpoint;Lock of setup of a unqiue tablespace checkpoint in null mode;ROCoordinates fast object reuse;Coordinates flushing of multiple object;PSParallel execution server process reservation and synchronization;首先来看问题1,因为锁是加在数据库对象上的,这些对象可以是表、文件、表空间、并行执行的从属进程、重做线程等等,我们将这些对象统一称为资源。为此,Oracle在SGA中设计了enqueue resource数组,数组中的每个元素代表一个资源,数组的总大小可通过参数_enqueue_resources设置(可通过x$ksqrs和v$resources查看enqueue resources)。Enqueue resource中的每个元素就是一个ksqrs结构,ksqrs结构中的关键成员有:enqueue type:标识锁类型(或称为资源类型),Oracle内部的锁类型非常丰富,表6.2-3给出了部分常见的锁类型。各种internal locks都会在system enqueue中对应一种类型,记录锁和表锁属于user enqueue,分别对应于TX和TM类型;enqueue identification(ID1、ID2):用于标识具体的资源,例如当enqueue type等于TM时,identification存放具体哪个表(ID1等于表的object id),当enqueue type等于TX时,identification存放具体哪个事务(ID1高位的2个字节存放undo segment id,ID1低位的2个字节存放transaction table id,ID2存放wrap);link:双向指针,用于将相同状态的ksqrs结构链接在一起,例如处于空闲状态或者在同一个hash桶中;owners:指向双向链表的头部和尾部,该双向链表存放所有已经持有本资源的锁信息;converters:指向双向链表的头部和尾部,该双向链表存放所有已经持有本资源并等待升级到锁强度更高的锁信息;waiters:指向双向链表的头部和尾部,该双向链表存放所有已经等待本资源的锁信息;图6.2-2 Enqueue Free List与Hash Table 如图6.2-2所示,正常情况下单个ksqrs结构未被使用前通过link双向指针串在一起,组成ksqrs的free list。当需要申请一个资源时,从free list上摘一个ksqrs结构下来,填写enqueue type和enqueue identification,并根据enqueue type和enqueue identification计算hash值,从而算出本krqrs结构归属的hash bucket,并将该ksqrs结构加入到算出的hash bucket中的hash chains中(hash chain中的ksqrs也是通过link双向指针链接在一起的)。hash算法越优秀,hash冲突越小,hash chain的长度越短。可见,处于使用状态的ksqrs是通过hash进行管理的,这样可以快速定位某个资源是否已经加锁(enqueue type和enqueue identification可以唯一标识某个特定的资源)。Hash table的长度(即bucket的数量)可以通过参数_enqueue_hash设置。由于多个用户会并发访问enqueue hash table,所以需要对其进行并发访问保护。系统会申请若干个enqueue hash chains latch(parent latch与child latch,详细情况请回顾“同步与互斥”章节),每个enqueue hash chains latch保护一段bucket(实际上是round-roubin方式)以及这些bucket后面的hash chain。Enqueue hash chains latch的数量由参数_enqueue_hash_chain_latches设置,默认值为cpu_count。假设表t1的object id为1234,现在需要对t1加表锁,那么首先需要申请TM资源。申请资源的大致过程如下:step1:查找enqueue hash table中是否已经有表t1的资源(表资源的类型为TM),对TM、1234(id1=object id)、0(id2=0)计算hash值,从而得到对应的bucket(此处假设为bucket12);step2:申请获得bucket12对应的enqueue hash chains latch;step3:成功获得latch后,查找bucket12的hash chain,看是否已经有表t1的TM资源,如果有则表示不需要创建新的t1资源,释放latch直接退出,否则进入下一步;step4:从ksqrs free list上摘下一个ksqrs结构,将enqueue type设置为TM,将enqueue identification设置为id1=1234,id2=0,然后将该ksqrs结构添加到bucket12的hash chain中;step5:至此完成表t1资源的创建,释放latch,并退出;在上述步骤4中,需要从ksqrs free list上摘下一个空闲的ksqrs结构。Ksqrs free list本身也需要同步与互斥保护,在高并发场景下会有大量频繁的申请与释放,此处就会成为瓶颈。为此,Oracle采用了Lazy策略,即释放资源后对应的ksqrs结构并不立刻归还到ksqrs free list中,而是保留一部分空闲ksqrs结构在chain chain上,这样后继可以直接复用,从而提升性能。至此,我们已经完成enqueue resource的介绍。但enqueue resources只是一个容器,只能给出问题1的部分答案,即解决了如何快速找到某个数据对象(资源)的问题,还需要回答问题1提出的锁模式和问题2。为此,我们需要引入另外一个结构“锁”,“锁”是加在资源上的,即附着在某个ksqrs结构上的。图6.2-3 KSQRS结构及锁对应关系 如图6.2-3所示,每个资源都对应一个ksqrs结构,加在该资源上的所有锁都通过ksqrs结构进行排队:Owners:持有者,即该资源的拥有者,每个锁对应一个拥有者,拥有者不会被阻塞,当有多个拥有者时这些拥有者的锁一定是相容的;Converters:转换者,由拥有者转换而来,表示已经拥有低强度的锁,但在申请变更为更高强度锁时和其它拥有者的锁不相容;Waiters:等待者,和拥有者的锁不相容;当拥有者释放锁时,首先唤醒转换者,即将转换者变更为新的拥有者。当拥有者和转换者都为空时,依次唤醒等待者。如果等待者中有多个相邻的锁是相容的,可以同时唤醒成为拥有者,即如果锁4和锁5是相容的,可以同时成为拥有者。有了上述概念之后,我们首先来看表锁的互斥排队过程。表锁对表对象加锁,所以容纳表锁的ksqrs类型为TM。每个表锁是一个ktqdm结构,申请表锁时首先从ktqdm free list中申请一个ktqdm结构(ktqdm free list由dml allocation latch保护),然后将ktqdm结构附着到对应表的ksqrs结构上。ktqdm结构中关键成员有:sid:锁对应的会话(session);lmode:当前已经持有的锁模式;request:当前正在请求的锁模式;ctime:锁已经持有的时长或者等待的时长;表6.2-4 表锁阻塞时序示例TimeSession1(S1)Session2(S2)Session3(S3)Session4(S4)T1lock table t1 in row exclusive mode;Lock table t1 in row exclusive mode;  T2 Lock table t1 in share row exclusive mode;  T3  Lock table t1 in exclusive mode; T4   Lock table t1 in row exclusive mode;T5Commit;   T6 Commit;  T7  Commit; T8   Commit;图6.2-4 表锁阻塞队列示例 如表6.2-4所示,该表展示了一个针对表t1的时序示例,4个会话(s1、s2、s3、s4)同时对表t1加表锁。图6.2-4给出了T4时刻,表t1上的各表锁之间的阻塞情况。详细过程如下:因为都是对表t1加锁,所以相关的ktqdm结构都附着在同一个ksqrs结构上,ksqrs的类型为TM,id1=t1(实际上是表t1的object_id),表示资源为表t1;T1时刻:s1和s2两个会话同时对表t1加row exclusive锁,这两个锁是相容的,所以都在持有者队列中,通过ktqdm结构中的link链成双向链表,lmode=3表示持有的锁模式为row exclusive;T2时刻:会话s2尝试对表t1加SRX(share row exclusive),即将锁的强度从RX升级为SRX。由于s2的SRX与s1的RX是不相容的,所以s2的ktqdm结构从持有者链表中迁移到转换者链表中,lmode=3表示s2已经持有RX锁,request=5表示s2正在申请SRX锁,此时会话s2阻塞;T3、T4时刻:会话s3和s4分别对表t1加X和RX锁,这两个锁要么和持有者的锁不相容,要么和转换者的锁不相容,所以按照申请的顺序加入到等待者链表中,lmode=0表示尚未持有任何锁,request=6/3表示正在申请的锁模式,此时会话s3和s4阻塞;T5时刻:会话s1提交并释放锁,此时s2从转换者链表迁移到持有者链表中,更新(sid=s2, lmode=5, request=0)表示锁升级为SRX,此时会话s2开始运行;T6时刻:会话s2提交并释放锁,此时持有者和转换者链表都为空,从等待者链表中将s3迁移到持有者链表中,并更新(sid=s3, lmode=6, request=0),由于会话s4的RX和会话s3的X不相容,所以会话s4仍然留在等待者链表中,此时会话s3运行,会话s4继续阻塞;T7时刻:会话s3提交并释放锁,会话s4从等待者链表迁移到持有者链表中,开始运行;至此,我们介绍了表锁的整个运行过程,回答了表锁相关的问题1和问题2,即通过ksqrs结构定位到并发阻塞的资源,通过ksqrs的持有者、转换者、等待者三个链表结合ktqdm结构完成排队、阻塞和唤醒。从中我们可以发现如下关键点:一个会话对同一个表不管加多少次表锁,只会占用一个ktqdm结构;转换者链表中的元素优先级高于等待者链表中的元素,因为转换者中的元素已经持有锁,需要让它们尽快运行以尽快释放锁;从等待者链表向持有者链表迁移时,是按照入链的顺序迁移的,即按照申请的顺序迁移的,体现了FIFO的公平性;下面我们开始介绍记录锁。对于问题1,记录锁是很容易解决的,每条记录的头部有lb标志,且记录锁只有X模式,所以记录锁的重点是解决问题2,即对有记录锁冲突的事务如何进行排队。记录锁的排队机制和表锁的排队机制是类似的,主要区别如下:仍然通过ksqrs enqueue排队,但ksqrs的type为TX,id1和id2等于事务id,即每个事务开启第一次写时会申请一个标识本事的TX类型的ksqrs结构,后继因为和本事务记录锁发生冲突的会话全部附着在该ksqrs结构上;不需要像表锁为每个锁申请一个kstqdm,只需要为每个冲突的事务申请一个ktcxb结构;表6.2-5 记录锁阻塞时序示例TimeSession1(S1)Session2(S2)T1--transaction id=10.1.145Update t1 set c1=1;100 rows updated. T2 --transaction id=10.2.23Update t2 set c1=2;100 rows updated.T3 Update t1 set c1=2 where c2=3;T4Commit; T5 1 rows updated.T6 Commit;图6.2-5 记录锁阻塞队列示例 如表6.2-5所示,该表展示了两个事务(10.1.145和10.2.23)同时修改表t1和表t2中记录的情况,因为同时修改t1中的记录而发生记录锁冲突。图6.2-5展示了在T3时刻TX排队情况。详细过程如下:T1时刻:会话s1开启一个写事务(第一条语句就是更新表t1的全表记录),申请一个ksqrs结构,类型为TX,id1=655361(10*65536+1),id2=145,并加入到ksqrs enqueue hash表中。同时申请一个ktcxb结构,设置sid=s1,lmode=6(记录锁只能是X模式),request=0,并将标识本事务的ktxcb附着在该ksqrs的持有者链表上;T2时刻:会话s2开启一个写事务(第一条语句就是更新表t2的全表记录),申请一个ksqrs结构,类型为TX,id1=655362(10*65536+2),id2=23,并加入到ksqrs enqueue hash表中。同时申请一个ktcxb结构,设置sid=s2,lmode=6,request=0,并将标识本事务的ktxcb附着在该ksqrs的持有者链表上;T3时刻:会话s2更新t1表的单条记录,通过检查该记录的记录头,发现该记录已经被事务10.1.145锁住,申请一个ktcxb结构,设置sid=s2,lmode=0,request=6,并将标识本会话的ktcxb附着在事务10.1.145的ksqrs的等待者链表上,以等待事务10.1.145释放该记录锁,此时会话s2阻塞;T4时刻:事务10.1.145提交,唤醒ksqrs(TX,id1=655361,id2=145)中等待者链表中的所有会话,然后释放ksqrs结构(TX,id1=655361,id2=145)和附着在该ksqrs上的ktcxb结构,此时s2会话激活,继续执行对表t1的记录更新;至此,我们介绍了记录锁的整个运作过程,回答了记录锁相关的问题1和问题2,即在记录头部发现记录冲突,在通过ksqrs的持有者、等待者链表结合ktcxb完成排队、阻塞和激活。从中我们还可以发现如下关键点:每个写事务都会申请一个ksqrs(类型为TX)结构,并持有到事务结束,可见事务本身一种资源;每个写事务都会申请一个或两个ktxcb结构,可见ktcxb结构的数量和修改的记录数无关,只可冲突的事务数相关;所有和事务A有记录冲突的事务都会申请一个ktxcb结构,并将这些ktxcb结构附着在事务A的ksqrs的等待者链表中;TX事务锁除了用于记录锁的排队之外,还用于ITL Entry Shortage时事务的排队。当事务修改block中的数据时,首先需要在该block中占用一个ITL Entry。如果ITL Entry已经被用满,且无法动态扩展ITL时,本事务就需要阻塞等待。此时为本事务申请一个ktxcb结构,然后在本block的ITL中随机选择一个活跃事务,将ktxcb结构附着在该活跃事务的ksqrs结构的等待者链表上。这样当该活跃事务提交时,其占用的ITL Entry就会空出来,唤醒本事务复用该ITL Entry。实际上,Oracle不仅仅将enqueue机制应用于表锁和记录锁,而是将enqueue机制通用化,当系统资源冲突或者不足时都采用enqueue机制进行排队。enqueue机制通用化时,都是通过ksqrs进行排队,只是enqueue type不同。同时不同的资源,用于排队的结构也不同,ktqdm用于表锁,ktcxb用于事务锁,ksqeq、kdnssf、ktatrfil、ktatrfsl、ktatl、ktstusc、ktstusg、ktstuss等等都是用于各种internal locks。不过不管是表锁、事务锁,还是各种internal locks,最终都是通过_enqueue_locks参数设置总lock的数量。enqueue采用数组结构,同时又通过双向指针对数组中的结构进行分类管理。对于大小和属性相同的对象,Oracle一般采用数组这种数据结构进行管理。数组是采用分段方式进行分配和管理的,即Oracle初始只会分配一个容纳固定数量数据单元的内存块,然后在运行过程中动态分配更多的内存块。例如,x$ksqrs数组初始会申请一个较大的内存块,后继不够时再每次申请可容纳32个ksqrs结构的内存块,以此进行动态扩容。死锁Oracle的latch是通过对latch设置level属性在事前规避死锁,而lock的申请顺序和用户语句的执行时序强相关,无法通过事前规定lock的顺序来规避。因此,Oracle采用了事后检测的方法来解决死锁。当会话因为锁等待达到3秒后会醒来,这时会检查等待关系。如果存在循环等待表示存在死锁,否则进行下一个3秒周期的等待。如果检查发现存在死锁,就会触发ORA-60 deadlock detected错误,让应用参与决策。由于是事后超时检查死锁,所以一般是等待时间长的事务先报错。MySQL设计原理事务MySQL数据库支持ANSI SQL定义的Read Uncommitted、Read Committed、Repeatable Read、Serializable四种隔离级别,默认隔离级别为Repeatable Read。MySQL采用的是索引组织表,表中的记录时按照索引键或主键存放的,这就为加断言锁提供了基础。实际上,MySQL就是通过间隙锁锁住记录之间的间隙,从而达到断言锁的目的,防止幻读。各隔离级别下,MySQL的并发控制机制如下:Read Uncommitted:不使用一致性读,允许读取未提交事务的记录,因此会有脏读。只有更改记录或者用户强制lock read才会加锁,且只对记录加record_lock,不会间隙加锁;Read Committed:使用一致性读,且总是读取最新提交的快照数据,允许不可重复读和幻读,在外键检查时对间隙加锁,其它情况只对记录加锁;Repeatable Read:使用一致性读,且在同一个事务中读取的总是相同的历史快照数据,在更改记录或者用户强制lock read时对记录和间隙加锁,这样避免不可重读和幻读(在某些情况下可以只对记录加锁,如唯一索引等);Serializable:不使用一致性读,所有更改和读取操作都会加锁,加锁机制和可重复读一致;可见,MySQL的并发控制机制与“事务”章节介绍的Locking理论是最接近的,同时在Read Committed、Repeatable Read隔离级别下采用了一致性读机制(详细情况请参加“前像数据与回滚”章节),读不加锁,从而最大化地提高并发度。当然在Read Committed、Repeatable Read隔离级别下也可以通过lock read(select ... lock in share mode加共享锁,select ... for update加排它锁)主动对记录加锁,从而在较低隔离级别下也可以解决lost update、write skew等问题。记录锁表6.3-1 记录锁相容矩阵(行为已加锁类型,列为待加锁类型) LOCK_S_GAPLOCK_S_REC_NOT_GAPLOCK_S_ORDINARYLOCK_S_INSERT_INTENTIONLOCK_X_GAPLOCK_X_REC_NOT_GAPLOCK_X_ORDINARYLOCK_X_INSERT_INTENTIONLOCK_S_GAPYYYYYYYYLOCK_S_REC_NOT_GAPYYYYYNNYLOCK_S_ORDINARYYYYYYNNYLOCK_S_INSERT_INTENTIONYYYYNYNYLOCK_X_GAPYYYYYYYYLOCK_X_REC_NOT_GAPYNNYYNNYLOCK_X_ORDINARYYNNYYNNYLOCK_X_INSERT_INTENTIONNYNYNYNY记录锁类型包括共享锁(LOCK_S)和排它锁(LOCK_X)两种类型。MySQL支持对间隙加锁,所以有如下不同的锁算法:LOCK_GAP:间隙锁,仅对间隙加锁,锁住前一条记录和本条记录之间的间隙,但不包括本条记录和前一条记录本身;LOCK_REC_NOT_GAP:记录锁,仅锁住本条记录;LOCK_ORDINARY:Next_Key锁,是LOCK_GAP和LOCK_REC_NOT_GAP的组合,锁住本条记录以及本条记录和前一条记录之间的间隙,但不包括前一条记录;LOCK_INSERT_INTENTION:插入意向锁是一种特殊的间隙锁类型,又称为插入意向间隙锁(insertion intention gap lock),这种锁在插入操作执行前产生。假设已经存在两个索引值4和7,两个事务分别插入记录5和6,每个事务在插入数据前都能在(4, 7)中获得一个插入意向间隙锁,并且由于这两个事务插入的记录不相等而不会互相阻塞。但是,如果间隙(4, 7)之前已经被其它事务加上间隙锁,插入意向间隙锁就会被阻塞,从而防止前事务幻读;可见,MySQL支持两种锁类型,四种锁算法,这样共计可以组合出八种不同的锁,具体相容关系如表6.3-1所示,并从中可以发现如下规律:不管是哪种锁算法,共享锁与共享锁之间都是相容的,即LOCK_S_*和LOCK_S_*是相容的;不管已经持有的锁是哪种类型和算法,待加的LOCK_S_GAP和LOCK_X_GAP都是相容的,即GAP锁(不含插入意向锁)和所有已经持有的锁都是相容的,因为GAP锁主要用于防止将来其它事务的插入操作(避免幻读);LOCK_S_REC_NOT_GAP、LOCK_S_ORDINARY、LOCK_X_REC_NOT_GAP、LOCK_X_ORDINARY之间的不相容主要发生在记录本身的共享与排它、排它与排它的不相容;LOCK_S_INSERT_INTENTION和LOCK_X_INSERT_INTENTION表示即将进行插入操作,所以不相容性主要发生在GAP类的锁上,包括LOCK_S_GAP、LOCK_X_GAP、LOCK_S_ORDINARY和LOCK_X_ORDINARY;表6.3-2 lock_t结构域类型含义trxtrx_t本lock_t归属的事务trx_locksUT_LIST_NODE_T(lock_t)一个事务可能有多个lock_t结构,trx_locks用于将事务的多个lock_t结构链成链表,便于管理type_modeulint组合标志位:0-3bits:0 LOCK_IS、1 LOCK_IX、2 LOCK_S、3 LOCK_X、4   LOCK_AUTO_INC;4bit:LOCK_TABLE   表锁;5bit:LOCK_REC 记录锁;7bit:LOCK_WAIT   本锁处于阻塞等待状态;8bit:LOCK_GAP;9bit:LOCK_REC_NOT_GAP;10bit:LOCK_INSERT_INTENTION;hashhash_node_t用于构建lock_t结构组成的hash表,方便查找indexdict_index_t记录的索引un_memeberlock_rec_t或者lock_table_t具体的表锁结构或记录锁结构lock_bitmapbyte(var)锁位图图6.3-1 记录锁与记录之间的映射关系 和Oracle不同,MySQL是以独立的锁结构lock_t来管理锁信息的。最便捷的方式是为每个事务的每个记录锁申请独立的锁结构,但这样会引入数量庞大的锁结构,严重消耗内存资源,为此不得不采用多粒度锁机制,并进行复杂的锁升级。MySQL在速度和资源之间做了平衡,以每个事务处理的page为单位申请lock_t结构,即如果同一个事务对同一个page上多条记录加相同类型的锁,那么只需要申请一个lock_t结构。下面首先来看lock_t结构中最重要的lock_rec_t和lock_bitmap。如图6.3-1所示:lock_rec_t:对应于一个page,space和page no用于标识针对具体哪个page,nbits用于表达变长变量lock_bitmap的长度,lock_bitmap的字节数等于1+(nbits/8);lock_bitmap:变长,和page中的记录数强相关,MySQL每条记录的ROW HEADER结构中有一个REC_NEW_HEAP_NO(详细情况请参见“空间管理与数据布局”章节),用于对page内每条记录生成唯一的编号。这样lock_bitmap中的每个bit位对应于page中的一条记录,bit位的位置就对应于记录的REC_NEW_HEAP_NO,该bit位为1就表示对应的记录上有锁;可见,MySQL是按照page为单位组织锁结构的。优点是节约了内存资源,不需要引入复杂的锁升级机制。缺点是判断某条记录上是否有锁的效率相对较低,首先找到该page相关的所有lock_t结构(事务、锁类型和算法不同,同一个page会有多个lock_t),遍历这些lock_t结构,并根据记录的REC_NEW_HEAP_NO检查每一个lock_t结构中的lock_bitmap,以核实该记录上是否有锁。除了lock_rec_t和lock_bitmap之外,lock_t结构中的详细情况如表6.3-2所示,其中重要的成员还有:trx:指向本lock_t归属的事务,由此可得到对应的事务结构;trx_locks:双向链表,同一个事务可能申请多个lock_t结构,通过该指针将同一个事务的lock_t链接在一起;type_mode:锁的状态、类型以及算法等信息;hash:用于构建hash链表,MySQL会组建锁的hash表,方便以page为单位找到对应的lock_t结构;了解了锁的基本结构后,下面来看MySQL是如何组织lock_t的。MySQL中主要有两种情况查询锁:情况1:事务需要知道本事务已经持有了哪些锁,阻塞在哪个锁上;情况2:事务在扫描或修改某个page中的记录时,需要知道该记录上是否有锁,以及锁的类型和算法是什么;首先来看情况1,每个事务都会维护一个trx_lock_t结构,该结构包含如下关键成员:wait_lock:一个指向lock_t结构的指针,指向本事务当前等待的锁结构;trx_locks:类型为UT_LIST_BASE_NODE(lock_t),指向链表的指针,结合每个lock_t中的trx_locks将属于本事务的所有lock_t结构链接在一起,构成一个链表;wait_started:锁等待的开始时间;lock_heap:lock_t结构是动态生成的,维护本事务所有动态锁的内存;可见,通过wait_lock和trx_locks,事务将归属于本事务的所有lock管理起来。一个事务只可能阻塞等待在一个锁上,所以wait_lock只是一个指针。下面来看情况2,全局变量lock_sys会维护一个大的hash表(rec_hash)和因为锁等待而阻塞的线程(waiting_threads)。Rec_hash实际上就是按照space和page no对lock_t进行hash管理的大hash表。其中关键的成员有:array:hash表的桶数组;n_cells:hash表的桶数量,即桶数组的长度;sync_obj:互斥量数组,用于保护并发访问hash表;这样根据space和page no算出具体的hash值,从而得到对应page 所在的桶,即array数组的下标。然后遍历该桶对应的哈希链,即由lock_t结构组成的链表,比较lock_t.lock_rec_t结构中的space和page no,从而找到对应的page。由于存在多个事务对同一个page的不同记录加锁,所以同一个page会有多个lock_t结构,需要遍历这些结构。对于每个lock_t结构,比较记录REC_NEW_HEAP_NO对应的位图,从而判断是否有锁。至于锁的类型和算法,则根据lock_t中的type_mode来判断。图6.3-2 lock_t锁布局 如图6.3-2所示,每个事务维护一个trx_lock_t结构,通过该结构总额trx_locks和wait_lock以及每个lock_t的trx_locks指针,将属于某个事务的所有锁结构链接在一起。同时维护一张rec_hash表,将hash值相同的lock_t结构通过hash指针链接在一起,这就可以查询特定page的锁情况。多个用户线程会并发访问hash表,需要同步机制进行并发保护。考虑到并发性,会有多个mutexes(sync_obj),每个mutexes保护一段bucket数组以及后面的哈希链表,提高并发性。表锁表6.3-3 表锁之间的相容关系 LOCK_ISLOCK_IXLOCK_SLOCK_XLOCK_AILOCK_ISYYYNYLOCK_IXYYNNYLOCK_SYNYNNLOCK_XNNNNNLOCK_AIYYNNN表6.3-4 表锁之间的强度关系(Y表示行的强度大于列,N表示列的强度大于行) LOCK_ISLOCK_IXLOCK_SLOCK_XLOCK_AILOCK_ISYNNNNLOCK_IXYYNNNLOCK_SYNYNNLOCK_XYYYYYLOCK_AINNNNYMySQL的表锁和Oracle比较类似,也是通过多粒度锁解决效率问题,支持如下锁类型:LOCK_IS:意向共享锁;LOCK_IX:意向排它锁;LOCK_S:共享锁;LOCK_X:排它锁;LOCK_AUTO_INC:自增长锁,含有自增长列的表才会加该类型的锁;表锁之间的相容性如表6.3-3所示,之间的强度关系如表6.3-4所示。在实现上,表锁也是一个lock_t结构。和记录锁不同的是lock_t中的um_member不同,um_member是一个union结构。当lock_t为记录锁时,um_member为lock_rec_t结构。当lock_t为表锁时,um_member为lock_table_t结构。Lock_table_t结构中的关键成员有:table:指向dict_table_t类型的指针,表示本表锁归属于哪个表;locks:组成lock_t的链表,用于将归属于同一个表的所有lock_t结构链接在一起;图6.3-3 表锁布局 如图6.3-3所示,每个表在缓存中对应一个字典结构dict_table_t。Dict_table_t结构中的locks以及各个lock_t中的locks(实际上是um_member.lock_table_t.locks)将归属于同一个表的所有lock_t结构管理起来。Dict_table_t结构中的autoinc_lock将该表LOCK_AUTO_INC自增长锁独立出来,避免事务频繁地创建和释放该结构。表锁和记录锁都是lock_t结构,不同的是表锁不需位图结构,直接通过type_mode标识具体的锁类型。当然不管是表锁还是记录锁,从事务的角度来看,都是通过trx_locks和wait_lock进行管理的。聚集索引和辅助索引MySQL是索引组织表,索引又分为聚集索引和辅助索引,其加锁原则为:通过主键进行加锁的场景,仅对聚集索引加锁;通过辅助索引进行加锁的场景,先对辅助索引加锁,再对聚集索引加锁;在加锁的过程中,加锁策略和隔离级别、扫描类型、索引的唯一性等强相关。总的来说,规则如下:如果没有任何索引,需要全表扫描(或者覆盖索引扫描),所有记录全部加锁。RC与RR、Serialiable的区别是只在记录上加锁,不在间隙上加锁。当然MySQL出于性能的目的,对于不满足更改条件的记录会调用unlock_row提前释放锁,一定程度上违反了2PL;如果是非唯一索引,在[index first key, index last key)范围内加记录锁,如果是RR或者Serialiable隔离级别,间隙也需要加锁;如果是唯一索引,在[index first key, index last key)范围内加记录锁,如果是等值查询,即使是RR或者Serialiable隔离级别也不需要加间隙锁,因为唯一性已经保障不会出现幻读;隐式锁与显式锁虽然MySQL以page为粒度组织lock_t结构,以计算换空间(无法直接判断某行记录上是否有锁,需要遍历lock_t中的bitmap),一定程度上节约了内存资源。然而lock_t的量级仍然是事务数*page数*锁类型,锁资源的压力仍然非常大。为了节约锁资源,MySQL实现了一种称为隐式锁的延迟加锁机制。其核心思想是锁是非常消耗资源的,能不加锁就不加锁,只有在发生冲突时再加锁。显式锁是明确的锁,对应于lock_t对象,而隐式锁只是逻辑上的“锁”,没有lock_t对象,需要通过其它规则间接地发现该记录上有锁。如何判断某条记录上是否有隐式锁?对于聚集索引来说比较简单,每条记录上都有该记录的事务id(trx_id),如果该事务id对应的事务仍然是活跃的,那么该记录上有隐式锁,否则没有隐式锁。辅助索引比较复杂,每个page上都有一个PAGE_MAX_TRX_ID(该域在PAGE HEADER结构中,详细情况请参考“空间管理与数据布局”章节),用于表示更新本page的最后一个事务id。如果PAGE_MAX_TRX_ID比最小活跃事务id还要小,说明该page上的所有记录都没有隐式锁,否则需要找到对应的主键记录进行更加复杂的判断。图6.3-4 辅助索引与聚集索引的逻辑关系 如图6.3-4所示,现在需要判断辅助索引current_index_rec上是否有隐式索引,需要通过对应的聚集索引来判断。聚集索引结合undo日志可以构造出历史版本,包括聚集索引的历史版本和辅助索引的历史版本。有了这些历史版本之后,辅助索引上的隐式索引判断规则如下:current_trx不是活跃事务(通过current_cluster_rec中的隐藏事务id获得),current_index_rec上没有隐式锁;current_cluster_rec没有历史记录,表示本条记录是current_trx插入的,所以current_index_rec上有隐式锁;current_index_rec=history1_index_rec,但current_index_rec和history1_index_rec的delete flag不同,表示current_index_rec正在被current_trx删除,所以current_index_rec上有隐式锁;current_index_rec=history1_index_rec,且current_index_rec和history1_index_rec的delete flag相同,修改current_index_rec的事务既可能已经提交了,也有可能没有提交。如果current_trx!=history1_trx表示current_trx没有修改current_index_rec,所以没有隐式锁,否则要进一步判断history2;current_index_rec!=history1_index_rec,且current_index_rec和history1_index_rec的delete flag都为0,表示current_trx修改了current_index_rec,所以current_index_rec上有隐式锁;current_index_rec!=history1_index_rec,且current_index_rec的delete flag为1,修改current_index_rec的事务既可能已经提交了,也有可能没有提交。如果current_trx!=history1_trx表示current_trx没有修改current_index_rec,所以没有隐式锁,否则要进一步判断history2;通过上述规则,MySQL就可以通过比较和计算发现辅助索引上是否有隐式锁。在后继事务的加锁过程中,如果发现某条记录有隐式锁,那么以前事务的名义为该记录申请加显式锁。可见,在隐式锁机制下,只有发生锁冲突时才会加锁,为系统节约了大量资源:如果在原事务提交或回滚前,没有其它事务访问对应的记录,实际上所有的隐式锁都不会被转换为显式锁;如果在原事务提交或回滚前,其它事务访问该记录的某些辅助索引,只有被访问到的辅助索引才会被转换为显式锁,其它辅助索引上隐式锁仍然不会被转换;由于隐式锁只能通过规则和事务id进行判断,无法获取锁模式和锁类型等信息,所以隐式锁有如下限制:隐式锁针对的是记录锁,不可能是间隙或Next-Key类型;INSERT操作只加隐式锁,不加显式锁(包括聚集索引);UPDATE、DELETE在查询时,对查询用到的辅助索引和聚集索引加显式锁,其它二级索引使用隐式锁;记录锁的维护MySQL是以page为单位维护lock_t对象的,而page会随着数据的变化而变化,产生分裂、合并等现象。因此,lock_t对象也要随着page的分裂、合并而分裂、合并。分裂、合并的机制和原理基本一致,而分裂又分为左分裂和右分裂,其原理也是一致的,所以下面以右分裂为例来讲述记录锁的分裂维护。假设某page中的记录为R1、R2、R3、R4、R5、R6、R7,那么可以锁定的范围有:(infimum,R1](R1,R2](R2,R3](R3,R4](R4,R5](R5,R6](R6,R7](R7,supremum)此时page需要进行右分裂,分裂点为记录R4,即记录R4~R7需要迁移到一个新的page中。那么需要生成一个新的lock_t对象(right):left lock_t:(infimum,R1](R1,R2](R2,R3](R3,supremum);right lock_t:(infimum,R4](R4,R5](R5,R6](R6,R7](R7,supremum);Right lock_t的supremum继承于原lock_t对象的supremum,同时left lock_t对象的supremum和right lock_t的infimum需要根据分裂前(R3, R4]进行设置,即(R3,supremum)和(infimum,R4]要等效于分裂前的(R3,R4]。死锁MySQL对死锁采用了主动检测机制,其检测原理就是有向循环图。记录锁的hash组织方式为有向循环图的检测提供了充分必要条件。当某事务在加锁时因为锁冲突要等待,就开始进行深度优先的递归遍历,检测是否存在有向循环图。如果存在循环就表示有死锁,寻找一个undo量最小的事务进行回滚。PostgreSQL设计原理事务PostgreSQL数据库支持ANSI SQL定义的Read Uncommitted、Read Committed、Repeatable Read和Serializable四种隔离级别,默认隔离级别为Read Committed。在各隔离级别下PostgreSQL的并发控制机制分别如下:Read Uncommitted:实际上就是Read Committed;Read Committed:使用一致性读,且总是读取最新提交的快照数据,允许不可重复读和幻读;Repeatable Read:实际上是snapshot isolation,使用一致性读,且在同一个事务中读取的总是相同的历史快照数据,允许写倾斜(write skew);Serializable:实际上是serializable snapshot isolation,使用一致性读,并在snapshot isolation基础上加入SIREAD锁和RW-Conflicts机制,解决写倾斜异常,保证可序列化;可见,PostgreSQL的并发控制机制与Oracle和MySQL有很大的不同,通过snapshot isolation和serializable snapshot isolation机制实现Repeatable Read和Serializable。同时PostgreSQL也采用了锁机制,解决表级冲突以及记录级的写冲突,也支持通过在select语句上指定for update或者for share强制加记录排它或者共享锁。因此,PostgreSQL综合运用了乐观控制和悲观控制方法,以达到最优的并发控制效率。记录锁图6.4-1 Tuple结构 正常情况下PostgreSQL直接在记录上设置标志位就可以完成对记录加记录锁,不需要申请独立的内存锁结构,从而提高内存资源利用率和锁效率。如图6.4-1所示,每条记录都有一个HeadTupleHeaderData(详细情况请参考“数据前像与回滚”章节),该头部包含了如下重要信息:x_min:insert本条记录的事务id;x_max:delete/update本条记录的事务id;t_infomask:大量的组合标志位,通过综合这些标志完成记录锁的设置和判断,具体有HEAP_XMAX_KEYSHR_LOCK、HEAP_XMAX_EXCL_LOCK、HEAP_XMAX_LOCK_ONLY、HEAP_XMAX_COMMITTED、HEAP_XMAX_INVALID、HEAP_XMIN_COMMITTED、HEAP_XMIN_INVALID;图6.4-2 记录判断伪代码FOR each row that will be updated by this UPDATEWHILE TRUEIF (row1 is being updated) THENWAIT for the termination of the transaction that update row1IF (status of the terminated transaction is COMMITTED)AND (this transaction is REPEATABLE READ or SERIALIZABLE) THENABORT this transaction /*first-update-win*/ELSEGOTO step (2)END IFELSE IF(row1 has been updated by another concurrent transaction) THENIF (this transaction is READ COMMITTED) THENUPDATE row1ELSEABORT this transaction /*first-update-win */END IFELSEUPDATE row1 /*row1 is not yet modified or has been updated by a terminated transaction */END IFEND WHILEEND FOR了解了锁记录的标志位之后,我们以update语句为例来看PostgreSQL是如何基于锁进行并发控制的。如图6.4-2所示,判断过程要点如下:Step3:如果记录正在被更新,证明记录上有排它锁,有写写冲突,需要阻塞等待;Step5:当前事务被唤醒后,如果对方事务已经提交且隔离级别为Repeatable Read或者Serielizable,表示对方事务已经修改了当前记录,可能会引起Lost Update异常,当前事务必须强制退出,否则跳转到第2步,重新对本记录进行判断;Step11:如果记录已经被更新,更新该记录的事务已经提交,且该事务与当前事务是并发事务(即当前事务启动时该事务尚未提交)。如果当前事务的隔离级别为Read Committed直接修改该记录,否则强制退出当前事务以防止Lost Update异常;Step12:没有任何冲突,直接修改记录;可见,通过记录上的标志位即可判断出是否有冲突。同时PostgreSQL也支持通过select语句指定for update或者for share提前设置标志,解决频繁强制事务退出的问题。当然上述机制仍然存在一个问题,当存在冲突时,如何有效地对阻塞事务进行排队,这些就需要显式地申请记录锁,详细情况请参考后面的“表锁与记录锁”章节。表锁与记录锁表6.4-1 语句与表锁模式的对应关系模式名称模式id语句场景NoLock0 AccessShare1select RowShare2select for update/ for shareRowExclusive3insert/ update/ deleteShareUpdateExclusive4vaccum(non-full), analyze, create index concurrentlyShare5create index(without concurrently)ShareRowExclusive6任何postgresql命令不会自动获得这种锁Exclsuvie7任何postgresql命令不会自动获得这种锁AccessExclusice8alter table, drop table, vaccum full, unqualified lock table表6.4-2 表锁模式相容矩阵(行为已加锁类型,列为待加锁类型) AccessShareRowShareRowExclusiveShareUpdateExclusiveShareShareRowExclusiveExclusiveAccessExclusiveAccessShareYYYYYYNNRowShareYYYYYYNNRowExclusiveYYYYNNNNShareUpdateExclusiveYYYNNNNNShare      NNShareRowExclusiveYYNN NNNExclusiveYYNNNNNNAccessExclusiveYNNNNNNN和Oracle、MySQL一样,PostgreSQL出于效率考虑表锁也采用了多粒度机制,表锁的模式和相容矩阵如表6.4-1和6.4-2所示,不同的是PostgreSQL的VACCUM机制非常厚重,所以在表锁中需要引入相关的锁模式。在实现层面,不管是表锁还是显式的记录锁,都采用类似的机制,相关的结构分别为LOCKTAG、LOCK、PROCLOCKTAG、PROCLOCK、PGPROC、LOCKLOCKTAG、LOCALLOCK。需要注意的是,记录锁和表锁不同,记录锁只有共享锁和排它锁两种模式。表6.4-3 LOCKTAG结构域长度含义locktag_field14锁对象标识符locktag_field24锁对象标识符locktag_field34锁对象标识符locktag_field42锁对象标识符locktag_type1锁对象类型:LOCKTAG_RELATION:对表加锁,DB OID+RELOID;LOCKTAG_RELATION_EXTEND:对表加锁;LOCKTAG_PAGE:对page加锁,DB OID+RELOID+PageNumber;LOCKTAG_TUPLE:对记录加锁,DB OID+RELOID+PageNumber +OffsetNumber;LOCKTAG_TRANSACTION:TransactionId;LOCKTAG_VIRTUALTRASNACTIONID:VirtualTransactionId;LOCKTAG_SPECULATIVE_TOKEN:TransactionId;LOCKTAG_OBJECT:DB OID + CLASS OID + OBJECT OID + SUBID;LOCKTAG_USERLOCK;LOCKTAG_ADVISORY;locktag_lockmethodid1锁方法id:DEFAULT_LOCKMETHOD;USER_LOCKMETHOD;LOCKTAG用于标识某个具体被锁定的资源对象,locktag_type和locktag_lockmethodid分别用于标识锁定对象的类型和方法。例如,当locktag_type等于LOCKTAG_TUPLE时,表示锁定一条记录,即记录锁,此时locktag_field1等库对象ID,locktag_field2等于表对象ID,locktag_field3等于PageNumber,表示哪个Page,locktag_field4等于OffsetNumber,表示page内记录的偏移。可见,通过4个locktag_field就可以唯一确定一条记录。当然有时不需要设置所有的locktag_field,例如,当locktag_type等于LOCKTAG_TRANSACTION时只需要将locktag_field1设置为xid。图6.4-3 LOCK结构及与PGPROC、PROCLOCK间的关系 LOCK对象表示一个具体的锁对象,例如一个记录锁就是一个LOCK对象,一个表锁也是一个LOCK对象。如图6.4-3所示,LOCK对象详细描述了某个对象资源上的锁信息,具体情况如下:tag:类型为LOCKTAG,唯一地标识被锁定的某个资源对象;grantMask:类型为LOCKMASK,实际上就4个字节,通过bitmap标识已经在该资源对象上加了哪些锁模式,例如,如果第1个bit位设置为1表示已经加上AccessShare锁。通过1<<LockMode可以标识加上多个锁模式;waitMask:类型同grantMask,grantMask表示已经加上的锁模式,而waitMask表示正在等待的锁模式;procLocks:对tag资源对象加锁的进程列表,指向PROCLOCK对象,并通过PROCLOCK对象中的locklink指针将所有和本LOCK对象相关的PROCLOCK对象链接在一起;waitProcs:当锁模式不相容时,相关进程就需要阻塞等待,waitProc指向等待的PGPROC对象,并通过PGPROC对象的links指针将所有阻塞在本LOCK对象的PGPROC对象链接在一起;Requested、nRequested:本LOCK对象上各种锁模式被请求的次数,总次数,MAX_LOCKMODES为当前系统支持的锁模式数量;granted、nGranted:本LOCK对象上各种锁模式已经被授予的次数,总次数;图6.4-4 PGPROC结构 通过LOCK对象及其哈希表可以从资源的角度找到任何锁对象,从而确定该资源上的锁情况,这是第一个维度。然而我们还需要从事务或者进程的角度查看锁的情况,这是第二个维度。在进入第二个维度之前,我们首先来看PGPROC结构。PostgreSQL是多进程设计,每个后台进程在共享内存中都有一个PGPROC对象。如图6.4-4所示,PGPROC对象中与锁强相关的信息如下:links:和LOCK对象中的waitProcs指针相对应,用于将阻塞等待在同一个LOCK对象上的PGPROC链成一个链表;waitLock:指向本进程正在阻塞等待的LOCK对象;waitProcLock:指向本进程正在阻塞等待的PROCLOCK对象;waitLockMode:本进程阻塞等待的锁模式;heldLocks:本进程已经持有的锁模式;myProcLocks:本进程拥有的所有PROCLOCK对象,通过分区数组以及PROCLOCK中的procLink指针,将所有属于本进程的PROCLOCK对象链接在一起;图6.4-5 资源对象与进程之间的关系 表6.4-4 PROCLOCK结构域类型含义tagPROCLOCKTAGPROCLOCK对象标识符holdMaskLOCKMASK当前已经持有的锁模式releaseMaskLOCKMASK可以释放的锁模式lockLinkSHM_QUEUE用于将归属于同一个LOCK对象的所有PROCLOCK链接在一起procLinkSHM_QUEUE用于将归属于同一个PGPROC进程的所有PROCLOCK链接在一起表6.4-5 PROCLOCKTAG结构域类型含义myLockLOCK*指向LOCK对象的指针myProcPGPROC*指向PGPROC对象的指针LOCK对象描述了某个具体资源对象的锁情况,PGPROC对象描述了某个具体进程的锁情况。如图6.4-5所示,某个资源可以被多个进程加锁,某个进程也可以对多个资源加锁,所以LOCK对象和PGPROC对象时多对多的关系。PostgreSQL设计了PROCLOCK对象以维护LOCK对象和PGPROC对象之间的对应关系。每个PROCLOCK对象代表一个LOCK对象和一个PGPROC对象的对应关系。详细情况如表6.4-4和6.4-5所示,其中的关键信息如下:tag:唯一确定一个LOCK对象和PGPROC对象的对应关系;holdMask:该进程在该对象上已经持有的锁模式;releaseMask:该进程在该对象上可以被释放的锁模式;lockLink和procLink:分别按照Lock对象维度和PGPROC对象维度将相关的LOCKPROC对象链接在一起;表6.4-6 LOCALLOCK结构域类型含义tagLOCALLOCKTAGLOCALLOCK对象标识符lockLOCK*指向共享内存中对应的LOCK对象proclockPROCLOCK*指向共享内存中对应的PROCLOCK对象hashcodeuint32LOCKTAG hash值的拷贝nLocksint64该锁被本进程持有的总次数numLockOwnersint相关的lock   owner个数maxLockOwnersintlockOwners数组的大小lockOwnersLOCKLOCALOWNER*动态申请的lock   owner数组表6.4-7 LOCALLOCKTAG结构域类型含义lockLOCKTAG标识对应的LOCK对象modeLOCKMODE锁模式LOCK、LOCKPROC、PGPROC等对象都存放在共享内存中,运行时都访问共享内存,同时还要考虑互斥,代价比较高。为此,PostgreSQL的每个后台进程在本地维护了LOCALLOCK对象,更新LOCK、LOCKPROC、PGPROC等对象时同时更新LOCALLOCK对象。这样在访问锁时,如果LOCALLOCK对象已经满足要求,就可以不用访问共享内存,从而提高效率。例如,对同一个锁多次加锁或者释放只属于某个资源的锁。死锁对于死锁,PostgreSQL采用了事前预防和事后检测相结合的方式,具体包括:当进程加锁冲突时,就会进入等待队列。如果在队列中已有其它进程请求本进程已经持有的锁,为了避免死锁,可以将本进程插入到该进程的前面;当释放锁时,会尝试唤醒等待队列中的进程。如果某进程请求的锁与该进程前序进程的锁不相容,那么该进程不会被唤醒;通过上述方式,在尽量保证先请求先处理的原则下,尽可能规避潜在的死锁。然而,上述方法只是进行了简单的规避,并不能彻底解决死锁,完全解决需要通过有向等待图来解决,但成本较高,PostgreSQL将这一过程放在了事后。图6.4-6 死锁检测的触发过程 如图6.4-6所示,当阻塞等待超时后就开始进行死锁检测。不过PostgreSQL在有向循环图中引入了Soft Edge和Hard Edge的概念:Soft Edge:进程A和进程B都在同一个锁的等待队列中。进程A和进程B的锁请求不相容,且进程A在进程B的后面,这时进程A指向进程B的有向边为Soft Edge;Hard Edge:进程A请求的锁和进程B已经持有的锁冲突,这时进程A指向进程B的有向边为Hard Edge;可见,Soft Edge是可以通过重新排队进行规避的,而Hard Edge已经形成,是无法改变的。有了Soft Edge和Hard Edge概念之后,我们来看看PostgreSQL是如何进行死锁检测的:从每一个点出发,沿着有向循环图的有向边行进,如果能够回到起点,说明存在死锁;在遍历过程中将Soft Edge记录下来,如果存在死锁且没有Soft Edge,直接终止本事务;如果有Soft Edge。对于每个Soft Edge,递归枚举它的所有子集,尝试进行调整。调整方法采用拓扑进行排序,并遍历测试,如果通过测试表明可以规避死锁,直接结束。如果调整任何一个Soft Edge都无法解决死锁,终止本事务;SIREAD锁和RW-Conflicts图6.4-7 写倾斜于依赖图 在Serializable隔离级别下,PostgreSQL可以解决所有异常,其采用的方法并不是读写都加断言锁和记录锁,而是采用SSI策略(详细情况请参考“事务”章节)。如图6.4-7所示,当依赖图(dependency graph)中存在循环,表示存在写倾斜异常,需要强制某个事务退出,从而打破循环,保证可序列化。可见,SSI的重点是标识rw关系和检测依赖图中是否有循环,为此PostgreSQL定义了SIREAD锁和RW-Conflicts两种数据结构。为了构建RW-Conflicts,首先需要表示出哪些事务读取了哪些记录,这就是SIREAD锁的作用。当执行DML语句时,CheckTargetForConflictsOut函数会创建SIREAD锁。例如,当事务txid1读取记录tuple1时会创建SIREAD锁{tuple1, {txid1}},之后事务txid2也读取记录tuple1时该SIREAD锁会更新为{tuple1, {txid1, txid2}}。可见,SIREAD锁是以记录为单位跟踪相关事务。然而在高并发下,SIREAD锁的数量会非常大,严重消耗系统资源。为此,PostgreSQL采用锁升级的机制来缓解资源消耗。SIREAD锁有tuple、page、relation三个层次。如果某个page的所有tuple都创建了SIREAD锁,那么升级为page级,即以page为单位创建SIREAD锁,原来属于该page的tuple级SIREAD锁全部释放。Relation级即表级,原理同page级。RW-Conflicts是一个三元组,由读事务、写事务、记录(元组)组成。例如,事务txid1读取了记录tuple1,之后事务txid2更新了记录tuple1,那么就需要创建一个RW-Conflict,{txid1, txid2, {tuple1}}。在执行insert、update、delete命令时,CheckTargetForConflictsIn函数会检查相关SIREAD锁,从而判断是否存在RW-Conflicts。如果存在,就创建RW-Conflicts。表6.4-8 写倾斜检测示例一时间Tx_A(txid_a)Tx_B(txid_b)SIREAD LocksRW-ConflictsT1start transaction isolation level serializable;start transaction isolation level serializable;  T2select * from t1 where id=2;(1 row returned) L1:{pkey_2,{txid_a}}L2:{tuple_2,{txid_a}} T3 select * from t1 where id=1;(1 row returned)L1:{pkey_2,{txid_a}}L2:{tuple_2,{txid_a}}L3:{pkey_1,{txid_b}}L4:{tuple_1,{txid_b}} T4update t1 set val=”++” where id=1;(1 row updated)  C1:{r=txid_b, w=txid_a, {pkey_1, tuple_1}}T5 update t1 set val=”++” where id=2;(1 row updated) C1:{r=txid_b, w=txid_a, {pkey_1, tuple_1}}C2:{r=txid_a, w=txid_b, {pkey_2, tuple_2}}T6commit;(success)   T7 commit;(failed)  表6.4-9 写倾斜检测示例二时间Tx_A(txid_a)Tx_B(txid_b)T1start transaction isolation level serializable;start transaction isolation level serializable;T2select * from t1 where id=2;(1 row returned) T3 select * from t1 where id=1;(1 row returned)T4update t1 set val=”++” where id=1;(1 row updated) T5commit;(success) T6 update t1 set val=”++” where id=2;(failed)表6.4-10 写倾斜检测示例三时间Tx_A(txid_a)Tx_B(txid_b)T1start transaction isolation level serializable;start transaction isolation level serializable;T2select * from t1 where id=2;(1 row returned) T3 select * from t1 where id=1;(1 row returned)T4update t1 set val=”++” where id=1;(1 row updated) T5 update t1 set val=”++” where id=2;(1 row updated)T6commit;(success) T7 select * from t1;(failed)假设表t1,在id列上有主键索引。表6.4-8给出了写倾斜检测的详细过程,具体如下:T2:事务Tx_A查询id为2的记录,由于涉及主键,所以CheckTargetForConflictsOut创建了两个SIREAD Locks,分别为L1和L2;T3:事务Tx_B查询id为1的记录,由于涉及主键,所以CheckTargetForConflictsOut创建了两个SIREAD Locks,分别为L3和L4;T4:事务Tx_A更新id为1的记录,CheckTargetForConflictsIn检测SIREAD Locks,发现存在RW-Conflicts,所以创建RW-Conflicts C1;T5:事务Tx_B更新id为2的记录,CheckTargetForConflictsIn检测SIREAD Locks,发现存在RW-Conflicts,所以创建RW-Conflicts C2。此时依赖图已经存在循环,即写倾斜已经产生,然而事务Tx_A和Tx_B都没有提交,所以CheckTargetForConflictsIn无法基于“first-committer-win”原则决策让哪个事务失败;T6:事务Tx_A提交,PreCommit_CheckForSerializationFailure函数检查发现Tx_A和Tx_B存在写倾斜,但Tx_B事务仍然处于运行状态,所以事务Tx_A提交成功;T7:事务Tx_B提交,PreCommit_CheckForSerializationFailure函数检查发现Tx_A和Tx_B存在写倾斜,但Tx_A事务已经提交,所以事务Tx_B提交失败;从上述过程我们发现SIREAD Locks和RW-Conflicts不能在事务提交后立刻释放,需要存在一段时间,以确保相关事务的写倾斜检测能够正常进行。另外,并不意味着事务的倾斜异常只会发生在提交阶段。事实上,CheckTargetForConflictsIn和CheckTargetForConflictsOut都会进行依赖图检测,只要存在循环,且有一个事务已经提交,就会立刻让当前事务失败,例如表6.4-9和6.4-10。CockroachDB设计原理设计思路CockroachDB是一种基于乐观机制的分布式数据库,其默认的隔离级别是可序列化快照(SS, Serializable Snapshot)。和PostgreSQL相比,CockroachDB不采用锁机制,而是将SS发挥到极致,其采用的并发控制有如下特征:可序列化:执行结果和某种串行执行的结果是等价的;可恢复:对于一系列并发执行的事务,有些事务执行成功,有些事务异常退出,仍然能够保证系统可恢复至一致性状态。原子性保证单个事务是可恢复的,严格的乐观调度策略保证任何事务的组合执行也是可恢复的;无锁:执行期间不会在资源上加锁。如果某事务和可序列化、乐观调度机制相关冲突,通过强制该事务退出来保证正确性;分布式:系统无集中的授时、协调或者其它服务;可序列化图在序列化理论中,冲突的发生条件是两个不同事务中的操作操作了相同的数据,且至少有一个操作是写操作。满足上述条件时,就可以说第二个操作和第一个操作相冲突。冲突有三种类型:读写冲突(RW):第二个操作覆盖了第一个操作读取的结果;写读冲突(WR):第二个操作读取了第一个操作写的结果;写写冲突(WW):第二个操作覆盖了第一个操作写的结果;图6.5-1 可序列化图示例 对于事务执行的任何历史,通过这些冲突可以建立一个可序列化图。如图6.5-1所示,将所有事务链接在一起的有向图有如下部分组成:事务是图中的节点;当某操作和另外一个事务的操作冲突时,就画一个从被冲突事务到冲突事务的有向边;图6.5-2 循环可序列化图示例,该历史不可序列化 执行历史是可序列化的,当且仅当可序列化图是非循环的。图6.5-2中的示例就是不可序列化的。CockroachDB采用时间排序来保证可序列化图是非循环的,方法如下:每个事务启动时都会赋予一个时间戳,此后该事务中的所有语句都使用此时间戳;每个操作都可以独立地判断自己和其它事务的哪个操作冲突,以及被冲突操作的时间戳是什么;允许操作和拥有更早时间戳的其它操作相冲突,但不允许和拥有更晚时间戳的操作相冲突;由于在时间前进方向上不允许存在冲突,所以可序列化图就不存在循环。下面章节我们将介绍CockroachDB是如何检测和防止这些冲突的。WR冲突与MVCCWR冲突采用多版本来解决。CockroachDB不仅仅存储单值,而是存储了基于时间戳的多个版本值。写操作不会覆盖旧值,而是创建一个带新时间戳的新值。图6.5-3 多版本值读示例 如图6.5-3所示,对某key的读操作将返回比读操作时间戳小的最新版本。因此,在CockroachDB中后继事务不会形成WR冲突,因此读操作不会使用更晚的时间戳。RW冲突与时间戳缓存任何读操作的时间戳都会缓存在时间戳缓存中。通过该缓存我们可以查询某个key最近进行了哪些读操作,以及这些读操作的时间戳是怎样的。所有写操作在对key进行写时都需要查询时间戳缓存。如果返回的时间戳大于写操作的时间戳,表明RW和一个更晚的时间戳相冲突。这是不允许的,必须以一个更晚的时间戳重启写操作所在的事务。时间戳缓存是一个区间缓存,也就是说其存储的是key的范围。如果某读操作读取了某段范围内的所有key(例如扫描),那么扫描的这些key都以范围的形式存在时间戳缓存中。时间戳缓存完全缓存在内存中,采用LRU算法。当缓存大小达到设定的限制后,最老的时间戳条目就会被删除。为了处理不在缓存中的key,需要定义“低水位线”,其等价于所有key的最早时间戳。如果写操作查询的key不在时间戳缓存中,就返回低水位线。WW冲突与只写最新版本写操作尝试写某key时,该key的时间戳比操作本身的时间戳还要新,表明WW和一个更晚的时间戳相冲突。为了高正可序列化,必须以一个更晚的时间戳重启写操作所在的事务。通过时间排序,拒绝任何不满足排序要求的冲突,CockroachDB的SS可以保证执行结果是可序列化的。严格调度与可恢复性通过前面章节介绍的冲突规则可以保证执行历史是可序列化的。另一个问题是如何保证两个满足冲突规则的未提交事务是可恢复的。假设两个事务T1和T2,T1的时间戳小于T2的时间戳。T1写了key“A”,之后T2在T1提交前读取key“A”。该冲突是被时间排序规则所允许的。但T2应该从key“A”中读到哪一个值呢?假设忽略掉T1的未提交数据,读取数据的前一个版本。如果T1和T2都成功提交,这将引起WR冲突,且和时间排序规则相冲突,因此不可序列化;假设读取T1的未提交数据。如果T2提交成功,T1回滚了,这和T1的原子性相冲突(T1回滚了,但仍然对数据库的状态产生了影响);上述两种情况都是不允许的。为了维护调户的可恢复性,在T1提交前T2不可以提交。为此,CockroachDB采取了严格的调度策略处理此场景:读操作和覆盖操作只允许作用在已提交数据上,操作永远不允许在未提交数据上实施。为了实现原子性提交,key上的未提交数据都保存在意向记录中(Intent Record)。如图6.5-4所示,在MVCC存储结构中,key上的意向记录可以很容易地被查到。在并发环境中,意向记录意味着存在一个正在运行的并发事务。图6.5-4 意向记录与MVCC 严格调度存在两种场景:读操作遇到一个时间戳更小的意向记录,或者写操作遇到一个意向记录(不管时间戳的大小)。对于这两种场景,CockroachDB有两种选择:如果第二个事务的时间戳更大,该事务可以等待第一个事务提交或回滚完毕,然后再继续执行自己的操作;强制其中一个事务退出;作为一种乐观的系统(无等待),CockroachDB选择了强制退出其中一个事务。决策将哪个事务退出的过程如下:step1:第二个事务(遇到意向记录的那个事务)读取第一个事务的事务记录(CockroachDB为每个活跃事务维护一条事务记录,以表征该事务的提交状态);step2:如果第一个事务已经提交(意向记录还没有来得及清理),第二事务清理该意向记录,即将意向记录中的值当成正常值来处理;step3:如果第一个事务已经回滚,第二事务删除该意向记录,并将意向记录当成不存在处理;step4:如果第一个事务处于运行态(未提交),固定选择第一个或第二个事务都是不合理的。同时还存在两个事务同时处理对方,对于冲突的两个事务,胜利的一方最好是确定性的。为此,每个事务记录都赋予一个优先级,永远强制退出优先级地的那个事务。如果优先级相等,强制退出时间戳大的事务。新事务启动时获取一个随机的优先级,当事务因为冲突而重启时,其新的优先级等于max(random, [导致本事务重启的哪个事务的优先级]-1),最终事务在重启的过程中优先级会不断提升。采用本方法,未提交事务之间的冲突可以通过强制退出其中一个事务而立刻得到解决。因此,严格调度确保了所有的事务执行历史都是可恢复的。优先级已经在概率上解决了导致异常事务的问题,即被异常打败的事务会不断地重启,且在重启的过程中优先级会不断地上升,最终获得胜利。另外,CockroachDB在所有事务中增加了心跳。在运行过程中,活跃事务需要周期性地更新其事务记录中的心跳时间戳。如果其它事务碰到某事务的记录时,该事务的心跳时间戳超时,那么该事务被认为是异常事务,此时强制异常事务退出而不是比较优先级。VoltDB设计原理传统数据库的成本Micheal Stonebraker等人在开源数据库Shore上进行了各种基准测试,以调研传统数据库中各组件的成本。测试环境为桌面系统,刚开始性能大约为640TPS。之后每次删除系统中的一个特征,并重新进行基准测试,直至仅剩下一个非常薄的查询内核,性能为12700TPS。这个内核是单线程、无锁、无恢复功能的全内存数据库。通过分解发现了4个影响性能的最大组件:Logging:跟踪数据结构的所有变化并记录日志,拖慢了性能。如果可恢复性不是必须的,或者可通过集群中其它节点进行恢复,日志就不是必须的;Lock:两阶段锁产生了相当大的负载,因为所有对数据的访问都要经过Lock Manager这个单点组件;Latch:在多线程数据库中,很多数据结构在被访问前都要先加上Latch,通过单线程机制可以避免这个诉求,并获得可观的性能提升;BufferManager:内存数据库不需要通过缓存池访问数据页,消除了访问每条记录的间接成本;表6.6-1 传统数据库各组件指令数占比组件New OrderPaymentBtree keys16.2%10.1%Logging11.9%17.7%Locking16.3%25.2%Latching14.2%12.6%Buffer manager34.6%29.8%others6.8%4.7%图6.6-1 NewOrder下各组件指令占比 图6.6-1和表6.6-1给出了这些挑战对应的性能变化情况(测试模型为TPC-C下NewOrder事务和Payment事务,统计的是运行该事务的CPU指令数)。可见每个组件都占整个系统的10%~35%指令数(整个系统运行一遍NewOrder事务的指令数为1.73M)。“hand-coded optimizations”代表的是对B树进行一系列优化。“useful work”代表的是处理查询的实际工作,只占总工作的1/60。“buffer manager”下面的方框代码的是移除上面所有组件之后的性能,这时仍然支持事务,指令数只有总体的1/15,不过仍然是实际工作的4倍(两者之间的差距主要源于函数调用栈的深度,以及无法完全消除缓存管理和事务相关的所有代码)。基于上述分析,Micheal Stonebraker在设计VoltDB时,期望通过裁减Buffer Manager、Latch和Lock等组件以获得更高的性能。因此,VoltDB是一款仅支持序列化隔离级别的分布式内存数据库。内存数据库可以降低Buffer Manager的成本,仅支持序列化隔离级别可以降低Latch和Lock的成本。本章重点讨论VoltDB的并发控制是如何避免Lock成本的。图6.6-2 串行执行队列 假设只有单颗CPU和DRAM内存,我们应该设计一个怎样的程序,在单位时间内仅可能多地执行命令。这些命令可以是创建、查询或者更新结构化数据。如图6.6-2所示,解决方案之一就是将命令放在一个队列中。然后执行一个循环,不断地从队列中取命令并执行。显而易见的是此方法可以让单颗CPU充分运转起来,当然有几纳秒的时间周期用于从命令队列中取命令和将响应放入响应队列中。在循环中,CPU执行的任务基本上100%都是实际工作,而不是系统调度、锁控制、缓存控制等和实际工作不相关的工作。在VoltDB中,用户的命令就是SQL执行计划、分布式分片上的执行计划、或者存储过程的调用,循环就对应于单个分片上的命令队列。并发控制VoltDB每次只会运行一个命令,命令之间无并行无重叠,从而提供了序列化的隔离性。在单颗CPU上高饱和地运行应用的实际工作。然而服务器上有多颗CPU,如何让多颗CPU都高饱和地运行起来?首先对数据进行分片,然后在每个分片上维护一个命令队列。这也是大部分分布式NoSQL数据库的设计思路:操作需要制定待操作数据的KEY。VoltDB采用的是一致性哈希分片,用户需要为每个表指定分片列。这些分片列和NoSQL存储的KEY非常类似。根据分片列判断SQL语句或者存储过程涉及哪个分片,然后将其路由到对应分片的命令队列上。集群中多个服务器或者服务器上多颗CPU,都可以通过增加分片的方法让各CPU繁忙起来,每颗CPU独立运行某个分片上的命令队列,各自提供ACID语义。可见:在每个分片上串行地执行查询或者修改命令;命令可以是SQL、存储过程、SQL执行计划的某个片段;每个命令都提供ACID;表数据分布在各个分片上;通过增加分片的方法在多CPU、多服务器上获得扩展性;事务VoltDB将存储过程作为单独的事务来执行,SQL语句作为自动提交的事务来执行。单分片事务是在单个分片上直接执行的事务。单分片事务可以是只读事务,也可以是读写事务,每个单分片事务都完全满足ACID。实际上单分片只读事务的执行过程可以进一步优化,即越过SPI,以负载均衡的方式直接路由到分片的某个副本上。VoltDB的副本间是以同步的方式执行读写事务,所以只读事务即使越过SPI,仍然可以读到前面事务的结果。此优化可以提升只读事务的吞度量,降低只读事务的延时,减轻SPI的工作量。图6.6-3 只读事务在分片的副本间负载均衡 图6.6-3以示例的方式展示了优化的正确性,事务A和事务C为读写事务,事务B为只读事务,且应用的发起顺序为事务B先于事务C而后于事务A。事务B放在任何一个副本的序列化命令队列中都是正确的(不影响其它副本的结果)。VoltDB支持事务在多分片上进行读写操作,这样的是称为多分片事务。SPI为单个分片实施序列化工作,MPI为跨分片事务实施序列化共诺。MPI会和相干分片的SPI交互,以将分解后的命令注入到对应分片的命令队列中。图6.6-4 只读事务在分片的副本间负载均衡 图6.6-4示例了MPI执行多分片事务M的过程。SPI#1将事务M序列化在单分片事务C的后面执行,SPI#2将事务M序列化在单分片事务B的前面执行。从全局来看,事务的执行顺序为C、M、B。为了执行多分片SQL,VoltDB的SQL执行计划生成器会将执行计划分解成多个片段,有些片段在多个分片上分布式执行,有些片段对分布式执行的结果进行汇总。多分片写事务在各分片间采用两阶段提交协议。在Prepare阶段,MPI将执行接话片段分发到各个分片执行。如果这些片段在各分片上执行成功,无约束性冲突,MPI通知所有分片进行提交。各分片不会执行命令队列中的任何其它命令,只到收到提交消息。VoltDB中多分片事务的大部分案例是分布式读,要么是读取记录时不知道分片的取值,要么是进行汇聚分析。对于仅含只读工作的多分片事务,用户可以通过标签显式地表达出来,这样上述分布式过程就可以进行优化:SPI可以将命令发给任何某个副本,而不需要在副本间同步;分片执行完读操作后,可以立刻执行命令队列中的其它命令,而不是阻塞在那里等待提交消息;总结与分析并发控制的原则是在保证正确性的前提下尽可能地提高并发性,为此Oracle、MySQL、PostgreSQL、CockroachDB、VoltDB采用了不同的策略以提高并发性。从并发控制算法的用户友好度和ANSI SQL隔离级别匹配度来看。MySQL支持ANSI SQL定义的四种隔离级别,在任何情况下都会加记录级写锁,避免了Dirty Write异常。在Read Committed级别下通过语句级一致性读解决了Dirty Read异常。在Repeatable Read级别下,通过事务级一致性读以及Next_key写锁解决了Fuzzy Read和Phantom异常,但由于读不加锁,仍然存在Lost Update和Write Skew异常。在Serializable级别下,读写都加Next_key锁,可以解决所有异常。PostgreSQL真正意义上仅支持三种隔离级别(Read Uncommitted实际上就是Read Committed),在任何情况下都会加记录级写锁,避免了Dirty Write异常。在Read Committed级别下通过语句级一致性读解决了Dirty Read异常。在Repeatable Read级别下,通过事务级一致性读以及写锁解决了Lost Update、Fuzzy Read、Phantom异常,但由于读不加锁,Lost Update异常只能采取“First-Update-Win”原则,对用户不友好,而Write Skew异常仍然无法解决。在Serializable级别下,通过SSI算法进一步解决Write Skew异常,但解决的方法是一旦发现潜在的Write Skew,就强制某个事务退出,对用户并不友好。Oracle仅支持Read Committed和Serializable两种隔离级别,在任何情况下都会将记录级写锁,避免了Dirty Write异常。在Read Committed级别下通过语句级一致性读解决了Dirty Read异常。在Serializable级别下,通过事务级一致性读和SCN比较,解决了Lost Update、Fuzzy Read和Phantom异常,读不加锁导致Lost Update只能通过报错来解决,对用户不友好,同时读不加锁导致Write Skew异常无法解决。CockroachDB支持Snapshot Isolation和Serializable Snapshot Isolation隔离级别,通过多版本和时间戳排序达到可序列化要求。然而为了可恢复新采用了严格的调度策略,不管是读操作还是写操作一旦遇到比自己跟到且未提交的时间戳,必须强制一个事务退出,对用户不友好。VoltDB仅支持Serializable隔离级别,所有事务都串行执行,不存在任何异常。可见在ANSI SQL隔离级别匹配度上MySQL最高,然后依次是PostgreSQL、Oracle、CockroachDB和VoltDB。MySQL、PostgreSQL、Oracle都支持用户在select语句上指定加锁,这样即使在低隔离级别上也可以选择性地解决Lost Update和Write Skew异常。从并发控制算法的效率上来看,Oracle没有设计独立的锁结构,仅在记录上通过1个字节的lb表达出锁信息,理论上锁资源是无穷的。Enqueue机制对等待的事务进行排队,并区分拥有者和等待者,进行非常精准的唤醒。MySQL和PostgreSQL的锁机制比较类似,正常情况下通过记录上的标志位进行判断(判断规则比较复杂),一旦出现冲突则转换为显式锁。在显式锁方面,MySQL以page为单位组织锁资源,在空间和时间上做了权衡。PostgreSQL采取了记录、page、表多粒度的方式组织锁资源。在冲突对事务进行排队时,两者相对Oracle都比较粗糙。和MySQL不同的是,PostgreSQL在SSI方面又引入了SIREAD锁和RW-Conflicts,对所有读操作、读写操作都要进行跟踪记录,并进行检索判断,成本非常高。CockroachDB需要对所有记录的读操作维护时间戳,成本较高。当然由于采用的是乐观控制,在低冲突场景下效率相对较高,在中高冲突下由于要频繁地重做,效率是极低的。VoltDB采用的是串行执行策略,效率非常高。但场景首先,需要以存储过程为事务执行单位,减少应用和数据库之间的来回交互,同时负载要有非常好的可切分性,每颗CPU负责一个分片,分片之间无相关性。可见,在效率上Oracle是最高的,MySQL和PostgreSQL相当。CockroachDB和VoltDB引入了新思路,但场景相对受限。Oracle、MySQL、PostgreSQL采用了锁机制,存在死锁的情况,三者都采用有向循环图的检测方法。Oracle认为死锁检测的代价较大,只有在锁等待超时后才会检测死锁。MySQL在发生锁等待时提前进行死锁检测,提前解决死锁问题。PostgreSQL也采用了锁等待超时后进行检测的策略,但在事前和事后都做了一些小的优化,尽可能地避免死锁。PDF版本下载地址:http://blog.itpub.net/69912723/viewspace-2725664/
文章
机器学习/深度学习  ·  SQL  ·  缓存  ·  Oracle  ·  算法  ·  关系型数据库  ·  MySQL  ·  数据库  ·  PostgreSQL  ·  索引
2023-03-27
【数据库设计与实现】第6章:并发控制
并发控制设计原则事务的并发控制首先要保证并发执行的正确性,满足可序列化要求,即并发执行的结果和某种串行执行的结果是一致的,然后在满足正确性的前提下尽可能地获得最高的并发度。当然在某些业务场景下,可以适当牺牲部分正确性(即接受某些异常),从而获得更高的并发性能。并发控制大体分为悲观算法和乐观算法,为了尽可能深入了解各种算法的优缺点,本章在Oracle、MySQL的基础上增加了PostgreSQL、CockroachDB和VoltDB。Oracle、MySQL、PostgreSQL采用了悲观控制策略,同时通过MVCC进一步提高并发性,而PostgreSQL在此基础上实现了Serializable Snapshot Isolation。CockroachDB完全采用了乐观控制,是乐观控制的开源和商业化实现。VoltDB在并发控制策略上做了突破新创新,舍弃了悲观控制和乐观控制,采用了全串行化的执行策略。在设计和实现并发控制时,有如下几点需要考虑:并发控制算法的用户友好度和正确性,与ANSI SQL隔离级别的匹配度;并发控制算法的效率;并发控制算法的死锁策略;Oracle设计原理事务Oracle数据库支持ANSI SQL定义的Read Committed、Serializable隔离级别以及一个自定义的Read Only隔离级别,且默认Read Committed隔离级别。不支持ANSI SQL定义的Read Uncommitted和Repeatable Read隔离级别,主要基于如下考虑:Read Uncommitted:脏读主要有两个作用,其一是读不加锁,降低读操作的成本,以提高并发度。其二是可以读到最新的未提交数据。Oracle采用了多版本设计,读语句天然不会对记录加锁,同时读取最新脏数据的应用场景也比较少,基于上述考虑,Oracle不支持Read Uncommitted隔离级别;Repeatable Read:Repeatable Read和Serializable的主要区别是读断言锁是长期的还是短期的(详细情况请回顾“事务”章节的“Locking”小节),而Oracle在记录上没有设计读锁,所以两者没有区别,因此Oracle只提供了Serializable隔离级别,不支持Repeatable Read隔离级别;Oracle在事务的并发控制上综合运用了锁机制和多版本机制(MVCC),在锁机制中仅在记录上设计了行级写锁,没有设计行级读锁,读导致的相关异常通过多版本机制和用户加锁来解决(select ... for update)。在Read Committed隔离级别下,记录被修改时会在该记录上加上写锁且一直持续到事务提交或回滚,而读取记录则通过多版本机制读取已经提交的最新记录(多版本一致性读的原理,请回顾“数据前像与回滚”章节的“一致性读”小节)。为了能够读到最新的已提交数据,在每次查询语句开始前会获取当前的最新scn,该scn之前的最新已提交记录都可以被读取。这样既可以保证读到最新的已提交事务的数据,又保证了语句执行过程中的一致性。在Serializable隔离级别下,记录被修改时会在该记录上加上写锁且一直持续到事务提交或回滚。多版本机制和Read Committed下有所不同,Read Committed在每条查询语句开始之前都会获取当前最新的scn,而Serializable在事务开始前获取当前最新的scn。整个事务运行期间保持该scn不变,从而解决了不可重复读和幻读异常。然而只有写加锁,读不加锁,从而存在如下异常:Lost Update:r1[x=100]r2[x=100]w2[x=120]c2w1[x=130]c1是“事务”章节中的示例,事务T2对x增加的20将丢失。Oracle是通过报错来解决Lost Update异常的。当事务修改某条记录时,发现该记录的当前提交scn大于本事务的开始scn,说明该记录在本事务运行期间被其它事务修改并提交过,此时已经无法达成可序列化,报“ORA-08177: can’t serialize access for this transaction”错误,本次事务执行失败;Write Skew:r1[x=50]r1[y=50]r2[x=50]r2[y=50]w1[y=-40]w2[x=-40]c1c2是“事务”章节中的示例,需满足x+y为正数的约束,从单个事务T1和T2来看都是满足该约束的,但执行成功后不再满足该约束;解决上述异常的方式是使用select ... for update,即应用通过语句要求数据库在读操作时在记录上加锁(原则上此处加读锁即可,但Oracle没有记录级读锁,所以此处加的仍然是记录级写锁,一定程度上影响并发性),从而解决上述两种异常。实际上,在Read Committed隔离级别下,应该也可以通过select ... for update强制读语句加上写锁以达成可序列化的效果,缺点是降低了并发性。在Read Only隔离级别下,多版本机制和在Serializable隔离级别下是一样的,不同的是Read Only隔离级别下不允许执行DML写语句。Read Only对于分析型等只读场景是非常有意义的,既可以读到一致性的数据,同时又不阻塞正常的写事务。记录锁图6.2-1 记录级锁示意图 在上节我们知道Oracle只有记录级写锁,没有记录级读锁,即完全是通过记录级写锁达成事务的并发控制。图6.2-1给出了Oracle记录级写锁的示意图,在每行记录的头部都有一个字节的lb字段,记录本条记录被ITL中的哪个事务给锁定了。如果某条记录的lb指向ITL中的事务A,且该事务处于活跃态,那么该记录就被事务A锁住了,即事务A在该记录上加了记录级写锁。此处引出了Oracle的一个重要的设计理念,锁就是数据的一部分(占用1个字节),存在于block(data block、index block)中。这样的设计有如下优势:锁资源轻量且无限大:不需要在独立的内存区域中设计锁结构,锁就在数据中,随着block在内存和持久设备中换入换出,锁资源无限大,所以Oracle不需要设计多层次的锁粒度,并根据锁记录的数目在不同锁粒度间升级;易于传输:锁是记录的一部分,可以随着block进行传输,这一点在Oracle RAC中体现得非常明显,当block在数据库实例间传输时锁信息自然也就传输过去了;表锁当我们在做DDL语句时需要对操作的表加表锁,从而防止其他用户同时对该表做DDL操作。在更改表结构时还需要防止此时有其它事务正在更改本表中的记录,为此需要逐行检查本表的记录上是否有锁。如果表中的记录非常多,逐行检查表上记录是否有锁非常消耗资源,可能还涉及block的读入与写出,导致性能进一步恶化。为了解决此问题,可以在表上引入新的锁类型,以表明其所属的行上有锁,这就是意向锁。意向锁指如果对某个节点加意向锁,则说明该节点的下层节点正在被加锁。对任一节点加锁,必须先对上层节点加意向锁。对应到表和记录,对表中的任何记录加记录锁前,必须先对该表加意向锁,然后再对该记录加记录锁。这样DDL对表加锁时,不需要再逐行检查表中每条记录上的锁标志了,直接判断表上是否有意向锁即可,系统效率得以提升。意向锁有如下锁类型:意向共享锁(Intent Share Lock,IS锁):如果对记录加S锁,需要先对表加IS锁,以表示该表的记录准备(意向)加S锁;意向排它锁(Intent Exclusive Lock,IX锁):如果对记录加X锁,需要先对表加IX锁,以表示该表的记录准备(意向)加X锁;表上有基本的S锁和X锁,意向锁又引入了IS锁和IX锁,这样可以组合出新的S+IS、S+IX、X+IS、X+IX四种锁。但实际上只有S+IX有意义,其它三种组合都没有使锁的强度得以增强(即:S+IS=S,X+IS=X,X+IX=X,等于指强度相等)。这样我们引入了一种新的锁类型:共享意向排它锁(Shared Intent Exclusive Lock,SIX锁)。事务对某表加SIX锁,表示该事务要读取整个表(所以要对该表加S锁),同时会更新表中的部分记录(所以要对该表加IX锁)。意向锁封锁的策略:加锁:申请封锁时,应按照自上而下的次序进行;解锁:释放锁时,应按照自下而上的次序进行;可见,数据库表上的锁类型有S、X、IS、IX、SIX五种。Oracle的表锁分别有S、X、RS、RX、SRX,与S、X、IS、IX、SIX一一对应。需要注意的是Oracle在记录上只提供X锁,所以与RS(通过select ... for update语句获得)对应的记录锁也是X锁(该行实际上海没有被修改),这与理论上的IS锁有所区别的。表6.2-1 表锁相容矩阵 SXRSRXSRXSYNYNNXNNNNNRSYNYYYRXNNYYNSRXNNYNN表6.2-2 语句与表锁的对应关系锁锁语句场景NULL1select ... from table_nameRS2select ... from table_name for update(9.2.0.5之前版本)lock table table_name in row share modeRX3insert into table_nameupdate table_namedelete from table_nameselect ... from table_name for update(9.2.0.5及后继版本)lock table table_name in row exclusive modeS4create index ...lock table table_name in share mode外键上没有索引SRX5lock table table_name in share row exclusive mode外键约束定义成on   delete cascadeX6alter table ...drop table ...drop index ...truncate table ...lock table table_name in exclusive mode表6.2-1为Oracle表锁的相容矩阵,Y表示相容,N表示不相容,需要阻塞等待。表6.2-2给出了语句与表锁之间的对应关系示例,锁给出了字符和数值两种表达方式。当Oracle执行select ... for update、insert、update、delete等DML语句时,会在操作的表上自动加上表级RX锁。当执行alter table、drop table等DDL语句时,会在操作的表上自动加上表级X锁。另一方面,应用程序或者操作人员也可以通过lock table语句指定需要获得某种类型的表锁。最后再介绍一下Oracle的breakable parse locks(分析锁)。Oracle会在share pool中缓存分析和优化过的SQL语句和PL/SQL程序,这样再次执行这些相同的SQL或PL/SQL程序时,不必再进行解析、编译、生成执行计划,直接使用缓存的执行计划。缓存的执行计划对所涉数据库表是有依赖的,即当表结构发生变更时,缓存的所涉的执行计划需要及时失效。分析锁就是为了解决及时通知问题的,当缓存执行计划时,会在所涉数据库对象上加上分析锁。该分析锁会一直持有,直到对应的执行计划失效。分析锁不会产生任何阻塞,当表结构发生变更时,会及时通知对缓存的相关执行计划失效。Enqueue在上面章节我们知道Oracle有记录级X锁,有多种模式的表锁。通过这些锁在保证正确性的前提下,提供了最大的事务并发度。但从实现层面来看,我们还有两个关键问题尚未解决:问题1:如何高效地知道某个数据库对象上已经加了锁,加了什么模式的锁;问题2:当发生冲突时如何对事务排队,持有者释放锁时如何及时唤醒阻塞事务并保证公平性;表6.2-3 部分常见enqueue type大类类型场景User enqueuesTXAllocating an ITL entry in order to begin a transaction;Lock held by a transaction to allow other transactions to wait for it;Lock held on a particular row by a transaction to prevent other transactions from modifying it;Lock held on an index during a split to prevent other operations on it;TMSynchronizes accesses to an object;ULLock used by user applications(通过DBMS_LOCK.REQUEST加锁);System enqueuesSTSynchronizes space management activities in dictionary-managed tablespace;CICoordinates cross-instance function invocations;TTSerializes DDL operations on tablespace;USLock held to perform DDL on the undo segment;CFSynchronizes accesses to the controlfile;TCLock held to guarantee uniqueness of a tablespace checkpoint;Lock of setup of a unqiue tablespace checkpoint in null mode;ROCoordinates fast object reuse;Coordinates flushing of multiple object;PSParallel execution server process reservation and synchronization;首先来看问题1,因为锁是加在数据库对象上的,这些对象可以是表、文件、表空间、并行执行的从属进程、重做线程等等,我们将这些对象统一称为资源。为此,Oracle在SGA中设计了enqueue resource数组,数组中的每个元素代表一个资源,数组的总大小可通过参数_enqueue_resources设置(可通过x$ksqrs和v$resources查看enqueue resources)。Enqueue resource中的每个元素就是一个ksqrs结构,ksqrs结构中的关键成员有:enqueue type:标识锁类型(或称为资源类型),Oracle内部的锁类型非常丰富,表6.2-3给出了部分常见的锁类型。各种internal locks都会在system enqueue中对应一种类型,记录锁和表锁属于user enqueue,分别对应于TX和TM类型;enqueue identification(ID1、ID2):用于标识具体的资源,例如当enqueue type等于TM时,identification存放具体哪个表(ID1等于表的object id),当enqueue type等于TX时,identification存放具体哪个事务(ID1高位的2个字节存放undo segment id,ID1低位的2个字节存放transaction table id,ID2存放wrap);link:双向指针,用于将相同状态的ksqrs结构链接在一起,例如处于空闲状态或者在同一个hash桶中;owners:指向双向链表的头部和尾部,该双向链表存放所有已经持有本资源的锁信息;converters:指向双向链表的头部和尾部,该双向链表存放所有已经持有本资源并等待升级到锁强度更高的锁信息;waiters:指向双向链表的头部和尾部,该双向链表存放所有已经等待本资源的锁信息;图6.2-2 Enqueue Free List与Hash Table 如图6.2-2所示,正常情况下单个ksqrs结构未被使用前通过link双向指针串在一起,组成ksqrs的free list。当需要申请一个资源时,从free list上摘一个ksqrs结构下来,填写enqueue type和enqueue identification,并根据enqueue type和enqueue identification计算hash值,从而算出本krqrs结构归属的hash bucket,并将该ksqrs结构加入到算出的hash bucket中的hash chains中(hash chain中的ksqrs也是通过link双向指针链接在一起的)。hash算法越优秀,hash冲突越小,hash chain的长度越短。可见,处于使用状态的ksqrs是通过hash进行管理的,这样可以快速定位某个资源是否已经加锁(enqueue type和enqueue identification可以唯一标识某个特定的资源)。Hash table的长度(即bucket的数量)可以通过参数_enqueue_hash设置。由于多个用户会并发访问enqueue hash table,所以需要对其进行并发访问保护。系统会申请若干个enqueue hash chains latch(parent latch与child latch,详细情况请回顾“同步与互斥”章节),每个enqueue hash chains latch保护一段bucket(实际上是round-roubin方式)以及这些bucket后面的hash chain。Enqueue hash chains latch的数量由参数_enqueue_hash_chain_latches设置,默认值为cpu_count。假设表t1的object id为1234,现在需要对t1加表锁,那么首先需要申请TM资源。申请资源的大致过程如下:step1:查找enqueue hash table中是否已经有表t1的资源(表资源的类型为TM),对TM、1234(id1=object id)、0(id2=0)计算hash值,从而得到对应的bucket(此处假设为bucket12);step2:申请获得bucket12对应的enqueue hash chains latch;step3:成功获得latch后,查找bucket12的hash chain,看是否已经有表t1的TM资源,如果有则表示不需要创建新的t1资源,释放latch直接退出,否则进入下一步;step4:从ksqrs free list上摘下一个ksqrs结构,将enqueue type设置为TM,将enqueue identification设置为id1=1234,id2=0,然后将该ksqrs结构添加到bucket12的hash chain中;step5:至此完成表t1资源的创建,释放latch,并退出;在上述步骤4中,需要从ksqrs free list上摘下一个空闲的ksqrs结构。Ksqrs free list本身也需要同步与互斥保护,在高并发场景下会有大量频繁的申请与释放,此处就会成为瓶颈。为此,Oracle采用了Lazy策略,即释放资源后对应的ksqrs结构并不立刻归还到ksqrs free list中,而是保留一部分空闲ksqrs结构在chain chain上,这样后继可以直接复用,从而提升性能。至此,我们已经完成enqueue resource的介绍。但enqueue resources只是一个容器,只能给出问题1的部分答案,即解决了如何快速找到某个数据对象(资源)的问题,还需要回答问题1提出的锁模式和问题2。为此,我们需要引入另外一个结构“锁”,“锁”是加在资源上的,即附着在某个ksqrs结构上的。图6.2-3 KSQRS结构及锁对应关系 如图6.2-3所示,每个资源都对应一个ksqrs结构,加在该资源上的所有锁都通过ksqrs结构进行排队:Owners:持有者,即该资源的拥有者,每个锁对应一个拥有者,拥有者不会被阻塞,当有多个拥有者时这些拥有者的锁一定是相容的;Converters:转换者,由拥有者转换而来,表示已经拥有低强度的锁,但在申请变更为更高强度锁时和其它拥有者的锁不相容;Waiters:等待者,和拥有者的锁不相容;当拥有者释放锁时,首先唤醒转换者,即将转换者变更为新的拥有者。当拥有者和转换者都为空时,依次唤醒等待者。如果等待者中有多个相邻的锁是相容的,可以同时唤醒成为拥有者,即如果锁4和锁5是相容的,可以同时成为拥有者。有了上述概念之后,我们首先来看表锁的互斥排队过程。表锁对表对象加锁,所以容纳表锁的ksqrs类型为TM。每个表锁是一个ktqdm结构,申请表锁时首先从ktqdm free list中申请一个ktqdm结构(ktqdm free list由dml allocation latch保护),然后将ktqdm结构附着到对应表的ksqrs结构上。ktqdm结构中关键成员有:sid:锁对应的会话(session);lmode:当前已经持有的锁模式;request:当前正在请求的锁模式;ctime:锁已经持有的时长或者等待的时长;表6.2-4 表锁阻塞时序示例TimeSession1(S1)Session2(S2)Session3(S3)Session4(S4)T1lock table t1 in row exclusive mode;Lock table t1 in row exclusive mode;  T2 Lock table t1 in share row exclusive mode;  T3  Lock table t1 in exclusive mode; T4   Lock table t1 in row exclusive mode;T5Commit;   T6 Commit;  T7  Commit; T8   Commit;图6.2-4 表锁阻塞队列示例 如表6.2-4所示,该表展示了一个针对表t1的时序示例,4个会话(s1、s2、s3、s4)同时对表t1加表锁。图6.2-4给出了T4时刻,表t1上的各表锁之间的阻塞情况。详细过程如下:因为都是对表t1加锁,所以相关的ktqdm结构都附着在同一个ksqrs结构上,ksqrs的类型为TM,id1=t1(实际上是表t1的object_id),表示资源为表t1;T1时刻:s1和s2两个会话同时对表t1加row exclusive锁,这两个锁是相容的,所以都在持有者队列中,通过ktqdm结构中的link链成双向链表,lmode=3表示持有的锁模式为row exclusive;T2时刻:会话s2尝试对表t1加SRX(share row exclusive),即将锁的强度从RX升级为SRX。由于s2的SRX与s1的RX是不相容的,所以s2的ktqdm结构从持有者链表中迁移到转换者链表中,lmode=3表示s2已经持有RX锁,request=5表示s2正在申请SRX锁,此时会话s2阻塞;T3、T4时刻:会话s3和s4分别对表t1加X和RX锁,这两个锁要么和持有者的锁不相容,要么和转换者的锁不相容,所以按照申请的顺序加入到等待者链表中,lmode=0表示尚未持有任何锁,request=6/3表示正在申请的锁模式,此时会话s3和s4阻塞;T5时刻:会话s1提交并释放锁,此时s2从转换者链表迁移到持有者链表中,更新(sid=s2, lmode=5, request=0)表示锁升级为SRX,此时会话s2开始运行;T6时刻:会话s2提交并释放锁,此时持有者和转换者链表都为空,从等待者链表中将s3迁移到持有者链表中,并更新(sid=s3, lmode=6, request=0),由于会话s4的RX和会话s3的X不相容,所以会话s4仍然留在等待者链表中,此时会话s3运行,会话s4继续阻塞;T7时刻:会话s3提交并释放锁,会话s4从等待者链表迁移到持有者链表中,开始运行;至此,我们介绍了表锁的整个运行过程,回答了表锁相关的问题1和问题2,即通过ksqrs结构定位到并发阻塞的资源,通过ksqrs的持有者、转换者、等待者三个链表结合ktqdm结构完成排队、阻塞和唤醒。从中我们可以发现如下关键点:一个会话对同一个表不管加多少次表锁,只会占用一个ktqdm结构;转换者链表中的元素优先级高于等待者链表中的元素,因为转换者中的元素已经持有锁,需要让它们尽快运行以尽快释放锁;从等待者链表向持有者链表迁移时,是按照入链的顺序迁移的,即按照申请的顺序迁移的,体现了FIFO的公平性;下面我们开始介绍记录锁。对于问题1,记录锁是很容易解决的,每条记录的头部有lb标志,且记录锁只有X模式,所以记录锁的重点是解决问题2,即对有记录锁冲突的事务如何进行排队。记录锁的排队机制和表锁的排队机制是类似的,主要区别如下:仍然通过ksqrs enqueue排队,但ksqrs的type为TX,id1和id2等于事务id,即每个事务开启第一次写时会申请一个标识本事的TX类型的ksqrs结构,后继因为和本事务记录锁发生冲突的会话全部附着在该ksqrs结构上;不需要像表锁为每个锁申请一个kstqdm,只需要为每个冲突的事务申请一个ktcxb结构;表6.2-5 记录锁阻塞时序示例TimeSession1(S1)Session2(S2)T1--transaction id=10.1.145Update t1 set c1=1;100 rows updated. T2 --transaction id=10.2.23Update t2 set c1=2;100 rows updated.T3 Update t1 set c1=2 where c2=3;T4Commit; T5 1 rows updated.T6 Commit;图6.2-5 记录锁阻塞队列示例 如表6.2-5所示,该表展示了两个事务(10.1.145和10.2.23)同时修改表t1和表t2中记录的情况,因为同时修改t1中的记录而发生记录锁冲突。图6.2-5展示了在T3时刻TX排队情况。详细过程如下:T1时刻:会话s1开启一个写事务(第一条语句就是更新表t1的全表记录),申请一个ksqrs结构,类型为TX,id1=655361(10*65536+1),id2=145,并加入到ksqrs enqueue hash表中。同时申请一个ktcxb结构,设置sid=s1,lmode=6(记录锁只能是X模式),request=0,并将标识本事务的ktxcb附着在该ksqrs的持有者链表上;T2时刻:会话s2开启一个写事务(第一条语句就是更新表t2的全表记录),申请一个ksqrs结构,类型为TX,id1=655362(10*65536+2),id2=23,并加入到ksqrs enqueue hash表中。同时申请一个ktcxb结构,设置sid=s2,lmode=6,request=0,并将标识本事务的ktxcb附着在该ksqrs的持有者链表上;T3时刻:会话s2更新t1表的单条记录,通过检查该记录的记录头,发现该记录已经被事务10.1.145锁住,申请一个ktcxb结构,设置sid=s2,lmode=0,request=6,并将标识本会话的ktcxb附着在事务10.1.145的ksqrs的等待者链表上,以等待事务10.1.145释放该记录锁,此时会话s2阻塞;T4时刻:事务10.1.145提交,唤醒ksqrs(TX,id1=655361,id2=145)中等待者链表中的所有会话,然后释放ksqrs结构(TX,id1=655361,id2=145)和附着在该ksqrs上的ktcxb结构,此时s2会话激活,继续执行对表t1的记录更新;至此,我们介绍了记录锁的整个运作过程,回答了记录锁相关的问题1和问题2,即在记录头部发现记录冲突,在通过ksqrs的持有者、等待者链表结合ktcxb完成排队、阻塞和激活。从中我们还可以发现如下关键点:每个写事务都会申请一个ksqrs(类型为TX)结构,并持有到事务结束,可见事务本身一种资源;每个写事务都会申请一个或两个ktxcb结构,可见ktcxb结构的数量和修改的记录数无关,只可冲突的事务数相关;所有和事务A有记录冲突的事务都会申请一个ktxcb结构,并将这些ktxcb结构附着在事务A的ksqrs的等待者链表中;TX事务锁除了用于记录锁的排队之外,还用于ITL Entry Shortage时事务的排队。当事务修改block中的数据时,首先需要在该block中占用一个ITL Entry。如果ITL Entry已经被用满,且无法动态扩展ITL时,本事务就需要阻塞等待。此时为本事务申请一个ktxcb结构,然后在本block的ITL中随机选择一个活跃事务,将ktxcb结构附着在该活跃事务的ksqrs结构的等待者链表上。这样当该活跃事务提交时,其占用的ITL Entry就会空出来,唤醒本事务复用该ITL Entry。实际上,Oracle不仅仅将enqueue机制应用于表锁和记录锁,而是将enqueue机制通用化,当系统资源冲突或者不足时都采用enqueue机制进行排队。enqueue机制通用化时,都是通过ksqrs进行排队,只是enqueue type不同。同时不同的资源,用于排队的结构也不同,ktqdm用于表锁,ktcxb用于事务锁,ksqeq、kdnssf、ktatrfil、ktatrfsl、ktatl、ktstusc、ktstusg、ktstuss等等都是用于各种internal locks。不过不管是表锁、事务锁,还是各种internal locks,最终都是通过_enqueue_locks参数设置总lock的数量。enqueue采用数组结构,同时又通过双向指针对数组中的结构进行分类管理。对于大小和属性相同的对象,Oracle一般采用数组这种数据结构进行管理。数组是采用分段方式进行分配和管理的,即Oracle初始只会分配一个容纳固定数量数据单元的内存块,然后在运行过程中动态分配更多的内存块。例如,x$ksqrs数组初始会申请一个较大的内存块,后继不够时再每次申请可容纳32个ksqrs结构的内存块,以此进行动态扩容。死锁Oracle的latch是通过对latch设置level属性在事前规避死锁,而lock的申请顺序和用户语句的执行时序强相关,无法通过事前规定lock的顺序来规避。因此,Oracle采用了事后检测的方法来解决死锁。当会话因为锁等待达到3秒后会醒来,这时会检查等待关系。如果存在循环等待表示存在死锁,否则进行下一个3秒周期的等待。如果检查发现存在死锁,就会触发ORA-60 deadlock detected错误,让应用参与决策。由于是事后超时检查死锁,所以一般是等待时间长的事务先报错。MySQL设计原理事务MySQL数据库支持ANSI SQL定义的Read Uncommitted、Read Committed、Repeatable Read、Serializable四种隔离级别,默认隔离级别为Repeatable Read。MySQL采用的是索引组织表,表中的记录时按照索引键或主键存放的,这就为加断言锁提供了基础。实际上,MySQL就是通过间隙锁锁住记录之间的间隙,从而达到断言锁的目的,防止幻读。各隔离级别下,MySQL的并发控制机制如下:Read Uncommitted:不使用一致性读,允许读取未提交事务的记录,因此会有脏读。只有更改记录或者用户强制lock read才会加锁,且只对记录加record_lock,不会间隙加锁;Read Committed:使用一致性读,且总是读取最新提交的快照数据,允许不可重复读和幻读,在外键检查时对间隙加锁,其它情况只对记录加锁;Repeatable Read:使用一致性读,且在同一个事务中读取的总是相同的历史快照数据,在更改记录或者用户强制lock read时对记录和间隙加锁,这样避免不可重读和幻读(在某些情况下可以只对记录加锁,如唯一索引等);Serializable:不使用一致性读,所有更改和读取操作都会加锁,加锁机制和可重复读一致;可见,MySQL的并发控制机制与“事务”章节介绍的Locking理论是最接近的,同时在Read Committed、Repeatable Read隔离级别下采用了一致性读机制(详细情况请参加“前像数据与回滚”章节),读不加锁,从而最大化地提高并发度。当然在Read Committed、Repeatable Read隔离级别下也可以通过lock read(select ... lock in share mode加共享锁,select ... for update加排它锁)主动对记录加锁,从而在较低隔离级别下也可以解决lost update、write skew等问题。记录锁表6.3-1 记录锁相容矩阵(行为已加锁类型,列为待加锁类型) LOCK_S_GAPLOCK_S_REC_NOT_GAPLOCK_S_ORDINARYLOCK_S_INSERT_INTENTIONLOCK_X_GAPLOCK_X_REC_NOT_GAPLOCK_X_ORDINARYLOCK_X_INSERT_INTENTIONLOCK_S_GAPYYYYYYYYLOCK_S_REC_NOT_GAPYYYYYNNYLOCK_S_ORDINARYYYYYYNNYLOCK_S_INSERT_INTENTIONYYYYNYNYLOCK_X_GAPYYYYYYYYLOCK_X_REC_NOT_GAPYNNYYNNYLOCK_X_ORDINARYYNNYYNNYLOCK_X_INSERT_INTENTIONNYNYNYNY记录锁类型包括共享锁(LOCK_S)和排它锁(LOCK_X)两种类型。MySQL支持对间隙加锁,所以有如下不同的锁算法:LOCK_GAP:间隙锁,仅对间隙加锁,锁住前一条记录和本条记录之间的间隙,但不包括本条记录和前一条记录本身;LOCK_REC_NOT_GAP:记录锁,仅锁住本条记录;LOCK_ORDINARY:Next_Key锁,是LOCK_GAP和LOCK_REC_NOT_GAP的组合,锁住本条记录以及本条记录和前一条记录之间的间隙,但不包括前一条记录;LOCK_INSERT_INTENTION:插入意向锁是一种特殊的间隙锁类型,又称为插入意向间隙锁(insertion intention gap lock),这种锁在插入操作执行前产生。假设已经存在两个索引值4和7,两个事务分别插入记录5和6,每个事务在插入数据前都能在(4, 7)中获得一个插入意向间隙锁,并且由于这两个事务插入的记录不相等而不会互相阻塞。但是,如果间隙(4, 7)之前已经被其它事务加上间隙锁,插入意向间隙锁就会被阻塞,从而防止前事务幻读;可见,MySQL支持两种锁类型,四种锁算法,这样共计可以组合出八种不同的锁,具体相容关系如表6.3-1所示,并从中可以发现如下规律:不管是哪种锁算法,共享锁与共享锁之间都是相容的,即LOCK_S_*和LOCK_S_*是相容的;不管已经持有的锁是哪种类型和算法,待加的LOCK_S_GAP和LOCK_X_GAP都是相容的,即GAP锁(不含插入意向锁)和所有已经持有的锁都是相容的,因为GAP锁主要用于防止将来其它事务的插入操作(避免幻读);LOCK_S_REC_NOT_GAP、LOCK_S_ORDINARY、LOCK_X_REC_NOT_GAP、LOCK_X_ORDINARY之间的不相容主要发生在记录本身的共享与排它、排它与排它的不相容;LOCK_S_INSERT_INTENTION和LOCK_X_INSERT_INTENTION表示即将进行插入操作,所以不相容性主要发生在GAP类的锁上,包括LOCK_S_GAP、LOCK_X_GAP、LOCK_S_ORDINARY和LOCK_X_ORDINARY;表6.3-2 lock_t结构域类型含义trxtrx_t本lock_t归属的事务trx_locksUT_LIST_NODE_T(lock_t)一个事务可能有多个lock_t结构,trx_locks用于将事务的多个lock_t结构链成链表,便于管理type_modeulint组合标志位:0-3bits:0 LOCK_IS、1 LOCK_IX、2 LOCK_S、3 LOCK_X、4   LOCK_AUTO_INC;4bit:LOCK_TABLE   表锁;5bit:LOCK_REC 记录锁;7bit:LOCK_WAIT   本锁处于阻塞等待状态;8bit:LOCK_GAP;9bit:LOCK_REC_NOT_GAP;10bit:LOCK_INSERT_INTENTION;hashhash_node_t用于构建lock_t结构组成的hash表,方便查找indexdict_index_t记录的索引un_memeberlock_rec_t或者lock_table_t具体的表锁结构或记录锁结构lock_bitmapbyte(var)锁位图图6.3-1 记录锁与记录之间的映射关系 和Oracle不同,MySQL是以独立的锁结构lock_t来管理锁信息的。最便捷的方式是为每个事务的每个记录锁申请独立的锁结构,但这样会引入数量庞大的锁结构,严重消耗内存资源,为此不得不采用多粒度锁机制,并进行复杂的锁升级。MySQL在速度和资源之间做了平衡,以每个事务处理的page为单位申请lock_t结构,即如果同一个事务对同一个page上多条记录加相同类型的锁,那么只需要申请一个lock_t结构。下面首先来看lock_t结构中最重要的lock_rec_t和lock_bitmap。如图6.3-1所示:lock_rec_t:对应于一个page,space和page no用于标识针对具体哪个page,nbits用于表达变长变量lock_bitmap的长度,lock_bitmap的字节数等于1+(nbits/8);lock_bitmap:变长,和page中的记录数强相关,MySQL每条记录的ROW HEADER结构中有一个REC_NEW_HEAP_NO(详细情况请参见“空间管理与数据布局”章节),用于对page内每条记录生成唯一的编号。这样lock_bitmap中的每个bit位对应于page中的一条记录,bit位的位置就对应于记录的REC_NEW_HEAP_NO,该bit位为1就表示对应的记录上有锁;可见,MySQL是按照page为单位组织锁结构的。优点是节约了内存资源,不需要引入复杂的锁升级机制。缺点是判断某条记录上是否有锁的效率相对较低,首先找到该page相关的所有lock_t结构(事务、锁类型和算法不同,同一个page会有多个lock_t),遍历这些lock_t结构,并根据记录的REC_NEW_HEAP_NO检查每一个lock_t结构中的lock_bitmap,以核实该记录上是否有锁。除了lock_rec_t和lock_bitmap之外,lock_t结构中的详细情况如表6.3-2所示,其中重要的成员还有:trx:指向本lock_t归属的事务,由此可得到对应的事务结构;trx_locks:双向链表,同一个事务可能申请多个lock_t结构,通过该指针将同一个事务的lock_t链接在一起;type_mode:锁的状态、类型以及算法等信息;hash:用于构建hash链表,MySQL会组建锁的hash表,方便以page为单位找到对应的lock_t结构;了解了锁的基本结构后,下面来看MySQL是如何组织lock_t的。MySQL中主要有两种情况查询锁:情况1:事务需要知道本事务已经持有了哪些锁,阻塞在哪个锁上;情况2:事务在扫描或修改某个page中的记录时,需要知道该记录上是否有锁,以及锁的类型和算法是什么;首先来看情况1,每个事务都会维护一个trx_lock_t结构,该结构包含如下关键成员:wait_lock:一个指向lock_t结构的指针,指向本事务当前等待的锁结构;trx_locks:类型为UT_LIST_BASE_NODE(lock_t),指向链表的指针,结合每个lock_t中的trx_locks将属于本事务的所有lock_t结构链接在一起,构成一个链表;wait_started:锁等待的开始时间;lock_heap:lock_t结构是动态生成的,维护本事务所有动态锁的内存;可见,通过wait_lock和trx_locks,事务将归属于本事务的所有lock管理起来。一个事务只可能阻塞等待在一个锁上,所以wait_lock只是一个指针。下面来看情况2,全局变量lock_sys会维护一个大的hash表(rec_hash)和因为锁等待而阻塞的线程(waiting_threads)。Rec_hash实际上就是按照space和page no对lock_t进行hash管理的大hash表。其中关键的成员有:array:hash表的桶数组;n_cells:hash表的桶数量,即桶数组的长度;sync_obj:互斥量数组,用于保护并发访问hash表;这样根据space和page no算出具体的hash值,从而得到对应page 所在的桶,即array数组的下标。然后遍历该桶对应的哈希链,即由lock_t结构组成的链表,比较lock_t.lock_rec_t结构中的space和page no,从而找到对应的page。由于存在多个事务对同一个page的不同记录加锁,所以同一个page会有多个lock_t结构,需要遍历这些结构。对于每个lock_t结构,比较记录REC_NEW_HEAP_NO对应的位图,从而判断是否有锁。至于锁的类型和算法,则根据lock_t中的type_mode来判断。图6.3-2 lock_t锁布局 如图6.3-2所示,每个事务维护一个trx_lock_t结构,通过该结构总额trx_locks和wait_lock以及每个lock_t的trx_locks指针,将属于某个事务的所有锁结构链接在一起。同时维护一张rec_hash表,将hash值相同的lock_t结构通过hash指针链接在一起,这就可以查询特定page的锁情况。多个用户线程会并发访问hash表,需要同步机制进行并发保护。考虑到并发性,会有多个mutexes(sync_obj),每个mutexes保护一段bucket数组以及后面的哈希链表,提高并发性。表锁表6.3-3 表锁之间的相容关系 LOCK_ISLOCK_IXLOCK_SLOCK_XLOCK_AILOCK_ISYYYNYLOCK_IXYYNNYLOCK_SYNYNNLOCK_XNNNNNLOCK_AIYYNNN表6.3-4 表锁之间的强度关系(Y表示行的强度大于列,N表示列的强度大于行) LOCK_ISLOCK_IXLOCK_SLOCK_XLOCK_AILOCK_ISYNNNNLOCK_IXYYNNNLOCK_SYNYNNLOCK_XYYYYYLOCK_AINNNNYMySQL的表锁和Oracle比较类似,也是通过多粒度锁解决效率问题,支持如下锁类型:LOCK_IS:意向共享锁;LOCK_IX:意向排它锁;LOCK_S:共享锁;LOCK_X:排它锁;LOCK_AUTO_INC:自增长锁,含有自增长列的表才会加该类型的锁;表锁之间的相容性如表6.3-3所示,之间的强度关系如表6.3-4所示。在实现上,表锁也是一个lock_t结构。和记录锁不同的是lock_t中的um_member不同,um_member是一个union结构。当lock_t为记录锁时,um_member为lock_rec_t结构。当lock_t为表锁时,um_member为lock_table_t结构。Lock_table_t结构中的关键成员有:table:指向dict_table_t类型的指针,表示本表锁归属于哪个表;locks:组成lock_t的链表,用于将归属于同一个表的所有lock_t结构链接在一起;图6.3-3 表锁布局 如图6.3-3所示,每个表在缓存中对应一个字典结构dict_table_t。Dict_table_t结构中的locks以及各个lock_t中的locks(实际上是um_member.lock_table_t.locks)将归属于同一个表的所有lock_t结构管理起来。Dict_table_t结构中的autoinc_lock将该表LOCK_AUTO_INC自增长锁独立出来,避免事务频繁地创建和释放该结构。表锁和记录锁都是lock_t结构,不同的是表锁不需位图结构,直接通过type_mode标识具体的锁类型。当然不管是表锁还是记录锁,从事务的角度来看,都是通过trx_locks和wait_lock进行管理的。聚集索引和辅助索引MySQL是索引组织表,索引又分为聚集索引和辅助索引,其加锁原则为:通过主键进行加锁的场景,仅对聚集索引加锁;通过辅助索引进行加锁的场景,先对辅助索引加锁,再对聚集索引加锁;在加锁的过程中,加锁策略和隔离级别、扫描类型、索引的唯一性等强相关。总的来说,规则如下:如果没有任何索引,需要全表扫描(或者覆盖索引扫描),所有记录全部加锁。RC与RR、Serialiable的区别是只在记录上加锁,不在间隙上加锁。当然MySQL出于性能的目的,对于不满足更改条件的记录会调用unlock_row提前释放锁,一定程度上违反了2PL;如果是非唯一索引,在[index first key, index last key)范围内加记录锁,如果是RR或者Serialiable隔离级别,间隙也需要加锁;如果是唯一索引,在[index first key, index last key)范围内加记录锁,如果是等值查询,即使是RR或者Serialiable隔离级别也不需要加间隙锁,因为唯一性已经保障不会出现幻读;隐式锁与显式锁虽然MySQL以page为粒度组织lock_t结构,以计算换空间(无法直接判断某行记录上是否有锁,需要遍历lock_t中的bitmap),一定程度上节约了内存资源。然而lock_t的量级仍然是事务数*page数*锁类型,锁资源的压力仍然非常大。为了节约锁资源,MySQL实现了一种称为隐式锁的延迟加锁机制。其核心思想是锁是非常消耗资源的,能不加锁就不加锁,只有在发生冲突时再加锁。显式锁是明确的锁,对应于lock_t对象,而隐式锁只是逻辑上的“锁”,没有lock_t对象,需要通过其它规则间接地发现该记录上有锁。如何判断某条记录上是否有隐式锁?对于聚集索引来说比较简单,每条记录上都有该记录的事务id(trx_id),如果该事务id对应的事务仍然是活跃的,那么该记录上有隐式锁,否则没有隐式锁。辅助索引比较复杂,每个page上都有一个PAGE_MAX_TRX_ID(该域在PAGE HEADER结构中,详细情况请参考“空间管理与数据布局”章节),用于表示更新本page的最后一个事务id。如果PAGE_MAX_TRX_ID比最小活跃事务id还要小,说明该page上的所有记录都没有隐式锁,否则需要找到对应的主键记录进行更加复杂的判断。图6.3-4 辅助索引与聚集索引的逻辑关系 如图6.3-4所示,现在需要判断辅助索引current_index_rec上是否有隐式索引,需要通过对应的聚集索引来判断。聚集索引结合undo日志可以构造出历史版本,包括聚集索引的历史版本和辅助索引的历史版本。有了这些历史版本之后,辅助索引上的隐式索引判断规则如下:current_trx不是活跃事务(通过current_cluster_rec中的隐藏事务id获得),current_index_rec上没有隐式锁;current_cluster_rec没有历史记录,表示本条记录是current_trx插入的,所以current_index_rec上有隐式锁;current_index_rec=history1_index_rec,但current_index_rec和history1_index_rec的delete flag不同,表示current_index_rec正在被current_trx删除,所以current_index_rec上有隐式锁;current_index_rec=history1_index_rec,且current_index_rec和history1_index_rec的delete flag相同,修改current_index_rec的事务既可能已经提交了,也有可能没有提交。如果current_trx!=history1_trx表示current_trx没有修改current_index_rec,所以没有隐式锁,否则要进一步判断history2;current_index_rec!=history1_index_rec,且current_index_rec和history1_index_rec的delete flag都为0,表示current_trx修改了current_index_rec,所以current_index_rec上有隐式锁;current_index_rec!=history1_index_rec,且current_index_rec的delete flag为1,修改current_index_rec的事务既可能已经提交了,也有可能没有提交。如果current_trx!=history1_trx表示current_trx没有修改current_index_rec,所以没有隐式锁,否则要进一步判断history2;通过上述规则,MySQL就可以通过比较和计算发现辅助索引上是否有隐式锁。在后继事务的加锁过程中,如果发现某条记录有隐式锁,那么以前事务的名义为该记录申请加显式锁。可见,在隐式锁机制下,只有发生锁冲突时才会加锁,为系统节约了大量资源:如果在原事务提交或回滚前,没有其它事务访问对应的记录,实际上所有的隐式锁都不会被转换为显式锁;如果在原事务提交或回滚前,其它事务访问该记录的某些辅助索引,只有被访问到的辅助索引才会被转换为显式锁,其它辅助索引上隐式锁仍然不会被转换;由于隐式锁只能通过规则和事务id进行判断,无法获取锁模式和锁类型等信息,所以隐式锁有如下限制:隐式锁针对的是记录锁,不可能是间隙或Next-Key类型;INSERT操作只加隐式锁,不加显式锁(包括聚集索引);UPDATE、DELETE在查询时,对查询用到的辅助索引和聚集索引加显式锁,其它二级索引使用隐式锁;记录锁的维护MySQL是以page为单位维护lock_t对象的,而page会随着数据的变化而变化,产生分裂、合并等现象。因此,lock_t对象也要随着page的分裂、合并而分裂、合并。分裂、合并的机制和原理基本一致,而分裂又分为左分裂和右分裂,其原理也是一致的,所以下面以右分裂为例来讲述记录锁的分裂维护。假设某page中的记录为R1、R2、R3、R4、R5、R6、R7,那么可以锁定的范围有:(infimum,R1](R1,R2](R2,R3](R3,R4](R4,R5](R5,R6](R6,R7](R7,supremum)此时page需要进行右分裂,分裂点为记录R4,即记录R4~R7需要迁移到一个新的page中。那么需要生成一个新的lock_t对象(right):left lock_t:(infimum,R1](R1,R2](R2,R3](R3,supremum);right lock_t:(infimum,R4](R4,R5](R5,R6](R6,R7](R7,supremum);Right lock_t的supremum继承于原lock_t对象的supremum,同时left lock_t对象的supremum和right lock_t的infimum需要根据分裂前(R3, R4]进行设置,即(R3,supremum)和(infimum,R4]要等效于分裂前的(R3,R4]。死锁MySQL对死锁采用了主动检测机制,其检测原理就是有向循环图。记录锁的hash组织方式为有向循环图的检测提供了充分必要条件。当某事务在加锁时因为锁冲突要等待,就开始进行深度优先的递归遍历,检测是否存在有向循环图。如果存在循环就表示有死锁,寻找一个undo量最小的事务进行回滚。PostgreSQL设计原理事务PostgreSQL数据库支持ANSI SQL定义的Read Uncommitted、Read Committed、Repeatable Read和Serializable四种隔离级别,默认隔离级别为Read Committed。在各隔离级别下PostgreSQL的并发控制机制分别如下:Read Uncommitted:实际上就是Read Committed;Read Committed:使用一致性读,且总是读取最新提交的快照数据,允许不可重复读和幻读;Repeatable Read:实际上是snapshot isolation,使用一致性读,且在同一个事务中读取的总是相同的历史快照数据,允许写倾斜(write skew);Serializable:实际上是serializable snapshot isolation,使用一致性读,并在snapshot isolation基础上加入SIREAD锁和RW-Conflicts机制,解决写倾斜异常,保证可序列化;可见,PostgreSQL的并发控制机制与Oracle和MySQL有很大的不同,通过snapshot isolation和serializable snapshot isolation机制实现Repeatable Read和Serializable。同时PostgreSQL也采用了锁机制,解决表级冲突以及记录级的写冲突,也支持通过在select语句上指定for update或者for share强制加记录排它或者共享锁。因此,PostgreSQL综合运用了乐观控制和悲观控制方法,以达到最优的并发控制效率。记录锁图6.4-1 Tuple结构 正常情况下PostgreSQL直接在记录上设置标志位就可以完成对记录加记录锁,不需要申请独立的内存锁结构,从而提高内存资源利用率和锁效率。如图6.4-1所示,每条记录都有一个HeadTupleHeaderData(详细情况请参考“数据前像与回滚”章节),该头部包含了如下重要信息:x_min:insert本条记录的事务id;x_max:delete/update本条记录的事务id;t_infomask:大量的组合标志位,通过综合这些标志完成记录锁的设置和判断,具体有HEAP_XMAX_KEYSHR_LOCK、HEAP_XMAX_EXCL_LOCK、HEAP_XMAX_LOCK_ONLY、HEAP_XMAX_COMMITTED、HEAP_XMAX_INVALID、HEAP_XMIN_COMMITTED、HEAP_XMIN_INVALID;图6.4-2 记录判断伪代码FOR each row that will be updated by this UPDATEWHILE TRUEIF (row1 is being updated) THENWAIT for the termination of the transaction that update row1IF (status of the terminated transaction is COMMITTED)AND (this transaction is REPEATABLE READ or SERIALIZABLE) THENABORT this transaction /*first-update-win*/ELSEGOTO step (2)END IFELSE IF(row1 has been updated by another concurrent transaction) THENIF (this transaction is READ COMMITTED) THENUPDATE row1ELSEABORT this transaction /*first-update-win */END IFELSEUPDATE row1 /*row1 is not yet modified or has been updated by a terminated transaction */END IFEND WHILEEND FOR了解了锁记录的标志位之后,我们以update语句为例来看PostgreSQL是如何基于锁进行并发控制的。如图6.4-2所示,判断过程要点如下:Step3:如果记录正在被更新,证明记录上有排它锁,有写写冲突,需要阻塞等待;Step5:当前事务被唤醒后,如果对方事务已经提交且隔离级别为Repeatable Read或者Serielizable,表示对方事务已经修改了当前记录,可能会引起Lost Update异常,当前事务必须强制退出,否则跳转到第2步,重新对本记录进行判断;Step11:如果记录已经被更新,更新该记录的事务已经提交,且该事务与当前事务是并发事务(即当前事务启动时该事务尚未提交)。如果当前事务的隔离级别为Read Committed直接修改该记录,否则强制退出当前事务以防止Lost Update异常;Step12:没有任何冲突,直接修改记录;可见,通过记录上的标志位即可判断出是否有冲突。同时PostgreSQL也支持通过select语句指定for update或者for share提前设置标志,解决频繁强制事务退出的问题。当然上述机制仍然存在一个问题,当存在冲突时,如何有效地对阻塞事务进行排队,这些就需要显式地申请记录锁,详细情况请参考后面的“表锁与记录锁”章节。表锁与记录锁表6.4-1 语句与表锁模式的对应关系模式名称模式id语句场景NoLock0 AccessShare1select RowShare2select for update/ for shareRowExclusive3insert/ update/ deleteShareUpdateExclusive4vaccum(non-full), analyze, create index concurrentlyShare5create index(without concurrently)ShareRowExclusive6任何postgresql命令不会自动获得这种锁Exclsuvie7任何postgresql命令不会自动获得这种锁AccessExclusice8alter table, drop table, vaccum full, unqualified lock table表6.4-2 表锁模式相容矩阵(行为已加锁类型,列为待加锁类型) AccessShareRowShareRowExclusiveShareUpdateExclusiveShareShareRowExclusiveExclusiveAccessExclusiveAccessShareYYYYYYNNRowShareYYYYYYNNRowExclusiveYYYYNNNNShareUpdateExclusiveYYYNNNNNShare      NNShareRowExclusiveYYNN NNNExclusiveYYNNNNNNAccessExclusiveYNNNNNNN和Oracle、MySQL一样,PostgreSQL出于效率考虑表锁也采用了多粒度机制,表锁的模式和相容矩阵如表6.4-1和6.4-2所示,不同的是PostgreSQL的VACCUM机制非常厚重,所以在表锁中需要引入相关的锁模式。在实现层面,不管是表锁还是显式的记录锁,都采用类似的机制,相关的结构分别为LOCKTAG、LOCK、PROCLOCKTAG、PROCLOCK、PGPROC、LOCKLOCKTAG、LOCALLOCK。需要注意的是,记录锁和表锁不同,记录锁只有共享锁和排它锁两种模式。表6.4-3 LOCKTAG结构域长度含义locktag_field14锁对象标识符locktag_field24锁对象标识符locktag_field34锁对象标识符locktag_field42锁对象标识符locktag_type1锁对象类型:LOCKTAG_RELATION:对表加锁,DB OID+RELOID;LOCKTAG_RELATION_EXTEND:对表加锁;LOCKTAG_PAGE:对page加锁,DB OID+RELOID+PageNumber;LOCKTAG_TUPLE:对记录加锁,DB OID+RELOID+PageNumber +OffsetNumber;LOCKTAG_TRANSACTION:TransactionId;LOCKTAG_VIRTUALTRASNACTIONID:VirtualTransactionId;LOCKTAG_SPECULATIVE_TOKEN:TransactionId;LOCKTAG_OBJECT:DB OID + CLASS OID + OBJECT OID + SUBID;LOCKTAG_USERLOCK;LOCKTAG_ADVISORY;locktag_lockmethodid1锁方法id:DEFAULT_LOCKMETHOD;USER_LOCKMETHOD;LOCKTAG用于标识某个具体被锁定的资源对象,locktag_type和locktag_lockmethodid分别用于标识锁定对象的类型和方法。例如,当locktag_type等于LOCKTAG_TUPLE时,表示锁定一条记录,即记录锁,此时locktag_field1等库对象ID,locktag_field2等于表对象ID,locktag_field3等于PageNumber,表示哪个Page,locktag_field4等于OffsetNumber,表示page内记录的偏移。可见,通过4个locktag_field就可以唯一确定一条记录。当然有时不需要设置所有的locktag_field,例如,当locktag_type等于LOCKTAG_TRANSACTION时只需要将locktag_field1设置为xid。图6.4-3 LOCK结构及与PGPROC、PROCLOCK间的关系 LOCK对象表示一个具体的锁对象,例如一个记录锁就是一个LOCK对象,一个表锁也是一个LOCK对象。如图6.4-3所示,LOCK对象详细描述了某个对象资源上的锁信息,具体情况如下:tag:类型为LOCKTAG,唯一地标识被锁定的某个资源对象;grantMask:类型为LOCKMASK,实际上就4个字节,通过bitmap标识已经在该资源对象上加了哪些锁模式,例如,如果第1个bit位设置为1表示已经加上AccessShare锁。通过1<<LockMode可以标识加上多个锁模式;waitMask:类型同grantMask,grantMask表示已经加上的锁模式,而waitMask表示正在等待的锁模式;procLocks:对tag资源对象加锁的进程列表,指向PROCLOCK对象,并通过PROCLOCK对象中的locklink指针将所有和本LOCK对象相关的PROCLOCK对象链接在一起;waitProcs:当锁模式不相容时,相关进程就需要阻塞等待,waitProc指向等待的PGPROC对象,并通过PGPROC对象的links指针将所有阻塞在本LOCK对象的PGPROC对象链接在一起;Requested、nRequested:本LOCK对象上各种锁模式被请求的次数,总次数,MAX_LOCKMODES为当前系统支持的锁模式数量;granted、nGranted:本LOCK对象上各种锁模式已经被授予的次数,总次数;图6.4-4 PGPROC结构 通过LOCK对象及其哈希表可以从资源的角度找到任何锁对象,从而确定该资源上的锁情况,这是第一个维度。然而我们还需要从事务或者进程的角度查看锁的情况,这是第二个维度。在进入第二个维度之前,我们首先来看PGPROC结构。PostgreSQL是多进程设计,每个后台进程在共享内存中都有一个PGPROC对象。如图6.4-4所示,PGPROC对象中与锁强相关的信息如下:links:和LOCK对象中的waitProcs指针相对应,用于将阻塞等待在同一个LOCK对象上的PGPROC链成一个链表;waitLock:指向本进程正在阻塞等待的LOCK对象;waitProcLock:指向本进程正在阻塞等待的PROCLOCK对象;waitLockMode:本进程阻塞等待的锁模式;heldLocks:本进程已经持有的锁模式;myProcLocks:本进程拥有的所有PROCLOCK对象,通过分区数组以及PROCLOCK中的procLink指针,将所有属于本进程的PROCLOCK对象链接在一起;图6.4-5 资源对象与进程之间的关系 表6.4-4 PROCLOCK结构域类型含义tagPROCLOCKTAGPROCLOCK对象标识符holdMaskLOCKMASK当前已经持有的锁模式releaseMaskLOCKMASK可以释放的锁模式lockLinkSHM_QUEUE用于将归属于同一个LOCK对象的所有PROCLOCK链接在一起procLinkSHM_QUEUE用于将归属于同一个PGPROC进程的所有PROCLOCK链接在一起表6.4-5 PROCLOCKTAG结构域类型含义myLockLOCK*指向LOCK对象的指针myProcPGPROC*指向PGPROC对象的指针LOCK对象描述了某个具体资源对象的锁情况,PGPROC对象描述了某个具体进程的锁情况。如图6.4-5所示,某个资源可以被多个进程加锁,某个进程也可以对多个资源加锁,所以LOCK对象和PGPROC对象时多对多的关系。PostgreSQL设计了PROCLOCK对象以维护LOCK对象和PGPROC对象之间的对应关系。每个PROCLOCK对象代表一个LOCK对象和一个PGPROC对象的对应关系。详细情况如表6.4-4和6.4-5所示,其中的关键信息如下:tag:唯一确定一个LOCK对象和PGPROC对象的对应关系;holdMask:该进程在该对象上已经持有的锁模式;releaseMask:该进程在该对象上可以被释放的锁模式;lockLink和procLink:分别按照Lock对象维度和PGPROC对象维度将相关的LOCKPROC对象链接在一起;表6.4-6 LOCALLOCK结构域类型含义tagLOCALLOCKTAGLOCALLOCK对象标识符lockLOCK*指向共享内存中对应的LOCK对象proclockPROCLOCK*指向共享内存中对应的PROCLOCK对象hashcodeuint32LOCKTAG hash值的拷贝nLocksint64该锁被本进程持有的总次数numLockOwnersint相关的lock   owner个数maxLockOwnersintlockOwners数组的大小lockOwnersLOCKLOCALOWNER*动态申请的lock   owner数组表6.4-7 LOCALLOCKTAG结构域类型含义lockLOCKTAG标识对应的LOCK对象modeLOCKMODE锁模式LOCK、LOCKPROC、PGPROC等对象都存放在共享内存中,运行时都访问共享内存,同时还要考虑互斥,代价比较高。为此,PostgreSQL的每个后台进程在本地维护了LOCALLOCK对象,更新LOCK、LOCKPROC、PGPROC等对象时同时更新LOCALLOCK对象。这样在访问锁时,如果LOCALLOCK对象已经满足要求,就可以不用访问共享内存,从而提高效率。例如,对同一个锁多次加锁或者释放只属于某个资源的锁。死锁对于死锁,PostgreSQL采用了事前预防和事后检测相结合的方式,具体包括:当进程加锁冲突时,就会进入等待队列。如果在队列中已有其它进程请求本进程已经持有的锁,为了避免死锁,可以将本进程插入到该进程的前面;当释放锁时,会尝试唤醒等待队列中的进程。如果某进程请求的锁与该进程前序进程的锁不相容,那么该进程不会被唤醒;通过上述方式,在尽量保证先请求先处理的原则下,尽可能规避潜在的死锁。然而,上述方法只是进行了简单的规避,并不能彻底解决死锁,完全解决需要通过有向等待图来解决,但成本较高,PostgreSQL将这一过程放在了事后。图6.4-6 死锁检测的触发过程 如图6.4-6所示,当阻塞等待超时后就开始进行死锁检测。不过PostgreSQL在有向循环图中引入了Soft Edge和Hard Edge的概念:Soft Edge:进程A和进程B都在同一个锁的等待队列中。进程A和进程B的锁请求不相容,且进程A在进程B的后面,这时进程A指向进程B的有向边为Soft Edge;Hard Edge:进程A请求的锁和进程B已经持有的锁冲突,这时进程A指向进程B的有向边为Hard Edge;可见,Soft Edge是可以通过重新排队进行规避的,而Hard Edge已经形成,是无法改变的。有了Soft Edge和Hard Edge概念之后,我们来看看PostgreSQL是如何进行死锁检测的:从每一个点出发,沿着有向循环图的有向边行进,如果能够回到起点,说明存在死锁;在遍历过程中将Soft Edge记录下来,如果存在死锁且没有Soft Edge,直接终止本事务;如果有Soft Edge。对于每个Soft Edge,递归枚举它的所有子集,尝试进行调整。调整方法采用拓扑进行排序,并遍历测试,如果通过测试表明可以规避死锁,直接结束。如果调整任何一个Soft Edge都无法解决死锁,终止本事务;SIREAD锁和RW-Conflicts图6.4-7 写倾斜于依赖图 在Serializable隔离级别下,PostgreSQL可以解决所有异常,其采用的方法并不是读写都加断言锁和记录锁,而是采用SSI策略(详细情况请参考“事务”章节)。如图6.4-7所示,当依赖图(dependency graph)中存在循环,表示存在写倾斜异常,需要强制某个事务退出,从而打破循环,保证可序列化。可见,SSI的重点是标识rw关系和检测依赖图中是否有循环,为此PostgreSQL定义了SIREAD锁和RW-Conflicts两种数据结构。为了构建RW-Conflicts,首先需要表示出哪些事务读取了哪些记录,这就是SIREAD锁的作用。当执行DML语句时,CheckTargetForConflictsOut函数会创建SIREAD锁。例如,当事务txid1读取记录tuple1时会创建SIREAD锁{tuple1, {txid1}},之后事务txid2也读取记录tuple1时该SIREAD锁会更新为{tuple1, {txid1, txid2}}。可见,SIREAD锁是以记录为单位跟踪相关事务。然而在高并发下,SIREAD锁的数量会非常大,严重消耗系统资源。为此,PostgreSQL采用锁升级的机制来缓解资源消耗。SIREAD锁有tuple、page、relation三个层次。如果某个page的所有tuple都创建了SIREAD锁,那么升级为page级,即以page为单位创建SIREAD锁,原来属于该page的tuple级SIREAD锁全部释放。Relation级即表级,原理同page级。RW-Conflicts是一个三元组,由读事务、写事务、记录(元组)组成。例如,事务txid1读取了记录tuple1,之后事务txid2更新了记录tuple1,那么就需要创建一个RW-Conflict,{txid1, txid2, {tuple1}}。在执行insert、update、delete命令时,CheckTargetForConflictsIn函数会检查相关SIREAD锁,从而判断是否存在RW-Conflicts。如果存在,就创建RW-Conflicts。表6.4-8 写倾斜检测示例一时间Tx_A(txid_a)Tx_B(txid_b)SIREAD LocksRW-ConflictsT1start transaction isolation level serializable;start transaction isolation level serializable;  T2select * from t1 where id=2;(1 row returned) L1:{pkey_2,{txid_a}}L2:{tuple_2,{txid_a}} T3 select * from t1 where id=1;(1 row returned)L1:{pkey_2,{txid_a}}L2:{tuple_2,{txid_a}}L3:{pkey_1,{txid_b}}L4:{tuple_1,{txid_b}} T4update t1 set val=”++” where id=1;(1 row updated)  C1:{r=txid_b, w=txid_a, {pkey_1, tuple_1}}T5 update t1 set val=”++” where id=2;(1 row updated) C1:{r=txid_b, w=txid_a, {pkey_1, tuple_1}}C2:{r=txid_a, w=txid_b, {pkey_2, tuple_2}}T6commit;(success)   T7 commit;(failed)  表6.4-9 写倾斜检测示例二时间Tx_A(txid_a)Tx_B(txid_b)T1start transaction isolation level serializable;start transaction isolation level serializable;T2select * from t1 where id=2;(1 row returned) T3 select * from t1 where id=1;(1 row returned)T4update t1 set val=”++” where id=1;(1 row updated) T5commit;(success) T6 update t1 set val=”++” where id=2;(failed)表6.4-10 写倾斜检测示例三时间Tx_A(txid_a)Tx_B(txid_b)T1start transaction isolation level serializable;start transaction isolation level serializable;T2select * from t1 where id=2;(1 row returned) T3 select * from t1 where id=1;(1 row returned)T4update t1 set val=”++” where id=1;(1 row updated) T5 update t1 set val=”++” where id=2;(1 row updated)T6commit;(success) T7 select * from t1;(failed)假设表t1,在id列上有主键索引。表6.4-8给出了写倾斜检测的详细过程,具体如下:T2:事务Tx_A查询id为2的记录,由于涉及主键,所以CheckTargetForConflictsOut创建了两个SIREAD Locks,分别为L1和L2;T3:事务Tx_B查询id为1的记录,由于涉及主键,所以CheckTargetForConflictsOut创建了两个SIREAD Locks,分别为L3和L4;T4:事务Tx_A更新id为1的记录,CheckTargetForConflictsIn检测SIREAD Locks,发现存在RW-Conflicts,所以创建RW-Conflicts C1;T5:事务Tx_B更新id为2的记录,CheckTargetForConflictsIn检测SIREAD Locks,发现存在RW-Conflicts,所以创建RW-Conflicts C2。此时依赖图已经存在循环,即写倾斜已经产生,然而事务Tx_A和Tx_B都没有提交,所以CheckTargetForConflictsIn无法基于“first-committer-win”原则决策让哪个事务失败;T6:事务Tx_A提交,PreCommit_CheckForSerializationFailure函数检查发现Tx_A和Tx_B存在写倾斜,但Tx_B事务仍然处于运行状态,所以事务Tx_A提交成功;T7:事务Tx_B提交,PreCommit_CheckForSerializationFailure函数检查发现Tx_A和Tx_B存在写倾斜,但Tx_A事务已经提交,所以事务Tx_B提交失败;从上述过程我们发现SIREAD Locks和RW-Conflicts不能在事务提交后立刻释放,需要存在一段时间,以确保相关事务的写倾斜检测能够正常进行。另外,并不意味着事务的倾斜异常只会发生在提交阶段。事实上,CheckTargetForConflictsIn和CheckTargetForConflictsOut都会进行依赖图检测,只要存在循环,且有一个事务已经提交,就会立刻让当前事务失败,例如表6.4-9和6.4-10。CockroachDB设计原理设计思路CockroachDB是一种基于乐观机制的分布式数据库,其默认的隔离级别是可序列化快照(SS, Serializable Snapshot)。和PostgreSQL相比,CockroachDB不采用锁机制,而是将SS发挥到极致,其采用的并发控制有如下特征:可序列化:执行结果和某种串行执行的结果是等价的;可恢复:对于一系列并发执行的事务,有些事务执行成功,有些事务异常退出,仍然能够保证系统可恢复至一致性状态。原子性保证单个事务是可恢复的,严格的乐观调度策略保证任何事务的组合执行也是可恢复的;无锁:执行期间不会在资源上加锁。如果某事务和可序列化、乐观调度机制相关冲突,通过强制该事务退出来保证正确性;分布式:系统无集中的授时、协调或者其它服务;可序列化图在序列化理论中,冲突的发生条件是两个不同事务中的操作操作了相同的数据,且至少有一个操作是写操作。满足上述条件时,就可以说第二个操作和第一个操作相冲突。冲突有三种类型:读写冲突(RW):第二个操作覆盖了第一个操作读取的结果;写读冲突(WR):第二个操作读取了第一个操作写的结果;写写冲突(WW):第二个操作覆盖了第一个操作写的结果;图6.5-1 可序列化图示例 对于事务执行的任何历史,通过这些冲突可以建立一个可序列化图。如图6.5-1所示,将所有事务链接在一起的有向图有如下部分组成:事务是图中的节点;当某操作和另外一个事务的操作冲突时,就画一个从被冲突事务到冲突事务的有向边;图6.5-2 循环可序列化图示例,该历史不可序列化 执行历史是可序列化的,当且仅当可序列化图是非循环的。图6.5-2中的示例就是不可序列化的。CockroachDB采用时间排序来保证可序列化图是非循环的,方法如下:每个事务启动时都会赋予一个时间戳,此后该事务中的所有语句都使用此时间戳;每个操作都可以独立地判断自己和其它事务的哪个操作冲突,以及被冲突操作的时间戳是什么;允许操作和拥有更早时间戳的其它操作相冲突,但不允许和拥有更晚时间戳的操作相冲突;由于在时间前进方向上不允许存在冲突,所以可序列化图就不存在循环。下面章节我们将介绍CockroachDB是如何检测和防止这些冲突的。WR冲突与MVCCWR冲突采用多版本来解决。CockroachDB不仅仅存储单值,而是存储了基于时间戳的多个版本值。写操作不会覆盖旧值,而是创建一个带新时间戳的新值。图6.5-3 多版本值读示例 如图6.5-3所示,对某key的读操作将返回比读操作时间戳小的最新版本。因此,在CockroachDB中后继事务不会形成WR冲突,因此读操作不会使用更晚的时间戳。RW冲突与时间戳缓存任何读操作的时间戳都会缓存在时间戳缓存中。通过该缓存我们可以查询某个key最近进行了哪些读操作,以及这些读操作的时间戳是怎样的。所有写操作在对key进行写时都需要查询时间戳缓存。如果返回的时间戳大于写操作的时间戳,表明RW和一个更晚的时间戳相冲突。这是不允许的,必须以一个更晚的时间戳重启写操作所在的事务。时间戳缓存是一个区间缓存,也就是说其存储的是key的范围。如果某读操作读取了某段范围内的所有key(例如扫描),那么扫描的这些key都以范围的形式存在时间戳缓存中。时间戳缓存完全缓存在内存中,采用LRU算法。当缓存大小达到设定的限制后,最老的时间戳条目就会被删除。为了处理不在缓存中的key,需要定义“低水位线”,其等价于所有key的最早时间戳。如果写操作查询的key不在时间戳缓存中,就返回低水位线。WW冲突与只写最新版本写操作尝试写某key时,该key的时间戳比操作本身的时间戳还要新,表明WW和一个更晚的时间戳相冲突。为了高正可序列化,必须以一个更晚的时间戳重启写操作所在的事务。通过时间排序,拒绝任何不满足排序要求的冲突,CockroachDB的SS可以保证执行结果是可序列化的。严格调度与可恢复性通过前面章节介绍的冲突规则可以保证执行历史是可序列化的。另一个问题是如何保证两个满足冲突规则的未提交事务是可恢复的。假设两个事务T1和T2,T1的时间戳小于T2的时间戳。T1写了key“A”,之后T2在T1提交前读取key“A”。该冲突是被时间排序规则所允许的。但T2应该从key“A”中读到哪一个值呢?假设忽略掉T1的未提交数据,读取数据的前一个版本。如果T1和T2都成功提交,这将引起WR冲突,且和时间排序规则相冲突,因此不可序列化;假设读取T1的未提交数据。如果T2提交成功,T1回滚了,这和T1的原子性相冲突(T1回滚了,但仍然对数据库的状态产生了影响);上述两种情况都是不允许的。为了维护调户的可恢复性,在T1提交前T2不可以提交。为此,CockroachDB采取了严格的调度策略处理此场景:读操作和覆盖操作只允许作用在已提交数据上,操作永远不允许在未提交数据上实施。为了实现原子性提交,key上的未提交数据都保存在意向记录中(Intent Record)。如图6.5-4所示,在MVCC存储结构中,key上的意向记录可以很容易地被查到。在并发环境中,意向记录意味着存在一个正在运行的并发事务。图6.5-4 意向记录与MVCC 严格调度存在两种场景:读操作遇到一个时间戳更小的意向记录,或者写操作遇到一个意向记录(不管时间戳的大小)。对于这两种场景,CockroachDB有两种选择:如果第二个事务的时间戳更大,该事务可以等待第一个事务提交或回滚完毕,然后再继续执行自己的操作;强制其中一个事务退出;作为一种乐观的系统(无等待),CockroachDB选择了强制退出其中一个事务。决策将哪个事务退出的过程如下:step1:第二个事务(遇到意向记录的那个事务)读取第一个事务的事务记录(CockroachDB为每个活跃事务维护一条事务记录,以表征该事务的提交状态);step2:如果第一个事务已经提交(意向记录还没有来得及清理),第二事务清理该意向记录,即将意向记录中的值当成正常值来处理;step3:如果第一个事务已经回滚,第二事务删除该意向记录,并将意向记录当成不存在处理;step4:如果第一个事务处于运行态(未提交),固定选择第一个或第二个事务都是不合理的。同时还存在两个事务同时处理对方,对于冲突的两个事务,胜利的一方最好是确定性的。为此,每个事务记录都赋予一个优先级,永远强制退出优先级地的那个事务。如果优先级相等,强制退出时间戳大的事务。新事务启动时获取一个随机的优先级,当事务因为冲突而重启时,其新的优先级等于max(random, [导致本事务重启的哪个事务的优先级]-1),最终事务在重启的过程中优先级会不断提升。采用本方法,未提交事务之间的冲突可以通过强制退出其中一个事务而立刻得到解决。因此,严格调度确保了所有的事务执行历史都是可恢复的。优先级已经在概率上解决了导致异常事务的问题,即被异常打败的事务会不断地重启,且在重启的过程中优先级会不断地上升,最终获得胜利。另外,CockroachDB在所有事务中增加了心跳。在运行过程中,活跃事务需要周期性地更新其事务记录中的心跳时间戳。如果其它事务碰到某事务的记录时,该事务的心跳时间戳超时,那么该事务被认为是异常事务,此时强制异常事务退出而不是比较优先级。VoltDB设计原理传统数据库的成本Micheal Stonebraker等人在开源数据库Shore上进行了各种基准测试,以调研传统数据库中各组件的成本。测试环境为桌面系统,刚开始性能大约为640TPS。之后每次删除系统中的一个特征,并重新进行基准测试,直至仅剩下一个非常薄的查询内核,性能为12700TPS。这个内核是单线程、无锁、无恢复功能的全内存数据库。通过分解发现了4个影响性能的最大组件:Logging:跟踪数据结构的所有变化并记录日志,拖慢了性能。如果可恢复性不是必须的,或者可通过集群中其它节点进行恢复,日志就不是必须的;Lock:两阶段锁产生了相当大的负载,因为所有对数据的访问都要经过Lock Manager这个单点组件;Latch:在多线程数据库中,很多数据结构在被访问前都要先加上Latch,通过单线程机制可以避免这个诉求,并获得可观的性能提升;BufferManager:内存数据库不需要通过缓存池访问数据页,消除了访问每条记录的间接成本;表6.6-1 传统数据库各组件指令数占比组件New OrderPaymentBtree keys16.2%10.1%Logging11.9%17.7%Locking16.3%25.2%Latching14.2%12.6%Buffer manager34.6%29.8%others6.8%4.7%图6.6-1 NewOrder下各组件指令占比 图6.6-1和表6.6-1给出了这些挑战对应的性能变化情况(测试模型为TPC-C下NewOrder事务和Payment事务,统计的是运行该事务的CPU指令数)。可见每个组件都占整个系统的10%~35%指令数(整个系统运行一遍NewOrder事务的指令数为1.73M)。“hand-coded optimizations”代表的是对B树进行一系列优化。“useful work”代表的是处理查询的实际工作,只占总工作的1/60。“buffer manager”下面的方框代码的是移除上面所有组件之后的性能,这时仍然支持事务,指令数只有总体的1/15,不过仍然是实际工作的4倍(两者之间的差距主要源于函数调用栈的深度,以及无法完全消除缓存管理和事务相关的所有代码)。基于上述分析,Micheal Stonebraker在设计VoltDB时,期望通过裁减Buffer Manager、Latch和Lock等组件以获得更高的性能。因此,VoltDB是一款仅支持序列化隔离级别的分布式内存数据库。内存数据库可以降低Buffer Manager的成本,仅支持序列化隔离级别可以降低Latch和Lock的成本。本章重点讨论VoltDB的并发控制是如何避免Lock成本的。图6.6-2 串行执行队列 假设只有单颗CPU和DRAM内存,我们应该设计一个怎样的程序,在单位时间内仅可能多地执行命令。这些命令可以是创建、查询或者更新结构化数据。如图6.6-2所示,解决方案之一就是将命令放在一个队列中。然后执行一个循环,不断地从队列中取命令并执行。显而易见的是此方法可以让单颗CPU充分运转起来,当然有几纳秒的时间周期用于从命令队列中取命令和将响应放入响应队列中。在循环中,CPU执行的任务基本上100%都是实际工作,而不是系统调度、锁控制、缓存控制等和实际工作不相关的工作。在VoltDB中,用户的命令就是SQL执行计划、分布式分片上的执行计划、或者存储过程的调用,循环就对应于单个分片上的命令队列。并发控制VoltDB每次只会运行一个命令,命令之间无并行无重叠,从而提供了序列化的隔离性。在单颗CPU上高饱和地运行应用的实际工作。然而服务器上有多颗CPU,如何让多颗CPU都高饱和地运行起来?首先对数据进行分片,然后在每个分片上维护一个命令队列。这也是大部分分布式NoSQL数据库的设计思路:操作需要制定待操作数据的KEY。VoltDB采用的是一致性哈希分片,用户需要为每个表指定分片列。这些分片列和NoSQL存储的KEY非常类似。根据分片列判断SQL语句或者存储过程涉及哪个分片,然后将其路由到对应分片的命令队列上。集群中多个服务器或者服务器上多颗CPU,都可以通过增加分片的方法让各CPU繁忙起来,每颗CPU独立运行某个分片上的命令队列,各自提供ACID语义。可见:在每个分片上串行地执行查询或者修改命令;命令可以是SQL、存储过程、SQL执行计划的某个片段;每个命令都提供ACID;表数据分布在各个分片上;通过增加分片的方法在多CPU、多服务器上获得扩展性;事务VoltDB将存储过程作为单独的事务来执行,SQL语句作为自动提交的事务来执行。单分片事务是在单个分片上直接执行的事务。单分片事务可以是只读事务,也可以是读写事务,每个单分片事务都完全满足ACID。实际上单分片只读事务的执行过程可以进一步优化,即越过SPI,以负载均衡的方式直接路由到分片的某个副本上。VoltDB的副本间是以同步的方式执行读写事务,所以只读事务即使越过SPI,仍然可以读到前面事务的结果。此优化可以提升只读事务的吞度量,降低只读事务的延时,减轻SPI的工作量。图6.6-3 只读事务在分片的副本间负载均衡 图6.6-3以示例的方式展示了优化的正确性,事务A和事务C为读写事务,事务B为只读事务,且应用的发起顺序为事务B先于事务C而后于事务A。事务B放在任何一个副本的序列化命令队列中都是正确的(不影响其它副本的结果)。VoltDB支持事务在多分片上进行读写操作,这样的是称为多分片事务。SPI为单个分片实施序列化工作,MPI为跨分片事务实施序列化共诺。MPI会和相干分片的SPI交互,以将分解后的命令注入到对应分片的命令队列中。图6.6-4 只读事务在分片的副本间负载均衡 图6.6-4示例了MPI执行多分片事务M的过程。SPI#1将事务M序列化在单分片事务C的后面执行,SPI#2将事务M序列化在单分片事务B的前面执行。从全局来看,事务的执行顺序为C、M、B。为了执行多分片SQL,VoltDB的SQL执行计划生成器会将执行计划分解成多个片段,有些片段在多个分片上分布式执行,有些片段对分布式执行的结果进行汇总。多分片写事务在各分片间采用两阶段提交协议。在Prepare阶段,MPI将执行接话片段分发到各个分片执行。如果这些片段在各分片上执行成功,无约束性冲突,MPI通知所有分片进行提交。各分片不会执行命令队列中的任何其它命令,只到收到提交消息。VoltDB中多分片事务的大部分案例是分布式读,要么是读取记录时不知道分片的取值,要么是进行汇聚分析。对于仅含只读工作的多分片事务,用户可以通过标签显式地表达出来,这样上述分布式过程就可以进行优化:SPI可以将命令发给任何某个副本,而不需要在副本间同步;分片执行完读操作后,可以立刻执行命令队列中的其它命令,而不是阻塞在那里等待提交消息;总结与分析并发控制的原则是在保证正确性的前提下尽可能地提高并发性,为此Oracle、MySQL、PostgreSQL、CockroachDB、VoltDB采用了不同的策略以提高并发性。从并发控制算法的用户友好度和ANSI SQL隔离级别匹配度来看。MySQL支持ANSI SQL定义的四种隔离级别,在任何情况下都会加记录级写锁,避免了Dirty Write异常。在Read Committed级别下通过语句级一致性读解决了Dirty Read异常。在Repeatable Read级别下,通过事务级一致性读以及Next_key写锁解决了Fuzzy Read和Phantom异常,但由于读不加锁,仍然存在Lost Update和Write Skew异常。在Serializable级别下,读写都加Next_key锁,可以解决所有异常。PostgreSQL真正意义上仅支持三种隔离级别(Read Uncommitted实际上就是Read Committed),在任何情况下都会加记录级写锁,避免了Dirty Write异常。在Read Committed级别下通过语句级一致性读解决了Dirty Read异常。在Repeatable Read级别下,通过事务级一致性读以及写锁解决了Lost Update、Fuzzy Read、Phantom异常,但由于读不加锁,Lost Update异常只能采取“First-Update-Win”原则,对用户不友好,而Write Skew异常仍然无法解决。在Serializable级别下,通过SSI算法进一步解决Write Skew异常,但解决的方法是一旦发现潜在的Write Skew,就强制某个事务退出,对用户并不友好。Oracle仅支持Read Committed和Serializable两种隔离级别,在任何情况下都会将记录级写锁,避免了Dirty Write异常。在Read Committed级别下通过语句级一致性读解决了Dirty Read异常。在Serializable级别下,通过事务级一致性读和SCN比较,解决了Lost Update、Fuzzy Read和Phantom异常,读不加锁导致Lost Update只能通过报错来解决,对用户不友好,同时读不加锁导致Write Skew异常无法解决。CockroachDB支持Snapshot Isolation和Serializable Snapshot Isolation隔离级别,通过多版本和时间戳排序达到可序列化要求。然而为了可恢复新采用了严格的调度策略,不管是读操作还是写操作一旦遇到比自己跟到且未提交的时间戳,必须强制一个事务退出,对用户不友好。VoltDB仅支持Serializable隔离级别,所有事务都串行执行,不存在任何异常。可见在ANSI SQL隔离级别匹配度上MySQL最高,然后依次是PostgreSQL、Oracle、CockroachDB和VoltDB。MySQL、PostgreSQL、Oracle都支持用户在select语句上指定加锁,这样即使在低隔离级别上也可以选择性地解决Lost Update和Write Skew异常。从并发控制算法的效率上来看,Oracle没有设计独立的锁结构,仅在记录上通过1个字节的lb表达出锁信息,理论上锁资源是无穷的。Enqueue机制对等待的事务进行排队,并区分拥有者和等待者,进行非常精准的唤醒。MySQL和PostgreSQL的锁机制比较类似,正常情况下通过记录上的标志位进行判断(判断规则比较复杂),一旦出现冲突则转换为显式锁。在显式锁方面,MySQL以page为单位组织锁资源,在空间和时间上做了权衡。PostgreSQL采取了记录、page、表多粒度的方式组织锁资源。在冲突对事务进行排队时,两者相对Oracle都比较粗糙。和MySQL不同的是,PostgreSQL在SSI方面又引入了SIREAD锁和RW-Conflicts,对所有读操作、读写操作都要进行跟踪记录,并进行检索判断,成本非常高。CockroachDB需要对所有记录的读操作维护时间戳,成本较高。当然由于采用的是乐观控制,在低冲突场景下效率相对较高,在中高冲突下由于要频繁地重做,效率是极低的。VoltDB采用的是串行执行策略,效率非常高。但场景首先,需要以存储过程为事务执行单位,减少应用和数据库之间的来回交互,同时负载要有非常好的可切分性,每颗CPU负责一个分片,分片之间无相关性。可见,在效率上Oracle是最高的,MySQL和PostgreSQL相当。CockroachDB和VoltDB引入了新思路,但场景相对受限。Oracle、MySQL、PostgreSQL采用了锁机制,存在死锁的情况,三者都采用有向循环图的检测方法。Oracle认为死锁检测的代价较大,只有在锁等待超时后才会检测死锁。MySQL在发生锁等待时提前进行死锁检测,提前解决死锁问题。PostgreSQL也采用了锁等待超时后进行检测的策略,但在事前和事后都做了一些小的优化,尽可能地避免死锁。PDF版本下载地址:http://blog.itpub.net/69912723/viewspace-2725664/
文章
机器学习/深度学习  ·  SQL  ·  缓存  ·  Oracle  ·  算法  ·  关系型数据库  ·  MySQL  ·  数据库  ·  PostgreSQL  ·  索引
2023-03-27
PG技术大讲堂 - Part 10:PostgreSQL数据库管理
PostgreSQL从小白到专家,是从入门逐渐能力提升的一个系列教程,内容包括对PG基础的认知、包括安装使用、包括角色权限、包括维护管理、、等内容,希望对热爱PG、学习PG的同学们有帮助,欢迎持续关注CUUG PG技术大讲堂。Part 10:PostgreSQL数据库管理内容1:PostgreSQL数据库结构内容2:PostgreSQL数据库级权限管理内容3:PG数据库级环境参数设置内容4:PostgreSQL数据库级属性修改10.1、数据库结构数据库集簇逻辑结构每个数据库存储的对象(表、索引、视图等等)是独立的、私有的,每个数据库类似于每个房间,从房间中取东西,就需要到房间里面;同理,要访问某个数据库中的对象,就需要登录到指定的数据库中。PostgreSQL数据库结构数据库集群是由PostgreSQL服务器管理的数据库的集合。PostgreSQL中的“数据库集群”一词并不意味着“一组数据库服务器”。PostgreSQL服务器在单个主机上运行,并管理单个数据库群集。 数据库是数据库对象的集合。在关系数据库理论中,数据库对象是用来存储或引用数据的数据结构。堆(heap)表是一个典型的例子,它有很多类似于索引、序列、视图、函数等等。在PostgreSQL中,数据库本身也是数据库对象,在逻辑上彼此分离。所有其他数据库对象(如表、索引等)都属于各自的数据库。PostgreSQL数据库属主· Postgres中的数据库属主属于创建者,只要有createdb的权限就可以创建数据库,数据库属主不一定拥有存放在该数据库中其它用户创建的对象的访问权限。· 数据库在创建后,允许public角色连接,即允许任何人连接。· 数据库在创建后,不允许除了超级用户和owner之外的任何人在数据库中创建schema。· 数据库在创建后,会自动创建名为public的schema,这个schema的all权限已经赋予给了public角色,即允许任何人在里面创建对象,但对己存在的其它用户的表不具有任何权限。10.2、数据库权限CREATE:可以在指定数据库创建schema的权限CONNECT:可以连接到指定数据库的权限TEMPORARY:可以创建临时表的权限ALL:指定数据库所有的权限语法:GRANT { { CREATE | CONNECT | TEMPORARY | TEMP } [, ...] | ALL [ PRIVILEGES ] } ON DATABASE 数据库名称 [, ...] TO role_specification [, ...] [ WITH GRANT OPTION ]由于数据库在创建后,允许public角色连接,即允许任何人连接。所以如果要取消某个用户对指定数据库连接的权限,需要先取消public的连接权限,再取消该用户的连接权限。--授权用户连接数据库的权限grant connect on database db_name to user_name;--撤销用户连接数据库的权限revoke connect on database db_name from public;revoke connect on database db_name from user_name;--查看哪些用户有某个数据库的connect权限select datname,datacl from pg_database where datname='db_name';10.3、数据库环境设置PostgreSQL参数设置分为实例级、数据库级、用户级和会话级,而有些参数可以在所有级别中设置,优先级顺序为会话级>用户级>数据库级>实例级。数据库参数配置语法:ALTER DATABASE 名称 SET 配置参数 { TO | = } { 值 | DEFAULT }ALTER DATABASE 名称 SET 配置参数 FROM CURRENTALTER DATABASE 名称 RESET 配置参数ALTER DATABASE 名称 RESET ALL配置示例(一):--设置数据库搜索路径:alter database postgres set search_path to "$user", public, schema_name;--配置连接某个库时可使用的工作内存alter database postgres set work_mem = '8MB'; --配置连接某个库时可使用的维护内存alter database postgres set maintenance_work_mem TO '256MB';配置示例(二):--配置连接某个库后使用的时区alter database postgres set TimeZone to cet;alter database postgres set DateStyle to SQL, DMY;(重新登录生效)--配置连接某个库后执行语句最多时长(执行1秒超时)alter database postgres set statement_timeout =1000;--配置连接某个库后默认的客户端编码,配置客户端编码为gbk,适用于数据库编码为utf8,应用程序编码为gbk的应用alter database postgres set client_encoding to gbk;配置示例(三):--配置某个库使用日志记录级别(设置后,对这个数据库的访问不记录日志)alter database postgres set log_statement=none;--配置连接某个库后的wal日志写盘级别(设置后,该库的更新操作只要求本地提交)alter database postgres set synchronous_commit to local;--配置连接某个库后禁用某个规划器(禁用indexonlyscan扫描)alter database postgres set enable_indexonlyscan to off;配置示例(四):--配置连接某个库后执行出错时中断连接(对新会话生效)alter database postgres set exit_on_error to on;--重新连接后select pg_backend_pid();--执行错误会导致连接中断select * from d1;配置示例(五):--查看所有个性化配置\drds--查询数据库的连接数限制只能查看数据字典表select datname,datconnlimit from pg_database--设置某个个性化设置为默认值ALTER DATABASE postgres reset exit_on_error;--设置所有个性化设置为默认值ALTER DATABASE postgres reset ALL;10.4、数据库属性修改数据库的属性我们可以进行修改,修改范围是数据库名字、属主、表空间。ALTER DATABASE 名称 RENAME TO 新的名称ALTER DATABASE 名称 OWNER TO { 新的属主 | CURRENT_USER | SESSION_USER }ALTER DATABASE 名称 SET TABLESPACE 新的表空间示例:--修改数据库名字ALTER DATABASE newdb2 RENAME TO newdb3;--修改数据库属主ALTER DATABASE newdb3 OWNER TO u1;--修改新表空间的名字ALTER DATABASE newdb3 SET TABLESPACE new_tbl;以上就是Part 10 - PostgreSQL数据库管理 的内容,欢迎进群一起探讨交流,钉钉交流群:35,82,24,60,钉钉群有专门讲解公开课往期课程,联系cuug咨询老师
文章
存储  ·  搜索推荐  ·  关系型数据库  ·  数据库连接  ·  分布式数据库  ·  数据库  ·  数据安全/隐私保护  ·  PostgreSQL  ·  数据库管理  ·  索引
2023-03-09
...
跳转至:
PolarDB开源社区
995 人关注 | 272 讨论 | 195 内容
+ 订阅
  • 源码解读:PolarDB-X中的窗口函数
  • [版本更新]PolarDB-X v2.2.1 生产级关键能力开源升级
  • PolarDB-X 全局Binlog解读之HA篇
查看更多 >
数据库
252947 人关注 | 52318 讨论 | 99299 内容
+ 订阅
  • MySQL5.6如何实现全文搜索?具体步骤是怎样的?底层原理是什么?
  • 如何在MySQL中优化表性能?底层原理是什么?
  • 如何在MySQL中优化表性能?
查看更多 >
人工智能
2875 人关注 | 12395 讨论 | 102726 内容
+ 订阅
  • MySQL5.6如何实现全文搜索?具体步骤是怎样的?底层原理是什么?
  • pytorch 神经网络设置初始固定参数
  • pytorch如何将多个tensor一维度张量,合并成一个张量
查看更多 >
云原生
234326 人关注 | 11613 讨论 | 47413 内容
+ 订阅
  • CSS块格式化上下文(Block Formatting Context,BFC)
  • CSS浮动
  • Spring框架尝鲜
查看更多 >
云计算
21833 人关注 | 59802 讨论 | 58199 内容
+ 订阅
  • 阿里云5大基础产品——ECS云服务器
  • 利用IBCS虚拟专线和haproxy,构建安全高效的本地数据中心
  • CSS实现单行、多行文本溢出隐藏
查看更多 >