月度归档: 2021 年 5 月

浅谈 DML、DDL、DCL的区别

 

一、DML

  • DML(data manipulation language)数据操纵语言:

就是我们*经常用到的 SELECT、UPDATE、INSERT、DELETE。 主要用来对数据库的数据进行一些操作。

SELECT 列名称 FROM 表名称
UPDATE 表名称 SET 列名称 = 新值 WHERE 列名称 = 某值
INSERT INTO table_name (列1, 列2,...) VALUES (值1, 值2,....)
DELETE FROM 表名称 WHERE 列名称 = 值

二、DDL

  • DDL(data definition language)数据库定义语言:

其实就是我们在创建表的时候用到的一些sql,比如说:CREATE、ALTER、DROP等。DDL主要是用在定义或改变表的结构,数据类型,表之间的链接和约束等初始化工作上

复制代码

CREATE TABLE 表名称
(
列名称1 数据类型,
列名称2 数据类型,
列名称3 数据类型,
....
)

ALTER TABLE table_name
ALTER COLUMN column_name datatype

DROP TABLE 表名称
DROP DATABASE 数据库名称

复制代码

三、DCL

  • DCL(Data Control Language)数据库控制语言:

是用来设置或更改数据库用户或角色权限的语句,包括(grant,deny,revoke等)语句。这个比较少用到。

 

一般情况下我们用到的是DDL、DML这两种。

必须了解的MySQL锁和事务

转载自:https://mp.weixin.qq.com/s/boNBI9y_W9pTqLR7NIaA0Q

一、锁定机制*常讨论的话题

 

1、什么是锁

 

锁是数据库系统区别于文件系统的一个关键特性。数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性。例如:操作缓冲池中的 LRU 列表,删除、添加、移动 LUR 列表中的元素。

 

对于任何一种数据库来说都需要有相应的锁定机制,所以 MySQL 自然也不能例外。

 

MySQL 数据库由于其自身架构的特点,存在多种数据存储引擎,每种存储引擎所针对的应用场景特点都不太一样,为了满足各自特定应用场景的需求,每种存储引擎的锁定机制都是为各自所面对的特定场景而优化设计,所以各存储引擎的锁定机制也有较大区别。

 

MySQL 常用存储引擎(MyISAM,InnoDB)用了两种类型(级别)的锁定机制:表级锁定,行级锁定。

 

1)表级锁 

 

表级别的锁定是 MySQL 各存储引擎中*大颗粒度的锁定机制。该锁定机制*大的特点是实现逻辑非常简单,带来的系统负面影响*小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题。

 

当然,锁定颗粒度大所带来*大的负面影响就是出现锁定资源争用的概率也会*高,致使并大度大打折扣。

 

使用表级锁定的主要是 MyISAM、MEMORY、CSV 等一些非事务性存储引擎。

 

2)行级锁 

 

行级锁定*大的特点就是锁定对象的颗粒度很小,也是目前各大数据库管理软件所实现的锁定颗粒度*小的。由于锁定颗粒度很小,所以发生锁定资源争用的概率也*小,能够给予应用程序尽可能大的并发处理能力而提高一些需要高并发应用系统的整体性能。

 

虽然能够在并发处理能力上面有较大的优势,但是行级锁定也因此带来了不少弊端。由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗自然也就更大了。此外,行级锁定也*容易发生死锁。

 

使用行级锁定的主要是 InnoDB 存储引擎。

 

总结如下:

 

  • 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率*高,并发度*低;
  • 行级锁:开销大,加锁慢;会出现死锁;锁定粒度*小,发生锁冲突的概率*低,并发度也*高;

 

例如,下图只是对 myisam 表修改一行记录:

 

 

 

其他 insert 操作就需要等待上个 update 语句执行完成,再执行 insert 操作,这时候就会产生表锁。

 

2、InnoDB锁的类型

 

InnoDB 存储引擎实现了如下两种标准的行级锁:

 

  • 共享锁(S Lock):允许事务读一行数据。但不能修改,增加,删除数据。
  • 排他锁(X Lock):获准排他锁的事务既能读数据,又能修改数据。

 

如果一个事务 t1 已近获得了行 r 的共享锁,那么另外的事务 t2 可以获得行 r 的共享锁,因为读取并没有改变行 r 的数据,称这种情况为锁兼容(Lock Compatible)。但若有其他的事务想获得行 r 的排它锁,则必须等待事务 t1,t2 释放行 r 的共享锁——这种情况称为锁不兼容(confilict)。

 

此外,InnoDB 存储引擎支持多粒度(granular)锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB 存储引擎支持了一种额外的锁方式,称为意向锁(Intention Lock)。

 

意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁。

 

如下图,若将上锁的对象看成一颗树:

 

 

 

那么*下层的对象(行记录)上锁,也就是对*细粒度的对象进行上锁,那么首先需要对粗粒度的对象上锁。

 

如果需要对页上的记录 r 进行上 X 锁,那么分别需要对数据库 A、表、页上意向锁,*后对记录 r 上 X 锁,若其中任何一个部分导致等待,那么该操作需要等待粗粒度锁的完成。

 

举例来说,在对记录 r 加 X 锁之前,已近有事务对表 1 进行了 S 表锁,那么表 1 上已存在 S 锁,之后事务需要对记录 r 表 1 上加 IX , 由于不兼容,所以该事务,需要等待表锁操作的完成。

 

InnoDB 存储引擎支持意向锁设计比较简练,其意向锁即为表级别的锁,设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。其支持两种意向锁:

 

  • 意向共享锁( intention shared lock, Is),事务有意向对表中的某些行加共享锁(S锁)
  • 意向排它锁(intention exclusive lock,IX),事务有意向对表中的某些行加排他锁(X锁)

 

意向锁是有数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。

 

由于 InnoDB 存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫以外的任何请求。

 

表级意向锁与行锁的兼容性:

 

  • S:共享锁
  • X:排它锁
  • IS:意向共享锁
  • IX:意向排它锁

 

 

 

  • 排它锁(X):与任何锁都不兼容
  • 共享锁(S):只兼容共享锁和意向共享锁
  • 意向锁(IS,IX): 互相兼容,行级别的锁只兼容共享锁

 

3、一致性锁定读

 

用户有时候需要显示地对数据库读取操作进行加锁以保证数据逻辑的一致性。而这要求数据库支持加锁语句,即使是对于 select 的只读操作。InnoDB 存储引擎对于 select 语句支持两种一致性的锁定读操作:

 

select … for update;

select … lock in share mode;

 

select … for update 对读取的行记录加一个 X 锁,其他事务不能对已锁定的行加上任何锁。

 

select … lock in share mode 对读取的行记录加一个 S 锁,其他事务可以向被锁定的加 S  锁,但是如果加 X 锁,则会组赛。

 

此外 select … for update , select … lock in share mode 必须在一个事务中,当事务提交了,锁也就释放了。因此在使用上诉两句select 锁定语句时,务必加上BEGIN,START TRANSACTION 或者 SET AUTOCOMMIT=0。

 

4、一致性非锁定读

 

在默认的隔离级别下,一致读是指 InnoDB 在多版本控制中在事务的首次读时产生一个镜像,在首次读时间点之前,其他事务提交的修改可以读取到,而首次读时间点之后,其他事务提交的修改或者是未提交的修改,都读取不到。

 

唯一例外的情况,是在首次读时间点之前的本事务未提交的修改数据可以读取到。

 

在读取提交数据隔离级别下,一致读的每个读取操作都会有自己的镜像。一致读操作不会施加任何的锁,所以就不会阻止其他事务的修改动作。

 

比如*经典的 mysqldump –single-transaction 备份的时候就是把当前的事务隔离级别改变为可重复读并开启一个一致性事务的快照 , 就是一致性非锁定读。

 

 

 

一致读在某些 DDL 语句下不生效:

 

  • 碰到 drop table 语句时,由于 InnoDB 不能使用被 drop 的表,所以无法实现一致读 。
  • 碰到 alter table 语句时,也无法实现一致读 。
  • 当碰到 insert into… select,update … select 和 create table … select 语句时,在默认的事务隔离级别下,语句的执行更类似于在读取提交数据的隔离级别下。

 

5、自增长与锁

 

自增长在数据库中非常常见的一种属性,也是很多 DBA 或开发人员首选主键方式。在 InnoDB 存储引擎的内存结构中,对每个含有自增长值的表都有一个自增长计数器。

 

插入操作会依据这个自增长的计数器加 1 赋予自增长列。这个实现方式称作 AUTO-INC Locking(自增锁)。 这种自增锁是采用一种特殊的表锁机制,为了提高插入的性能,锁不是在一个事务完成后才释放,而是在完成对自增长值插入的sql 语句后立即释放。

 

AUTO-INC Locking 从一定程度上提高了并发插入的效率,但还是存在一些性能上的问题。

 

  • 首先,对于有自增长值的列的并发插入性能较差,事务必须等待前一个插入完成。
  • 其次,对于 insert …select 的大数据量的插入会影响插入的性能,因为另一个事务中插入会被阻塞。

 

Innodb_autoinc_lock_mode 来控制自增长的模式,改参数的默认值为1。

 

 

 

InnoDB 提供了一种轻量级互斥量的自增长实现机制,大大提高了自增长值插入的性能。提供参数 innodb_autoinc_lock_mode 来控制自增长锁使用的算法,默认值为 1。他允许你在可预测的自增长值和*大化并发插入操作之间进行权衡。

 

插入类型的分类:

 

 

 

innodb_autoinc_lock_mode 在不同设置下对自增长的影响:

 

  • innodb_autoinc_lock_mode = 0 :

    MySQL 5.1.22版本之前自增长的实现方式,通过表锁的 AUTO-INC Locking 方式。

  • innodb_autoinc_lock_mode = 1(默认值): 

    对于『simple inserts』,该值会用互斥量(mutex)对内存中的计数器进行累加操作。对于『bulk inserts』会用传统的 AUTO-INC Locking 方式。这种配置下,如果不考虑回滚,自增长列的增长还是连续的。需要注意的是:如果已经使用 AUTO-INC Locking 方式去产生自增长的值,而此时需要『simple inserts』操作时,还需要等待 AUTO-INC Locking 的释放。

  • innodb_autoinc_lock_mode = 2 :

    对于所有『insert-like』自增长的产生都是通过互斥量,而不是AUTO-INC Locking方式。这是性能*高的方式。但会带来一些问题:因为并发插入的存在,每次插入时,自增长的值是不连续的基于statement-base replication会出现问题。

 

因此,使用这种方式,任何情况下都需要使用row-base replication,这样才能保证*大并发性能和replication的主从数据的一致。

 

二、行锁的几种算法

 

  • Record Lock:单个行记录上的锁 。
  • Gap Lock:间隙锁,锁定一个范围,但不包含记录本身。
  • Next-Key Lock:Gap Lock + Record Lock,锁定一个范围,并且锁定记录本身。
  • Insert Intention Locks:插入意向锁。

 

1、Record Lock

 

Record Lock 总是会去锁住索引记录, 如果 InnoDB 存储引擎表在建立的时候没有设置任何一个索引,那么这是 InnoDB 存储引擎会使用隐式的主键来进行锁定。

 

行级锁是施加在索引行数据上的锁,比如 SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE 语句是在 t.c1=10 的索引行上增加锁,来阻止其他事务对对应索引行的insert/update/delete操作。

 

行锁总是在索引记录上面加锁,即使一张表没有设置任何索引,InnoDB 会创建一个隐藏的聚簇索引,然后在这个索引上加上行锁。例如:

 

create table t (c1 int primary key);

insert into t select 1;

insert into t select 3;

insert into t select 10;

 

# 会话A

start transaction;

update t set c1=12 where c1 = 10 ;

# 会话B:

mysql> update t set c1=11 where c1=10;

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

会阻止该事务对索引行上的修改

 

当一个 InnoDB 表没有任何索引时, 则行级锁会施加在隐含创建的聚簇索引上,所以说当一条 SQL 没有走任何索引时,那么将会在每一条聚集索引后面加 X 锁,这个类似于表锁,但原理上和表锁应该是完全不同的。例:

 

# 删除表t的主键索引

alter table t drop primary key;

开启会话1:

start transaction;

update t set c1=11 where c1=10;

开启会话2:

start transaction;

update t set c1=8 where c1=10;

这个时候发生了锁等待,

这时候开启会话3,锁等待发生了什么:

mysql> select * from sys.innodb_lock_waits\G;

 

如下截图:

 

 

 

2、Gap Lock

 

当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件 的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB 也会对这个”间隙”加锁。

 

间隔锁是施加在索引记录之间的间隔上的锁,锁定一个范围的记录、但不包括记录本身,比如 SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE 语句,尽管有可能对 c1 字段来说当前表里没有=15的值,但还是会阻止=15的数据的插入操作,是因为间隔锁已经把索引查询范围内的间隔数据也都锁住了,间隔锁的使用只在部分事务隔离级(可重复读级)别才是生效的 。

 

间隔锁只会阻止其他事务的插入操作,就是只有 insert 操作会产生 GAP 锁,update 操作不会参数 GAP 锁。例:

 

# 创建keme1 测试数据, 插入模拟数据

create table keme1 (id int primary key,name varchar(10));

insert into keme1 values (1,’a’),(3,’c’), (4,’d’), (5,’e’), (6,’f’);

# 开启三个session 窗口,两个窗口模拟两个事务, 另外一个窗口看 两个事务发生一些间隔锁的信息

session1:

start transaction;

mysql> update keme1 set name=’bb’ where id between 1 and 3;

Query OK, 2 rows affected (0.00 sec)

Rows matched: 2  Changed: 2  Warnings: 0

session2:

start transaction;

mysql> insert into keme1 values (2,’bb’);

# 这时候就有锁等待了

select * from sys.innodb_lock_waits\G;

 

 

 

使用gap lock的前置条件:

 

  • 事务隔离级别为 REPEATABLE-READ,innodb_locks_unsafe_for_binlog 参数为0,且 sql 走的索引为非唯一索引(无论是等值检索还是范围检索)
  • 事务隔离级别为 REPEATABLE-READ,innodb_locks_unsafe_for_binlog 参数为0,且 sql 是一个范围的当前读操作,这时即使不是非唯一索引也会加 gap lock

 

Gap Lock 的作用是为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生。

 

可以通过两种方式来关闭 Gap Lock:

 

  • 将事务的隔离级别设置为 READ COMMITTED
  • 将参数 innodb_locks_unsafe_for_binlog 设置为 1

 

3、Next-Key Lock

 

在默认情况下,MySQL 的事务隔离级别是可重复读,并且 innodb_locks_unsafe_for_binlog 参数为 0,这时默认采用 next-key locks。

 

所谓 Next-Key Locks,就是记录锁和间隔锁的结合,即除了锁住记录本身,还要再锁住索引之间的间隙。

 

当扫描表的索引时,InnoDB 以这种形式实现行级的锁:遇到匹配的的索引记录,在上面加上对应的 S 锁或 X 锁。

 

因此,行级锁实际上是索引记录锁。如果一个事务拥有索引上记录 r 的一个 S 锁或 X 锁,另外的事务无法立即在 r 记录索引顺序之前的间隙上插入一条新的记录。

 

假设有一个索引包含值:10,11,13和20。下列的间隔上都可能加上一个 Next-Key 锁(左开右闭)。

 

(negative infinity, 10]

(10, 11]

(11, 13]

(13, 20]

(20, positive infinity)

 

在*后一个区间中,Next-Key 锁锁定了索引中的*大值到正无穷。

 

默认情况下,InnoDB 启用 RR 事务隔离级别。此时,InnoDB 在查找和扫描索引时会使用 Next-Key 锁,其设计的目的是为了解决『幻读』的出现。

 

当查询的索引含有唯一(主键索引和唯一索引)属性是,InnoDB 存储引擎会对 Next-Key Lock 进行优化,将其降级为 Record Lock ,即仅锁住索引本身,而不是范围。

 

4、Insert Intention Lock

 

插入意向锁是一种在数据行插入前设置的 gap 锁。这种锁用于在多事务插入同一索引间隙时,如果这些事务不是往这段 gap 的同一位置插入数据,那么就不用互相等待。

 

create table keme2 (a int primary key);

insert into keme2 values (10),(11),(13),(20);

开启三个会话窗口

session1:

start transaction;

mysql> select * from keme2 where a > 18 for update;

+—-+

| a  |

+—-+

| 20 |

+—-+

1 row in set (0.00 sec)

session2;

start transaction;

mysql> insert into keme2 select 19;

 

客户端 A 创建了一个 keme2 表,包含 10,11,13,20 四条索引记录,然后去设置一个互斥锁在大于 18 的所有索引记录上。这个互斥锁包含了在 20 记录前的 gap 锁。

 

三、锁问题

 

通过锁机制可以实现事务的隔离性要求,使得事务可以并发地工作。锁提高了并发,但是也有有潜在的问题。不过好在因为事务隔离性的要求,锁只会带来三种问题,如果可以防止这三种情况的发生,哪将不会产生并发异常。

 

1、脏读

 

先了解脏数据、脏页、脏读。

 

脏页:指的是在缓冲池中已近被修改的页,但是还没有刷新到磁盘中,即数据库实例内存中的页和磁盘中的页数据是不一致的,当然在刷新到磁盘之前,日志都已经被写入到了重做日志文件中。

 

脏数据:是指事务对缓冲池中行记录的修改,并且还没有被提交。

 

对于脏页的读取,是非常正常的。脏页是因为数据库实例内存和磁盘的异步造成的,这并不影响数据的一致性(或者说两者*终会达到一致性,即当脏页都刷到磁盘)。并且因为脏页的刷新是异步的,不影响数据库的可用性,因此可以带来性能的提高。

 

脏数据:是指未提交的数据,如果读到脏数据,即一个事务可以读到另外一个事务中未提交的数据,则显然违反了数据库的隔离性。

 

脏读:指的就是在不同的事务下,当前事务可以读到另外事务未提交的数据,简单来说就是可以读到脏数据。

 

脏读示例:

 

create table t (a int primary key);

insert into t values (1);

 

 

 

 

 

会话 A 并没有主动提交 2 这条插入事务,但是在会话 B 读取到了,这就是脏读。

 

2、不可重复读

 

不可重读是在一个事务内读取同一数据集合。在这个事务还没有结束时,另外一个事务也访问同一数据集合,并做了一些 DML 操作。因此在*个事务中的两次读取数据之间,由于第二个事务的修改,那么*个事务两次读到的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的情况,这种情况称为不可重复读。

 

不可重复读和脏读的区别是:脏读示读到未提交的数据,而不可重复读读到确实已近提交的数据。

 

3、丢失更新

 

虽然数据库能阻止更新问题的产生,但是在生产应用还有另一个逻辑意义丢失更新问题,而导致该问题的并不是因为数据库本身的问题。实际上,在所有多用户计算机系统环境下都有可能产生这个问题。比如下面的情况:

 

比如一个用户账号中有 10000 元,他用两个网上银行的客户端分别进行转账操作,*次转账 9000 人民币,因为网络和数据的关系,这时需要等待。

 

但是这时用户操作另一个网上银行客户端,转账 1 元,如果*终两笔操作都成功了,用户账号的余款是 9999 元,*次转的 9000 人民币并没有得到更新,但是在转账的另一个账号却会收到这 9000 元,这导致了结果就是钱变多,而账不平。

 

但是银行了也很聪明啊,个人网银绑定 usb key 的,不会发生这种情况的。是的,通过 usb key 登录也许可以解决这个问题。但是更重要的是在数据库层解决这个问题,避免任何可能发生丢失更新的情况。

 

要避免丢失更新发生 ,需要让事务在这种情况下的操作变成串行化,而不是并行的操作。

 

四、锁阻塞

 

因为不同锁之间的兼容性关系,在有些时刻一个事务中的锁需要等待另一个事务中的锁释放它所占用的资源,这就是阻塞。阻塞并不是一件坏事,其实为了确保事务可以并发正常地运行。

 

在 InnoDB 存储引擎中,参数 innodb_lock_wait_timeout 用来控制等待的时间(默认是50秒), innodb_rollback_on_timeout 用来设定是否在等待超时时对进行中的事务进行回滚操作(默认是off,不回滚)。参数 innodb_lock_wait_timeout 可以在 MySQL 数据库运行时进行调整:

 

在默认情况下 InnoDB 存储引擎不会回滚超时引发的错误异常。其实 InnoDB 存储引擎在大部分情况下都不会对异常进行回滚。

 

查看锁阻塞的信息:

 

select * from information_schema.innodb_trx\G; # 查看当前的事务信息

select * from information_schema.innodb_locks\G; # 查看当前的锁信息

select * from information_schema.innodb_lock_waits\G; # 查看当前的锁等待信息

可以联表查,查找自己想要的结果。

select * from sys.innodb_lock_waits\G; # 查看当前的锁等待信息

show engine innodb status\G;

还可以通过当前执行了执行了什么语句

select * from  performance_schema.events_statements_current\G;

show full processlist;

 

五、死锁

 

死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。

 

1、数据库层面解决死锁的两种方式

 

①解决死锁的问题*简单的方式是不要有等待,将任何的等待都转化为回滚,并且事务重新开始。

 

这种没有死锁问题的产生。在线上环境中,可能导致并发性能的下降,甚至任何一个事务都不能进行。而这锁带来的问题远比死锁问题更为严重,而这锁带来的问题原题远比死锁问题更为严重,因为这很难被发现并且浪费资源。

 

②解决死锁的问题*简单的一种方法时超时,即当两个事务互相等待是,当一个等待时超过设置的某一阈值是,其中一个事务进行回滚,另一个等待的事务就能继续进行。用 innodb_lock_wait_timeout 用来设置超时的时间。

 

超时机制虽然简单,仅通过超时后对事务进行回滚的方式来处理,或者说其根据 FIFO 的顺序选择回滚对象。但若超时的事务所占权重比较大,如事务操作更新很多行(比如某程序猿用死循环来执行一些事务),占用了较多的 undo log,这是采用 FIFO 的方式,就显得不合适了,因为回滚这个事务的时间相对另一个事务所占用的时间可能会更多。

 

在 mysql 5.7.x 和 mysql 5.6.x 对死锁采用的方式:

 

mysql 5.6.x 是用锁等待(超时)的方式来解决, 没有自动解决死锁的问题。

 

 

 

mysql 5.7.x 默认开启了死锁保护机制:

 

 

 

2、死锁演示

 

如果程序是串行的,那么不可能发生死锁。死锁只存在于并发的情况,而数据库本身就是一个并发运行的程序,因此可能会发生死锁。

 

死锁示例:

 

a :创建表

create table temp(id int primary key ,name varchar(10));

insert into temp values(1,’a’),(2,’b’),(3,’c’);

此时表里只有3条数据

执行步骤根据数据顺序来:

1. 事务1:

start transaction;

update temp set name=’aa’ where id=1;

2. 事务2:

start transaction;

update temp set name=’bb’ where id=2;

3. 事务1:update temp set name=’aaa’ where id=2;

这时候3的步骤会有锁等待, 立马执行4,就会马上产生死锁

4. 事务2: update temp set name=’bbb’ where id=1;

 

 

 

3、避免死锁发生的方法

 

在事务性数据库中,死锁是个经典的问题,但只要发生的频率不高,则死锁问题不需要太过担心。死锁应该非常少发生,若经常发生,则系统是不可用。

 

查看死锁的方法有两种:

 

  • 通过 show engine innodb status 命令可以查看*后一个死锁的情况。
  • 通过 innodb_print_all_deadlocks 参数配置可以将所有死锁的信息都打印到 MySQL 的错误日志中。

 

减少死锁发生的方法:

 

  • 尽可能的保持事务小型化,减少事务执行的时间可以减少发生影响的概率。
  • 及时执行 commit 或者 rollback,来尽快的释放锁。
  • 当要访问多个表数据或者要访问相同表的不同行集合时,尽可能的保证每次访问的顺序是相同的。比如可以将多个语句封装在存储过程中,通过调用同一个存储过程的方法可以减少死锁的发生。
  • 增加合适的索引以便语句执行所扫描的数据范围足够小。
  • 尽可能的少使用锁,比如如果可以承担幻读的情况,则直接使用 select 语句,而不要使用 select…for update 语句。
  • 如果没有其他更好的选择,则可以通过施加表级锁将事务执行串行化,*大限度的限制死锁发生。

 

六、事务

 

事务的主要目的了:事务会把数据库从一种一致状态转换为另一种一致状态。在数据库提交工作是,可以确保要么所有修改都已近保存了,要么所有修改都不保存。

 

InnoDB 存储引擎中的事务完全符合 ACID 的特性:

 

  • 原子性 (atomicity)
  • 一致性(consistency)
  • 隔离性(isolation)
  • 持久性(durability)

 

1、了解事务

 

事务可由一条非常简单的 SQL 语句组成,也可以有一组复杂的 SQL 组成。事务是访问并更新数据库中各种数据项的一个程序执行单元,在事务中的操作,要么都做修改,要么都不做这就是事务的目的。

 

事务 ACID 的特性:

 

A (Atomicity),原子性:

 

指整个数据库事务是不可分割的工作单位。只有使事务中所有的数据库操作都执行成功,才算整个事务成功。事务中任何一个 SQL 语句执行失败,已近执行成功的 SQL 语句也必须撤销。数据库状态应该退回到执行事务前的状态。

 

比如 ATM 取款流程:

 

  • 登录 ATM 机平台,验证密码。
  • 从远程银行数据库中,取得账户的信息。
  • 用户在 ATM 输入提取的金额。
  • 从远程银行的数据库中,更新账户信息。
  • ATM 机出款。
  • 用户取钱。

 

整个过程都视为原子操作,某一个步骤失败了,都不能进行下一步。

 

C (consistency),一致性:

 

一致性定义基本可以理解为是事务对数据完整性约束的遵循。这些约束可能包括主键约束、外键约束或是一些用户自定义约束。事务执行的前后都是合法的数据状态,不会违背任何的数据完整性,这就是“一致”的意思。事务是一致性的单位,如果事务中某个动作失败了,系统就可以自动撤销事务——返回事务初始化的状态。

 

I (isolation),隔离性:

 

隔离性还有其他的称呼,如并发控制、可串行化、锁等。事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见。

 

D(durability),持久性:

 

事务一旦提交,其结果就是永久性的(写入了磁盘),即使发生宕机等故障,数据库也能将数据恢复。需要注意的是,只能从事务本身的角度来保证结果的永久性。

 

例如:在事务提交后,所有的变化都是永久的,即使当数据库因为崩溃而需要恢复时,也能保证恢复后提交的数据都不会丢失。

 

但若不是数据库本身发生故障,而是一些外部的原因,如 RAID 卡损坏,自然灾害等原因导致数据库发生问题,那么所有提交的数据可能都会丢失。

 

因此持久性保证事务系统的高可靠性,而不是高可用性。对于高可用性的实现,事务本身并不能保证,需要一些系统来共同配合来完成。

 

2、事务的实现

 

事务的隔离性由锁来实现。原子性,一致性,持久性通过数据库的 redo log 和 undo log 来完成,redo log 成为重做日志,用来保证事务的原子性和持久性。undo log 用来保证事务的一致性。

 

redo 和 undo 的作用都可以视为是一种恢复操作,redo 恢复提交事务修改的页操作,而 undo 回滚行记录到某个特定版本。因此两者记录的内容不同,redo 通常是物理日志,记录的是页的物理修改操作,undo 是逻辑日志,根据每行记录进行记录。

 

1)redo

 

重做日志(redo log)用来实现事务的持久性,即事务 ACID 中的 D。 其中两部分组成:

 

  •  一是内存中的重做日志缓冲(redo log buffer),其实容易丢失的;
  • 二是重做日志文件(redo log file),其是持久的。

 

InnoDB 是事务的存储引擎,其通过 Force Log at Commit 机制实现事务的持久性,即当事务提交(commit)时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的 commit 操作完成才算完成。

 

这里的日志是指重做日志,在 InnoDB 存储引擎中,由两部分组成,即 redo log 和 undo log。

 

redo log 用来保证事务的持久性,undo log 用来帮助事务回滚及多版本控制(mvcc)的功能,redo log 基本上都是顺序写的,在数据库运行不需要对 redo log 的文件进行读取操作。而 undo log 是需要进行随机读写的。

 

为了确保每次日志都写入重做日志文件,在每次都将重做日志缓冲写入重做日志文件后,InnoDB 存储引擎都需要调用一次 fsync 操作。

 

由于重做日志文件打开并没有使用 O_DIRECT 选项,因此重做日志缓冲先写入文件系统缓冲。为了确保重做日志写入磁盘,必须进行一次 fsync 操作。由于 fsync 的效率取决于磁盘的性能,因此磁盘的性能决定了事务的提交的性能,也就是数据库的性能。

 

InnoDB 存储引擎允许用户手工非持久性的情况发生,以此提高数据库的性能。即当事务提交时,日志不写入重做日志文件,而是等待一个时间周期后再执行 fsync 操作。

 

用参数 innodb_flush_log_at_trx_commit 用来控制重做日志刷新到磁盘的策略。该参数默认值为1。

 

改参数可以设置值为 0、1、2

 

 

 

  • 0:表示事务提交时不进行写入重做日志操作,这个操作仅在 master thread 中完成,而在 master thread 中每 1 秒会进行一次重做日志的 fsync 操作。
  • 1:表示每个事务提交时进行写入到重做日志。
  • 2:表示事务提交时将重做日志写入重做日志文件,但仅写入文件系统的缓存中,不进行 fsync 操作。在这个设置下,当 MySQL 数据库发生宕机(就是数据库服务意外停止)而操作系统不发生宕机是,不会导致事务的丢失。而当操作系统宕机时,重启数据库后会丢失未从文件系统缓存刷新到重做日志文件那部分事务。

 

2)undo

 

重做日志记录了事务的行为,可以很好地通过其对页进行“重做”操作,但是事务有时还需要进行回滚操作,这时就需要 undo。 因此在对数据库进行修改时,InnoDB 存储引擎不但会产生 redo,还会产生一定量的 undo。这样如果用户执行的事务或语句由于原因失败了,又或者用户用一条 rollback 语句请求回滚,就可以利用这些 undo 信息将数据回滚到修改之前的样子。

 

redo 存放在重做日志文件中,与 redo 不同,undo 存放在数据库内部的一个特殊段(segment)中,这个段称为 undo 段 。undo 段位于共享表空间内。

 

undo 是逻辑日志,因此只是将数据库逻辑地恢复到原来的样子。所有修改都被逻辑地取消了,但是数据结构和页本身在回滚之后可能大不相同。这是因为在多用户并发系统中,可能会有数十,数百甚至数千个并发事务。数据库的主要任务就是协调对数据记录的并发访问。比如,一个事务在修改当前一个页中某几条记录,同时还有别的事务在对同一个页中另几条记录进行修改。因此,不能将一个页回滚到事务开始的样子,因为这样会影响其他事务正在进行的工作。

 

undo 除了回滚操作,undo 的另一个作用是mvcc,即在InnoDB 存储引擎中mvcc 的实现是通过undo 来完成。当用户读取一行记录时,若该记录已近被其他事务占用,当前事务可以通过undo 读取之前的行版本信息,以此实现 非锁定读取。

 

*重要的一点是,undo log 会产生redo log ,也就是undo log 的产生会伴随着redo log 的产生,这是因为undo log 也需要持久性的保护。

 

undo 存储管理

 

InnoDB 存储引擎有 rollback segment ,每个回滚段中记录了 1024 个undo log segment , 而在每个 undo log segment 段中进行 undo 页的申请。

 

InnoDB 支持*大128 个(回滚段)rollback segment ,故其支持同时在线的事务 128 * 1024,但是这些 rollback segment 都存储于共享表空间中。可以通过参数对 rollback segment 做进一步的设置。这些参数包括:

 

 

 

innodb_undo_directory

innodb_undo_logs

innodb_undo_tablespaces

 

innodb_undo_directory 用于设置 rollback segment 文件所在的路径。这意味着 rollback segment 可以放在共享表空间以外的位置,即可以设置为独立表空间。该参数的默认值为”.”,表示当前 InnoDB 存储引擎的目录。

 

innodb_undo_logs 用来设置 rollback segment 的个数,默认值为 128。

 

innodb_undo_tablespaces 用来设置构成 rollback segment 文件的数量,这样 rollback segment 可以较为平均地分布在多个文件。设置改参数后,会在路劲innodb_undo_directory 看到 undo 为前缀的文件,该文件就代表 rollback segment 文件。

 

数据库初始化后,innodb_undo_tablespaces 就再也不能被改动了;默认值为0,表示不独立设置undo的tablespace,默认记录到ibdata中;否则,则在undo目录下创建这么多个undo文件,例如假定设置该值为4,那么就会创建命名为undo001~undo004的undo tablespace文件,每个文件的默认大小为10M。修改该值会导致Innodb无法完成初始化,数据库无法启动,但是另两个参数可以修改。

 

3)purge

 

delete 和 update 操作可能并不直接删除原有的数据。

 

 

 

例如执行:

 

delete from z where a=1;

 

表z 上列a 有聚集索引,列表上有辅助索引,对于上述的delete 操作,在undo log 将主键列等于1 的记录delete flag 设置为1 ,记录并没有立即删除,记录还是存在B+树种,其次,对辅助索引上a 等于1 ,b等于1 的记录同样没有做任何处理,甚至没有产生undo log 。 而真正删除这行记录的删除操作其实被“延时”了,*终在purge 操作中完成。

 

purge 用于*终完成delete 和 update 操作。 因为InnoDB 存储引擎支持MVCC,所以记录不能再事务提交时立即进行处理。这时其他事务可能正在引用这行,故InnoDB 存储引擎需要保持记录之前的版本。而是否可以删除该条记录通过purge 来进行判断。若该行记录已不被任何其他事务引用,那么就可以进行真正的delete 操作。可见,purge 操作是清理之前的delete 和 update 操作, 将上述操作 “*终” 完成。 而实际执行的操作为delete 操作,清理之前行记录的版本。

 

4)group commit

 

5.6 版本之前的两次提交 :

 

若事务为非只读事务,则每次事务提交时需要进行一次fsync 操作,以此保证重做日志都已近写入磁盘。当数据库发生宕机时,可以通过重做日志进行恢复。虽然固态硬盘的出现提高了磁盘的性能,然后磁盘的rsync 性能是有限的。为了提高磁盘fsync 的效率,数据库提供了group commit 的功能,即一次fsync 可以刷新确保多个事务日志被写入文件。

 

对于InnoDB 存储引擎来说, 事务提交时会进行两个阶段的操作:

 

  • 修改内存中事务对应的信息,并且将日志写入重做日志缓冲。
  • 调用fsync 将确保日志都从重做日志缓冲写入磁盘。

 

步骤 1 相对步骤 2 是一个较慢的过程,这是因为存储引擎需要与磁盘打交道。但当有事务进行这个过程是,其他事务可以进行步骤 1 的 操作,正在提交的事务完成提交操作,再次进行步骤 2 时,可以将多个事务的重做日志通过一次fsync 刷新到磁盘,这样就大大减少了磁盘的压力,从而提高了数据库的整体性能。对于写入或更新较为频繁的操作,group commit 的效果尤为明显。

 

二段提交流程:

 

 

 

  • InnoDB 的事务 Prepare 阶段,即 SQL 已经成功执行并生成 redo 和 undo 的内存日志;
  • binlog 提交,通过 write() 将 binlog 内存日志数据写入文件系统缓存;
  • fsync() 将 binlog 文件系统缓存日志数据永久写入磁盘;
  • InnoDB 内部提交,commit 阶段在存储引擎内提交,通过 innodb_flush_log_at_trx_commit 参数控制,使 undo 和 redo 永久写入磁盘。

 

组提交 :

 

5.6 引入了组提交,并将提交过程分成 Flush stage、Sync stage、Commit stage 三个阶段。

 

  • InnoDB, Prepare : SQL 已经成功执行并生成了相应的 redo 和 undo 内存日志;
  • Binlog, Flush Stage :所有已经注册线程都将写入 binlog 缓存;
  • Binlog, Sync Stage :binlog 缓存将 sync 到磁盘,sync_binlog=1 时该队列中所有事务的 binlog 将永久写入磁盘;
  • InnoDB, Commit stage: leader 根据顺序调用存储引擎提交事务;

 

每个 Stage 阶段都有各自的队列,从而使每个会话的事务进行排队,提高并发性能。

 

如果当一个线程注册到一个空队列时,该线程就做为该队列的 leader,后注册到该队列的线程均为 follower,后续的操作,都由 leader 控制队列中 follower 行为。

 

参考网址:https://www.linuxidc.com/Linux/2018-01/150187.htm

 

参数 binlog_max_flush_queue_time 用来控制 flush 阶段中等待的时间,即使之前的一组事务完成提交,当前一组的事务也不马上进去 sync 阶段,而是至少需要等待一段时间。

 

这样做的好处是 group commit 的数量更多,然而这也可能会导致事务的相应时间变慢。该参数的默认值为 0,且推荐设置依然为 0。除非用户的 MySQL 数据库系统中有着大量的连接,并且不断地在进行事务的写入或更新操作。

 

 

 

注:任何参数都不要随意设置,看到别人设置参数能解决,为什么我的环境设置就报错了,看官方的改参数注意事项,各种版本的注意事项,在去相应测试环境实验一下。

 

3、事务控制语句

 

在 MySQLl 命令行的默认设置下,事务都是自动提交(auto commit)的,即执行 SQL 语句就会马上执行 commit 操作。

 

用户可以使用那些事务控制语句:

 

  • start transaction | begin :显示地开启一个事务(推荐start transaction)
  • commit:会提交事务,并使得已对数据库做的所有修改成为永久性的
  • rollback:回滚用户当前锁执行的事务,并撤销正在进行的所有未提交的修改。
  • savepoint identifer:savepoint 允许在事务中创建一个保存点,一个事务中可以有多个savepoint。
  • release savepoint identifier:删除一个事务的保存点,当没有一个保存点执行这句语句时,会抛出一个异常。
  • rollback to[savepoint] identifer:这个语句与savepoint 命令一起使用。可以把事务回滚到标记点,而不会滚在此标记点之前的任何工作。
  • set transaction:这个语句用来设置事务的隔离级别。InnoDB 存储引擎的事务隔离级别有:READ UNCOMMITED、READ COMMITED、REPEATABLE READ、SERIALIZABLE。

 

例:

 

mysql> create table u (a int primary key);

Query OK, 0 rows affected (0.01 sec)

mysql> start transaction;

Query OK, 0 rows affected (0.00 sec)

mysql> insert into u select 1;

Query OK, 1 row affected (0.00 sec)

Records: 1  Duplicates: 0  Warnings: 0

mysql> savepoint u1;

Query OK, 0 rows affected (0.00 sec)

mysql> insert into u select 2;

Query OK, 1 row affected (0.00 sec)

Records: 1  Duplicates: 0  Warnings: 0

mysql> savepoint u2;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from u\G;

****** 1. row ******

a: 1

****** 2. row ******

a: 2

2 rows in set (0.00 sec)

mysql> release savepoint u1;

Query OK, 0 rows affected (0.00 sec)

# 回到了*次插入数据的时候

mysql> insert into u select 2;

ERROR 1062 (23000): Duplicate entry ‘2’ for key ‘PRIMARY’

mysql> rollback to savepoint u2;

ERROR 1305 (42000): SAVEPOINT u2 does not exist

mysql> select * from u;

+—+

| a |

+—+

| 1 |

| 2 |

+—+

2 rows in set (0.00 sec)

# 这时候发现了,rollback to savepoint u1了,

后面的u2 的 事务已近不存在了, 但是两条记录的数据还在。

mysql> rollback;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from u;

Empty set (0.00 sec)

 

在上面的列子中,虽然在发生重复错误后用户通过 rollback to save point u1 命令回滚到了保存点 u1,但是事务此时没有结束。在运行命令 rollback 后,事务才会完整地回滚。

 

InnoDB 存储引擎中的事务都是原子的,这说明下两种情况:

 

构成事务的每天语句都会提交(成为永久),或者所有语句都回滚。这种保护还延伸到单个的语句。一条语句要么完全成功。要么完全回滚(注意,这里说的是语句回滚)。

 

因此一条语句失败并抛出异常时,并不会导致先前已近执行的语句自动回滚。所有的执行都会得到保留,必须由用户自己来决定是否对其进行提交或回滚的操作。

 

rollback to savepoint 命令并不真正地结束事务。

 

commit 和 rollback 才是真正的结束一个事务

 

4、隐式提交的SQL语句

 

以下这些 SQL 语句会产品一个隐式的提交操作即执行完这些语句后,会有一个隐式的 commit 操作:

 

DDL 语句:

 

ALTER DATABASE … UPGRADE DATA DIRECTORY NAME,<br data-filtered=”filtered”>ALTER EVENT,ALTER PROCEDURE,ALTER TABLE ,ALTER VIEW, <br data-filtered=”filtered”>CREATE DATABASE, CREATE EVENT, CREATE TRIGGER , CREATE VIEW, <br data-filtered=”filtered”>DROP DATABASE ,DROP EVENT , DROP INDEX , DROP PROCEDURE , DROP TABLE , DROP TRIGGER , DROP VIEW ,<br data-filtered=”filtered”>RENAME TABLE , TRUNCATE TABLE .

 

用来隐式修改 MySQL 架构的操作:

 

CREATE USER,DROP USER ,GRANT , RENAME USER ,REVOKE , SET PASSWORD.

 

管理语句:

 

ANALYZE TABLE,CACHE INDEX, CHECK TABLE ,<br data-filtered=”filtered”>LOAD INDEX INTO CACHE,OPTIMEIZE TABLE ,REPAIR TABLE

 

注: 我发现 sql server 的数据库有些 ddl 也是可以回滚的。这和 InnoDB 存储引擎,oracle 这些数据库完全不同。

 

truncate table 演示:

 

mysql> insert into u select 1;

Query OK, 1 row affected (0.01 sec)

Records: 1  Duplicates: 0  Warnings: 0

mysql> insert into u select 2;

Query OK, 1 row affected (0.01 sec)

Records: 1  Duplicates: 0  Warnings: 0

mysql> select * from u;

+—+

| a |

+—+

| 1 |

| 2 |

+—+

2 rows in set (0.00 sec)

mysql> start transaction;

Query OK, 0 rows affected (0.00 sec)

mysql> truncate table u;

Query OK, 0 rows affected (0.00 sec)

mysql> rollback;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from u;

Empty set (0.00 sec)

 

5、对于事务的操作的统计

 

由于 InnoDB 存储引擎是支持事务的,因此 InnoDB 存储引擎的应用需要在考虑每秒请求数(transaction per second ,TPS)

 

计算 TPS 的方法时( com_commit + com_rollback)/time 。但是利用这种方法进行计算的前提是:

 

所有的事务必须都是显示提交的,如果存在隐式提交和回滚(默认autocommit =1 ),不会计算到com_commit 和 com_rollback 变量中。 如:

 

 

 

MySQL 数据库中另外还有两个参数 handler_commit 和 handler_rollback 用于事务的统计操作。可以很好的用来统计 InnoDB 存储引擎显式和隐式的事务提交操作。

 

在 InnoDB Plugin 中这两个参数的表现有些“怪异”,如果用户的程序都是显示控制事务的提交和回滚,那么可以通过com_commit 和 com_rollback 进行统计。

 

6、事务的隔离级别

 

SQL 标准定义的四个隔离级别为:

 

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

 

sql server 和oracle 默认的隔离级别是 READ COMMITED。

 

 

 

  • 脏读:又称无效数据的读出,是指在数据库访问中,事务T1将某一值修改,然后事务T2读取该值,此后T1因为某种原因撤销对该值的修改,这就导致了T2所读取到的数据是无效的。
  • 不可重复读:是指在数据库访问中,一个事务范围内两个相同的查询却返回了不同数据。
  • 幻读:是指当事务不是独立执行时发生的一种现象,例如*个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。

 

不可能重复读和幻读的区别:

 

很多人容易搞混不可重复读和幻读,确实这两者有些相似。但不可重复读重点在于 update 和 delete,而幻读的重点在于 insert。

 

在 InnoDB 引擎中,可以使用一下命令来设置当前会话和全局的事务的隔离级别:

 

mysql> help isolation;

Name: ‘ISOLATION’

Description:

Syntax:

SET [GLOBAL | SESSION] TRANSACTION

transaction_characteristic [, transaction_characteristic] …

transaction_characteristic: {

ISOLATION LEVEL level

| READ WRITE

| READ ONLY

}

level: {

REPEATABLE READ

| READ COMMITTED

| READ UNCOMMITTED

| SERIALIZABLE

}

 

如果想在 MySQL 数据启动时就设置事务的默认隔离级别,那就需要修改 MySQL 的配置文件 my.cnf 在 [mysqld] 中添加如下行:

 

[mysqld]

transaction-isolation = REPEATABLE-READ

 

查看当前会话的事务隔离级别,可以使用:

 

mysql> select @@tx_isolation\G;

********** 1. row **********

@@tx_isolation: REPEATABLE-READ

1 row in set, 1 warning (0.00 sec)

 

查看全局的事务隔离级别,可以使用:

 

mysql> select @@global.tx_isolation\G;

******** 1. row ********

@@global.tx_isolation: REPEATABLE-READ

1 row in set, 1 warning (0.00 sec)

 

7、不好的事务的习惯

 

在循环中提交

 

用存储过程模拟一下:

 

create table t1 (a int ,b char(100));

创建load1

delimiter //

create procedure load1 (count INT UNSIGNED)

begin

declare s int unsigned default 1;

declare c char(80) default repeat(‘a’,80);

while s <= count do

insert into t1 select null,c;

commit;

set s = s+1;

end while;

end //

delimiter ;

创建load2

delimiter //

create procedure load2 (count int unsigned)

begin

declare s int unsigned default 1;

declare c char(80) default repeat(‘a’,80);

while s <= count do

insert into t1 select null,c;

set s = s+1;

end while;

end //

delimiter ;

创建load3

delimiter //

create procedure load3(count int unsigned)

begin

declare s int unsigned default 1;

declare c char(80) default repeat(‘a’,80);

start transaction;

while s <= count do

insert into t1 select null,c;

set s = s+1;

end while;

commit;

end //

delimiter ;

 

比较这三个存储过程执行时间:

 

mysql> call load1(20000);

Query OK, 0 rows affected (16.12 sec)

mysql> truncate table t1;

Query OK, 0 rows affected (0.01 sec)

mysql> call load2(20000);

Query OK, 1 row affected (16.06 sec)

mysql> truncate table t1;

Query OK, 0 rows affected (0.01 sec)

mysql> call load3(20000);

Query OK, 0 rows affected (0.51 sec)

 

 

注:mysql 默认是自动提交的,load1 和 load2 没执行一次都会自动提交。

 

显然,load3 方法要快的多,这是因为每一次提交都要写一次重做日志,存储过程 load1 和 load2 实际写了 20000 次重做日志文件,而对于存储过程 load3 来说,实际只写了一次。

 

8、长事务

 

长事务就是执行时间较长的事务。比如对于银行系统的数据库,没过一个阶段可能需要更新对应账户的利息。如果对应账号的数量非常大,例如对有 1 亿用户的表 account ,需要执行以下列语句;

 

update accout set account_total= accoutn_total + (1+inerset_rate)

 

这是这个事务可能需要非常长的时间来完成。可能需要 1 个小时,也可能 4、5 个小时,这取决于数据库的硬件配置。DBA 和开发人员本身能做的事情非常少。

 

然而,由于事务 ACID 的特性,这个操作被封装在一个事务中完成。这就产生了一个问题,在执行过程中,当数据库或操作系统,硬件等发生问题是,重新开始事务的代价变得不可接受。

 

数据库需要回滚所有已近发生的变化,而这个过程可能比产生这些变化的时间还要长。因此,对于长事务的问题,有时可以通过转化为小批量的事务来进行处理。当事务发生错误是,只需要回滚一部分数据,然后接着上次已完成的事务继续进行。

Android view滑动冲突的完美解决方案 listview和scroView

ViewPager已经为我们处理了滑动冲突!

 

针对上面*种场景,由于外部与内部的滑动方向不一致,那么我们可以根据当前滑动方向,水平还是垂直来判断这个事件到底该交给谁来处理。至于如何获得滑动方向,我们可以得到滑动过程中的两个点的坐标。如竖直距离与横向距离的大小比较;

 

针对第二种场景,由于外部与内部的滑动方向一致,那么不能根据滑动角度、距离差或者速度差来判断。这种情况下必需通过业务逻辑来进行判断。比较常见ScrollView嵌套了ListView。

 

 

滑动冲突解决套路
套路一 外部拦截法:

即父View根据需要对事件进行拦截。逻辑处理放在父View的onInterceptTouchEvent方法中。我们只需要重写父View的onInterceptTouchEvent方法,并根据逻辑需要做相应的拦截即可。

public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
if (满足父容器的拦截要求) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
上面伪代码表示外部拦截法的处理思路,需要注意下面几点

根据业务逻辑需要,在ACTION_MOVE方法中进行判断,如果需要父View处理则返回true,否则返回false,事件分发给子View去处理。
ACTION_DOWN 一定返回false,不要拦截它,否则根据View事件分发机制,后续ACTION_MOVE 与 ACTION_UP事件都将默认交给父View去处理!
原则上ACTION_UP也需要返回false,如果返回true,并且滑动事件交给子View处理,那么子View将接收不到ACTION_UP事件,子View的onClick事件也无法触发。而父View不一样,如果父View在ACTION_MOVE中开始拦截事件,那么后续ACTION_UP也将默认交给父View处理!
套路二 内部拦截法:

即父View不拦截任何事件,所有事件都传递给子View,子View根据需要决定是自己消费事件还是给父View处理。这需要子View使用requestDisallowInterceptTouchEvent方法才能正常工作。下面是子View的dispatchTouchEvent方法的伪代码:

public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x – mLastX;
int deltaY = y – mLastY;
if (父容器需要此类点击事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}

mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
父View需要重写onInterceptTouchEvent方法:

public boolean onInterceptTouchEvent(MotionEvent event) {

int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
使用内部拦截法需要注意:

内部拦截法要求父View不能拦截ACTION_DOWN事件,由于ACTION_DOWN不受FLAG_DISALLOW_INTERCEPT标志位控制,一旦父容器拦截ACTION_DOWN那么所有的事件都不会传递给子View。
滑动策略的逻辑放在子View的dispatchTouchEvent方法的ACTION_MOVE中,如果父容器需要获取点击事件则调用 parent.requestDisallowInterceptTouchEvent(false)方法,让父容器去拦截事件。

总结:

发现内部拦截和外面拦截的重要,

ACTION_DOWN,都不要拦截子类
要在
MotionEvent.ACTION_MOVE根据情况,是父类滑动还是子类滑动
可以看出外部拦截法实现起来更加简单,而且也符合View的正常事件分发机制,所以推荐使用外部拦截法(重写父View的onInterceptTouchEvent,父View决定是否拦截)来处理滑动冲突

 

Android 3步搞定事件分发机制,再也不用担心onTouch和onTouchEvent&dispatchTouchEvent

事件分发机制分为2种:View事件的分发和ViewGroup事件分发机制

先看简单的View事件分发机制

//子控件的ontouch方法影响子控件的函数
//onTouch====onTouchEvent====onClick;
/**
* 检验view的事件分发顺序,点击—dispatch- Ontouch返回值为ture 不执行—ontouchEvent—onclick
*/
button1.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(“TAG”, “button1 on touch”+event.getAction());
return true;
}
});

/**
* 检验view的事件分发顺序, 点击—dispatch- Ontouch返回值为false执行—ontouchEvent—onclick
*/
button2.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(“TAG”, “button1 on touch” + event.getAction());
return false;
}
});
然后我们来看一下View中dispatchTouchEvent方法的源码:

 

public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
 

1、整个View的事件转发流程是:

View.dispatchEvent->View.setOnTouchListener->View.onTouchEvent

在dispatchTouchEvent中会进行OnTouchListener的判断,如果OnTouchListener不为null且返回true,则表示事件被消费,onTouchEvent不会被执行;否则执行onTouchEvent。

onClick方法是在onTouchEvent方法里调用的

 

2个Button的onTouch返回值验证了以下:

onTouchListener的onTouch方法优先级比onTouchEvent高,会先触发。

假如onTouch方法返回false会接着触发onTouchEvent,反之onTouchEvent方法不会被调用。

 

Button和ImageView效果不一样:一个是自带点击,一个是要自己控制点击

onTouch能够得到执行需要两个前提条件,*mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。

/**ImageView默认是不能点击事件的,要想点击的话必须手动设置*/
imageView.setClickable(true);

imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e(“TAG”,”imageView setOnTouchListener”);
}
});

ViewGruop的事件分发:

多了一个拦截事件的方法:onInterceptTouchEvent

 

比较好的形容:

其中Activity和View控件(TextView)拥有分派和处理事件方法,View容器(LinearLayout)具有分派,拦截,处理事件方法。

这里也有个比喻:领导都会把任务向下分派,一旦下面的人把事情做不好,就不会再把后续的任务交给下面的人来做了,只能自己亲自做,

如果自己也做不了,就只能告诉上级不能完成任务,上级又会重复他的过程。

另外,领导都有权利拦截任务,对下级隐瞒该任务,而直接自己去做,如果做不成,也只能向上级报告不能完成任务。

 

/**默认为false,不拦截子控件的监听*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
public boolean dispatchTouchEvent(MotionEventev);  //用来分派event
public boolean onInterceptTouchEvent(MotionEventev);//用来拦截event
public boolean onTouchEvent(MotionEventev);//用来处理event

总结:

1. Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View的。

2. 在ViewGroup中可以通过onInterceptTouchEvent方法对事件传递进行拦截,onInterceptTouchEvent方法返回true代表不允许事件继续向子View传递,返回false代表不对事件进行拦截,默认返回false。

3.拦截的好好处在于调用谁的dispatchTouchEvent的方法,谁出来点击事件

4. 子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。

5.在ViewGroup中onInterceptTouchEvent方法若反回false,那么触屏事件会继续向下传递,

但如果没有子View去处理这个事件,即子view的onTouchEvent没有返回True

则*后还是由ViewGroup去处理这个事件,也就又执行了自己的onTouchEvent。

 

下面是方法总结:

OnTouch方法是在Activity里面设置的监听事件

1. onTouch和onTouchEvent有什么区别,又该如何使用?

从源码中可以看出,这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。

 

思考的问题:

1.View的dispatchTouchEvent重写了会怎么样,View的onTouchEvent重写了会怎样?

2.ViewGroup的dispatchTouchEvent重写了会怎么样,ViewGroup的onTouchEvent重写了会怎样?

3.ListView上的button,点击button2个控件同时要有相应,应该怎么处理?

4.

事件的传递顺序:

 

G—–>A—–>B—–>C—–>D—–>E—–>F

 

 

深入理解MYSQL的MDL元数据锁

前言

好久没更新,主要是因为Inside君*近沉迷于一部动画片——《新葫芦娃兄弟》。终于抽得闲,完成了本篇关于MySQL MDL锁的深入分析与介绍。虽然之前有很多小伙伴分析过,但总感觉少了点什么,故花了点时间翻看了下源码。Inside君或许不是*牛掰的内核开发人员,但自认为应该是业界*会讲故事的码农,希望本篇能做到通俗易懂,因为MDL锁其实并不好理解。如果同学们还有问题,也可以直接看源码文件mdl.cc。

MDL锁与实现

MySQL5.5版本引入了MDL锁(metadata lock),用于解决或者保证DDL操作与DML操作之间的一致性。例如下面的这种情形:

会话1 会话2
BEGIN;
SELECT * FROM XXX
DROP TABLE XXX
SELECT * FROM XXX

若没有MDL锁的保护,则事务2可以直接执行DDL操作,并且导致事务1出错,5.1版本即是如此。5.5版本加入MDL锁就在于保护这种情况的发生,由于事务1开启了查询,那么获得了MDL锁,锁的模式为SHARED_READ,事务2要执行DDL,则需获得EXCLUSIVE锁,两者互斥,所以事务2需要等待。

InnoDB层已经有了IS、IX这样的意向锁,有同学觉得可以用来实现上述例子的并发控制。但由于MySQL是Server-Engine架构,所以MDL锁是在Server中实现。另外,MDL锁还能实现其他粒度级别的锁,比如全局锁、库级别的锁、表空间级别的锁,这是InnoDB存储引擎层不能直接实现的锁。

但与InnoDB锁的实现一样,MDL锁也是类似对一颗树的各个对象从上至下进行加锁(对树进行加锁具体见:《MySQL技术内幕:InnoDB存储引擎》)。但是MDL锁对象的层次更多,简单来看有如下的层次:

 

上图中显示了*常见的4种MDL锁的对象,并且注明了常见的SQL语句会触发的锁。与InnoDB层类似的是,某些类型的MDL锁会从上往下一层层进行加锁。比如LOCK TABLE … WRITE这样的SQL语句,其首先会对GLOBAL级别加INTENTION_EXCLUSIVE锁,再对SCHEMA级别加INTENTION_EXCLUSIVE锁,*后对TABLE级别加SHARED_NO_READ_WRITE锁。

这里*令人意外的是还有COMMIT对象层次的锁,其实这主要用于XA事务中。比如分布式事务已经PREPARE成功,但是在XA COMMIT之前有其他会话执行了FLUSH TABLES WITH READ LOCK这样的操作,那么分布式事务的提交就需要等待。

除了上图标注的对象,其实还有TABLESPACE、FUNCTION、PROCEDURE、EVENT等其他对象类型,其实都是为了进行并发控制。只是这些在MySQL数据库中都不常用,故不再赘述(当然也是为了偷懒)。

GLOBAL=0,TABLESPACE,SCHEMA,TABLE,FUNCTION,PROCEDURE,TRIGGER,EVENT,COMMIT,USER_LEVEL_LOCK,LOCKING_SERVICE,NAMESPACE_END

 

目前MDL有如下锁模式,锁之间的兼容性可见源码mdl.cc:

锁模式 对应SQL
MDL_INTENTION_EXCLUSIVE GLOBAL对象、SCHEMA对象操作会加此锁
MDL_SHARED FLUSH TABLES with READ LOCK
MDL_SHARED_HIGH_PRIO 仅对MyISAM存储引擎有效
MDL_SHARED_READ SELECT查询
MDL_SHARED_WRITE DML语句
MDL_SHARED_WRITE_LOW_PRIO 仅对MyISAM存储引擎有效
MDL_SHARED_UPGRADABLE ALTER TABLE
MDL_SHARED_READ_ONLY LOCK xxx READ
MDL_SHARED_NO_WRITE FLUSH TABLES xxx,yyy,zzz READ
MDL_SHARED_NO_READ_WRITE FLUSH TABLE xxx WRITE
MDL_EXCLUSIVE ALTER TABLE xxx PARTITION BY …

MDL锁的性能与并发改进

讲到这同学们会发现MDL锁的开销并不比InnoDB层的行锁要小,而且这可能是一个更为密集的并发瓶颈。MySQL 5.6和5.5版本通常通过调整如下两个参数来进行并发调优:

  • metadata_locks_cache_size: MDL锁的缓存大小
  • metadata_locks_hash_instances:通过分片来提高并发度,与InnoDB AHI类似

MySQL 5.7 MDL锁的*大改进之处在于将MDL锁的机制通过lock free算法来实现,从而提高了在多核并发下数据库的整体性能提升。

MDL锁的诊断

MySQL 5.7版本之前并没有提供一个方便的途径来查看MDL锁,github上有一名为mysql-plugin-mdl-info的项目,通过插件的方式来查看,非常有想法的实现,大赞。好在官方也意识到了这个问题,于是在MySQL 5.7中的performance_schea库下新增了一张表metadata_locks,用其来查看MDL锁那是相当的方便:

不过默认PS并没有打开此功能,需要手工将wait/lock/metadata/sql/mdl监控给打开:

SELECT * FROM performance_schema.setup_instruments;

  UPDATE performance_schema.setup_consumers SET ENABLED = 'YES' WHERE NAME ='global_instrumentation';
  UPDATE performance_schema.setup_instruments SET ENABLED = 'YES' WHERE NAME ='wait/lock/metadata/sql/mdl';
  select * from performance_schema.metadata_locks\G

 

会话1

mysql> lock table xx read;
Query OK, 0 rows affected (0.21 sec)

会话2:

SELECT * FROM performance_schema.metadata_locks;

复制代码

mysql> SELECT * FROM performance_schema.metadata_locks\G;
*************************** 1. row ***************************
          OBJECT_TYPE: TABLE
        OBJECT_SCHEMA: performance_schema
          OBJECT_NAME: metadata_locks
OBJECT_INSTANCE_BEGIN: 240554768
            LOCK_TYPE: SHARED_READ
        LOCK_DURATION: TRANSACTION
          LOCK_STATUS: GRANTED
               SOURCE: sql_parse.cc:5927
      OWNER_THREAD_ID: 38
       OWNER_EVENT_ID: 10
1 row in set (0.00 sec)

ERROR:
No query specified

复制代码

复制代码

enum enum_mdl_type {
  /*
    An intention exclusive metadata lock. Used only for scoped locks.
    Owner of this type of lock can acquire upgradable exclusive locks on
    individual objects.
    Compatible with other IX locks, but is incompatible with scoped S and
    X locks.
  */
  MDL_INTENTION_EXCLUSIVE= 0,
  /*
    A shared metadata lock.
    To be used in cases when we are interested in object metadata only
    and there is no intention to access object data (e.g. for stored
    routines or during preparing prepared statements).
    We also mis-use this type of lock for open HANDLERs, since lock
    acquired by this statement has to be compatible with lock acquired
    by LOCK TABLES ... WRITE statement, i.e. SNRW (We can't get by by
    acquiring S lock at HANDLER ... OPEN time and upgrading it to SR
    lock for HANDLER ... READ as it doesn't solve problem with need
    to abort DML statements which wait on table level lock while having
    open HANDLER in the same connection).
    To avoid deadlock which may occur when SNRW lock is being upgraded to
    X lock for table on which there is an active S lock which is owned by
    thread which waits in its turn for table-level lock owned by thread
    performing upgrade we have to use thr_abort_locks_for_thread()
    facility in such situation.
    This problem does not arise for locks on stored routines as we don't
    use SNRW locks for them. It also does not arise when S locks are used
    during PREPARE calls as table-level locks are not acquired in this
    case.
  */
  MDL_SHARED,
  /*
    A high priority shared metadata lock.
    Used for cases when there is no intention to access object data (i.e.
    data in the table).
    "High priority" means that, unlike other shared locks, it is granted
    ignoring pending requests for exclusive locks. Intended for use in
    cases when we only need to access metadata and not data, e.g. when
    filling an INFORMATION_SCHEMA table.
    Since SH lock is compatible with SNRW lock, the connection that
    holds SH lock lock should not try to acquire any kind of table-level
    or row-level lock, as this can lead to a deadlock. Moreover, after
    acquiring SH lock, the connection should not wait for any other
    resource, as it might cause starvation for X locks and a potential
    deadlock during upgrade of SNW or SNRW to X lock (e.g. if the
    upgrading connection holds the resource that is being waited for).
  */
  MDL_SHARED_HIGH_PRIO,
  /*
    A shared metadata lock for cases when there is an intention to read data
    from table.
    A connection holding this kind of lock can read table metadata and read
    table data (after acquiring appropriate table and row-level locks).
    This means that one can only acquire TL_READ, TL_READ_NO_INSERT, and
    similar table-level locks on table if one holds SR MDL lock on it.
    To be used for tables in SELECTs, subqueries, and LOCK TABLE ...  READ
    statements.
  */
  MDL_SHARED_READ,
  /*
    A shared metadata lock for cases when there is an intention to modify
    (and not just read) data in the table.
    A connection holding SW lock can read table metadata and modify or read
    table data (after acquiring appropriate table and row-level locks).
    To be used for tables to be modified by INSERT, UPDATE, DELETE
    statements, but not LOCK TABLE ... WRITE or DDL). Also taken by
    SELECT ... FOR UPDATE.
  */
  MDL_SHARED_WRITE,
  /*
    A version of MDL_SHARED_WRITE lock which has lower priority than
    MDL_SHARED_READ_ONLY locks. Used by DML statements modifying
    tables and using the LOW_PRIORITY clause.
  */
  MDL_SHARED_WRITE_LOW_PRIO,
  /*
    An upgradable shared metadata lock which allows concurrent updates and
    reads of table data.
    A connection holding this kind of lock can read table metadata and read
    table data. It should not modify data as this lock is compatible with
    SRO locks.
    Can be upgraded to SNW, SNRW and X locks. Once SU lock is upgraded to X
    or SNRW lock data modification can happen freely.
    To be used for the first phase of ALTER TABLE.
  */
  MDL_SHARED_UPGRADABLE,
  /*
    A shared metadata lock for cases when we need to read data from table
    and block all concurrent modifications to it (for both data and metadata).
    Used by LOCK TABLES READ statement.
  */
  MDL_SHARED_READ_ONLY,
  /*
    An upgradable shared metadata lock which blocks all attempts to update
    table data, allowing reads.
    A connection holding this kind of lock can read table metadata and read
    table data.
    Can be upgraded to X metadata lock.
    Note, that since this type of lock is not compatible with SNRW or SW
    lock types, acquiring appropriate engine-level locks for reading
    (TL_READ* for MyISAM, shared row locks in InnoDB) should be
    contention-free.
    To be used for the first phase of ALTER TABLE, when copying data between
    tables, to allow concurrent SELECTs from the table, but not UPDATEs.
  */
  MDL_SHARED_NO_WRITE,
  /*
    An upgradable shared metadata lock which allows other connections
    to access table metadata, but not data.
    It blocks all attempts to read or update table data, while allowing
    INFORMATION_SCHEMA and SHOW queries.
    A connection holding this kind of lock can read table metadata modify and
    read table data.
    Can be upgraded to X metadata lock.
    To be used for LOCK TABLES WRITE statement.
    Not compatible with any other lock type except S and SH.
  */
  MDL_SHARED_NO_READ_WRITE,
  /*
    An exclusive metadata lock.
    A connection holding this lock can modify both table's metadata and data.
    No other type of metadata lock can be granted while this lock is held.
    To be used for CREATE/DROP/RENAME TABLE statements and for execution of
    certain phases of other DDL statements.
  */
  MDL_EXCLUSIVE,
  /* This should be the last !!! */
  MDL_TYPE_END};


/** Duration of metadata lock. */

enum enum_mdl_duration {
  /**
    Locks with statement duration are automatically released at the end
    of statement or transaction.
  */
  MDL_STATEMENT= 0,
  /**
    Locks with transaction duration are automatically released at the end
    of transaction.
  */
  MDL_TRANSACTION,
  /**
    Locks with explicit duration survive the end of statement and transaction.
    They have to be released explicitly by calling MDL_context::release_lock().
  */
  MDL_EXPLICIT,
  /* This should be the last ! */
  MDL_DURATION_END };


/** Maximal length of key for metadata locking subsystem. */
#define MAX_MDLKEY_LENGTH (1 + NAME_LEN + 1 + NAME_LEN + 1)




/**
  Metadata lock object key.

  A lock is requested or granted based on a fully qualified name and type.
  E.g. They key for a table consists of  <0 (=table)> + <database> + <table name>.
  Elsewhere in the comments this triple will be referred to simply as "key"
  or "name".
*/

struct MDL_key
{
public:
#ifdef HAVE_PSI_INTERFACE
  static void init_psi_keys();
#endif

  /**
    Object namespaces.
    Sic: when adding a new member to this enum make sure to
    update m_namespace_to_wait_state_name array in mdl.cc!

    Different types of objects exist in different namespaces
     - GLOBAL is used for the global read lock.
     - TABLESPACE is for tablespaces.
     - SCHEMA is for schemas (aka databases).
     - TABLE is for tables and views.
     - FUNCTION is for stored functions.
     - PROCEDURE is for stored procedures.
     - TRIGGER is for triggers.
     - EVENT is for event scheduler events.
     - COMMIT is for enabling the global read lock to block commits.
     - USER_LEVEL_LOCK is for user-level locks.
     - LOCKING_SERVICE is for the name plugin RW-lock service
    Note that although there isn't metadata locking on triggers,
    it's necessary to have a separate namespace for them since
    MDL_key is also used outside of the MDL subsystem.
    Also note that requests waiting for user-level locks get special
    treatment - waiting is aborted if connection to client is lost.
  */
  enum enum_mdl_namespace { GLOBAL=0,
                            TABLESPACE,
                            SCHEMA,
                            TABLE,
                            FUNCTION,
                            PROCEDURE,
                            TRIGGER,
                            EVENT,
                            COMMIT,
                            USER_LEVEL_LOCK,
                            LOCKING_SERVICE,
                            /* This should be the last ! */
                            NAMESPACE_END 
};
 。。。

复制代码

 if (!MY_TEST(table_options & TL_OPTION_ALIAS))
  {
    MDL_REQUEST_INIT(& ptr->mdl_request,
                     MDL_key::TABLE, ptr->db, ptr->table_name, mdl_type,
                     MDL_TRANSACTION);
 TABLE_LIST *ptr;

Android进程保活的一般套路

自己曾经也在这个问题上伤过脑经,前几日刚好有一个朋友说在做IM类的项目,问我进程保活如何处理比较恰当,决定去总结一下,网上搜索一下进程常驻的方案好多好多,但是很多的方案都是不靠谱的或者不是*好的,结合很多资料,今天总结一下Android进程保活的一些方案,都附有完整的实现源码,有些可能你已经知道,但是有些你可能是*次听说,(1像素Activity,前台服务,账号同步,Jobscheduler,相互唤醒,系统服务捆绑,如果你都了解了,请忽略)经过多方面的验证,Android系统中在没有白名单的情况下做一个任何情况下都不被杀死的应用是基本不可能的,但是我们可以做到我们的应用基本不被杀死,如果杀死可以马上满血复活,原谅我讲的特别含蓄,毕竟现在的技术防不胜防啊,不死应用还是可能的。

有几个问题需要思考,系统为什么会杀掉进程,杀的为什么是我的进程,这是按照什么标准来选择的,是一次性干掉多个进程,还是一个接着一个杀,保活套路一堆,如何进行进程保活才是比较恰当……如果这些问题你还还存在,或许这篇文章可以解答。

一、进程初步了解
每一个Android应用启动后至少对应一个进程,有的是多个进程,而且主流应用中多个进程的应用比例较大

1、如何查看进程解基本信息
对于任何一个进程,我们都可以通过adb shell ps|grep 的方式来查看它的基本信息

2、进程划分
Android中的进程跟封建社会一样,分了三流九等,Android系统把进程的划为了如下几种(重要性从高到低),网上多位大神都详细总结过(备注:严格来说是划分了6种)。

2.1、前台进程(Foreground process)
场景:
– 某个进程持有一个正在与用户交互的Activity并且该Activity正处于resume的状态。
– 某个进程持有一个Service,并且该Service与用户正在交互的Activity绑定。
– 某个进程持有一个Service,并且该Service调用startForeground()方法使之位于前台运行。
– 某个进程持有一个Service,并且该Service正在执行它的某个生命周期回调方法,比如onCreate()、 onStart()或onDestroy()。
– 某个进程持有一个BroadcastReceiver,并且该BroadcastReceiver正在执行其onReceive()方法。

用户正在使用的程序,一般系统是不会杀死前台进程的,除非用户强制停止应用或者系统内存不足等*端情况会杀死。

2.2、可见进程(Visible process)
场景:
– 拥有不在前台、但仍对用户可见的 Activity(已调用 onPause())。
– 拥有绑定到可见(或前台)Activity 的 Service

用户正在使用,看得到,但是摸不着,没有覆盖到整个屏幕,只有屏幕的一部分可见进程不包含任何前台组件,一般系统也是不会杀死可见进程的,除非要在资源吃紧的情况下,要保持某个或多个前台进程存活

2.3、服务进程(Service process)
场景
– 某个进程中运行着一个Service且该Service是通过startService()启动的,与用户看见的界面没有直接关联。

在内存不足以维持所有前台进程和可见进程同时运行的情况下,服务进程会被杀死

2.4、后台进程(Background process)
场景:
– 在用户按了”back”或者”home”后,程序本身看不到了,但是其实还在运行的程序,比如Activity调用了onPause方法

系统可能随时终止它们,回收内存

2.5、空进程(Empty process)
场景:
– 某个进程不包含任何活跃的组件时该进程就会被置为空进程,完全没用,杀了它只有好处没坏处,*个干它!

3、内存阈值
上面是进程的分类,进程是怎么被杀的呢?系统出于体验和性能上的考虑,app在退到后台时系统并不会真正的kill掉这个进程,而是将其缓存起来。打开的应用越多,后台缓存的进程也越多。在系统内存不足的情况下,系统开始依据自身的一套进程回收机制来判断要kill掉哪些进程,以腾出内存来供给需要的app, 这套杀进程回收内存的机制就叫 Low Memory Killer。那这个不足怎么来规定呢,那就是内存阈值,我们可以使用cat /sys/module/lowmemorykiller/parameters/minfree来查看某个手机的内存阈值。

注意这些数字的单位是page. 1 page = 4 kb.上面的六个数字对应的就是(MB): 72,90,108,126,144,180,这些数字也就是对应的内存阀值,内存阈值在不同的手机上不一样,一旦低于该值,Android便开始按顺序关闭进程. 因此Android开始结束优先级*低的空进程,即当可用内存小于180MB(46080*4/1024)。

读到这里,你或许有一个疑问,假设现在内存不足,空进程都被杀光了,现在要杀后台进程,但是手机中后台进程很多,难道要一次性全部都清理掉?当然不是的,进程是有它的优先级的,这个优先级通过进程的adj值来反映,它是linux内核分配给每个系统进程的一个值,代表进程的优先级,进程回收机制就是根据这个优先级来决定是否进行回收,adj值定义在com.android.server.am.ProcessList类中,这个类路径是${android-sdk-path}\sources\android-23\com\android\server\am\ProcessList.java。oom_adj的值越小,进程的优先级越高,普通进程oom_adj值是大于等于0的,而系统进程oom_adj的值是小于0的,我们可以通过cat /proc/进程id/oom_adj可以看到当前进程的adj值。

看到adj值是0,0就代表这个进程是属于前台进程,我们按下Back键,将应用至于后台,再次查看

adj值变成了8,8代表这个进程是属于不活跃的进程,你可以尝试其他情况下,oom_adj值是多少,但是每个手机的厂商可能不一样,oom_adj值主要有这么几个,可以参考一下。


备注:(上表的数字可能在不同系统会有一定的出入)

根据上面的adj值,其实系统在进程回收跟内存回收类似也是有一套严格的策略,可以自己去了解,大概是这个样子的,oom_adj越大,占用物理内存越多会被*先kill掉,OK,那么现在对于进程如何保活这个问题就转化成,如何降低oom_adj的值,以及如何使得我们应用占的内存*少。

一、进程保活方案
1、开启一个像素的Activity
据说这个是手Q的进程保活方案,基本思想,系统一般是不会杀死前台进程的。所以要使得进程常驻,我们只需要在锁屏的时候在本进程开启一个Activity,为了欺骗用户,让这个Activity的大小是1像素,并且透明无切换动画,在开屏幕的时候,把这个Activity关闭掉,所以这个就需要监听系统锁屏广播,我试过了,的确好使,如下。

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}

如果直接启动一个Activity,当我们按下back键返回桌面的时候,oom_adj的值是8,上面已经提到过,这个进程在资源不够的情况下是容易被回收的。现在造一个一个像素的Activity。

public class LiveActivity extends Activity {

public static final String TAG = LiveActivity.class.getSimpleName();

public static void actionToLiveActivity(Context pContext) {
Intent intent = new Intent(pContext, LiveActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
pContext.startActivity(intent);
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, “onCreate”);
setContentView(R.layout.activity_live);

Window window = getWindow();
//放在左上角
window.setGravity(Gravity.START | Gravity.TOP);
WindowManager.LayoutParams attributes = window.getAttributes();
//宽高设计为1个像素
attributes.width = 1;
attributes.height = 1;
//起始坐标
attributes.x = 0;
attributes.y = 0;
window.setAttributes(attributes);

ScreenManager.getInstance(this).setActivity(this);
}

@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, “onDestroy”);
}
}

为了做的更隐藏,*好设置一下这个Activity的主题,当然也无所谓了

<style name=”LiveStyle”>
<item name=”android:windowIsTranslucent”>true</item>
<item name=”android:windowBackground”>@android:color/transparent</item>
<item name=”android:windowAnimationStyle”>@null</item>
<item name=”android:windowNoTitle”>true</item>
</style>

在屏幕关闭的时候把LiveActivity启动起来,在开屏的时候把LiveActivity 关闭掉,所以要监听系统锁屏广播,以接口的形式通知MainActivity启动或者关闭LiveActivity。

public class ScreenBroadcastListener {

private Context mContext;

private ScreenBroadcastReceiver mScreenReceiver;

private ScreenStateListener mListener;

public ScreenBroadcastListener(Context context) {
mContext = context.getApplicationContext();
mScreenReceiver = new ScreenBroadcastReceiver();
}

interface ScreenStateListener {

void onScreenOn();

void onScreenOff();
}

/**
* screen状态广播接收者
*/
private class ScreenBroadcastReceiver extends BroadcastReceiver {
private String action = null;

@Override
public void onReceive(Context context, Intent intent) {
action = intent.getAction();
if (Intent.ACTION_SCREEN_ON.equals(action)) { // 开屏
mListener.onScreenOn();
} else if (Intent.ACTION_SCREEN_OFF.equals(action)) { // 锁屏
mListener.onScreenOff();
}
}
}

public void registerListener(ScreenStateListener listener) {
mListener = listener;
registerListener();
}

private void registerListener() {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_SCREEN_OFF);
mContext.registerReceiver(mScreenReceiver, filter);
}
}

public class ScreenManager {

private Context mContext;

private WeakReference<Activity> mActivityWref;

public static ScreenManager gDefualt;

public static ScreenManager getInstance(Context pContext) {
if (gDefualt == null) {
gDefualt = new ScreenManager(pContext.getApplicationContext());
}
return gDefualt;
}
private ScreenManager(Context pContext) {
this.mContext = pContext;
}

public void setActivity(Activity pActivity) {
mActivityWref = new WeakReference<Activity>(pActivity);
}

public void startActivity() {
LiveActivity.actionToLiveActivity(mContext);
}

public void finishActivity() {
//结束掉LiveActivity
if (mActivityWref != null) {
Activity activity = mActivityWref.get();
if (activity != null) {
activity.finish();
}
}
}
}

现在MainActivity改成如下

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final ScreenManager screenManager = ScreenManager.getInstance(MainActivity.this);
ScreenBroadcastListener listener = new ScreenBroadcastListener(this);
listener.registerListener(new ScreenBroadcastListener.ScreenStateListener() {
@Override
public void onScreenOn() {
screenManager.finishActivity();
}

@Override
public void onScreenOff() {
screenManager.startActivity();
}
});
}
}

按下back之后,进行锁屏,现在测试一下oom_adj的值

果然将进程的优先级提高了。

但是还有一个问题,内存也是一个考虑的因素,内存越多会被*先kill掉,所以把上面的业务逻辑放到Service中,而Service是在另外一个 进程中,在MainActivity开启这个服务就行了,这样这个进程就更加的轻量,

public class LiveService extends Service {

public static void toLiveService(Context pContext){
Intent intent=new Intent(pContext,LiveService.class);
pContext.startService(intent);
}

@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//屏幕关闭的时候启动一个1像素的Activity,开屏的时候关闭Activity
final ScreenManager screenManager = ScreenManager.getInstance(LiveService.this);
ScreenBroadcastListener listener = new ScreenBroadcastListener(this);
listener.registerListener(new ScreenBroadcastListener.ScreenStateListener() {
@Override
public void onScreenOn() {
screenManager.finishActivity();
}
@Override
public void onScreenOff() {
screenManager.startActivity();
}
});
return START_REDELIVER_INTENT;
}
}

<service android:name=”.LiveService”
android:process=”:live_service”/>

OK,通过上面的操作,我们的应用就始终和前台进程是一样的优先级了,为了省电,系统检测到锁屏事件后一段时间内会杀死后台进程,如果采取这种方案,就可以避免了这个问题。但是还是有被杀掉的可能,所以我们还需要做双进程守护,关于双进程守护,比较适合的就是aidl的那种方式,但是这个不是完全的靠谱,原理是A进程死的时候,B还在活着,B可以将A进程拉起来,反之,B进程死的时候,A还活着,A可以将B拉起来。所以双进程守护的前提是,系统杀进程只能一个个的去杀,如果一次性杀两个,这种方法也是不OK的。

事实上
那么我们先来看看Android5.0以下的源码,ActivityManagerService是如何关闭在应用退出后清理内存的

Process.killProcessQuiet(pid);

应用退出后,ActivityManagerService就把主进程给杀死了,但是,在Android5.0以后,ActivityManagerService却是这样处理的:

Process.killProcessQuiet(app.pid);
Process.killProcessGroup(app.info.uid, app.pid);

在应用退出后,ActivityManagerService不仅把主进程给杀死,另外把主进程所属的进程组一并杀死,这样一来,由于子进程和主进程在同一进程组,子进程在做的事情,也就停止了。所以在Android5.0以后的手机应用在进程被杀死后,要采用其他方案。

2、前台服务
这种大部分人都了解,据说这个微信也用过的进程保活方案,移步微信Android客户端后台保活经验分享,这方案实际利用了Android前台service的漏洞。
原理如下
对于 API level < 18 :调用startForeground(ID, new Notification()),发送空的Notification ,图标则不会显示。
对于 API level >= 18:在需要提优先级的service A启动一个InnerService,两个服务同时startForeground,且绑定同样的 ID。Stop 掉InnerService ,这样通知栏图标即被移除。

public class KeepLiveService extends Service {

public static final int NOTIFICATION_ID=0x11;

public KeepLiveService() {
}

@Override
public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException(“Not yet implemented”);
}

@Override
public void onCreate() {
super.onCreate();
//API 18以下,直接发送Notification并将其置为前台
if (Build.VERSION.SDK_INT <Build.VERSION_CODES.JELLY_BEAN_MR2) {
startForeground(NOTIFICATION_ID, new Notification());
} else {
//API 18以上,发送Notification并将其置为前台后,启动InnerService
Notification.Builder builder = new Notification.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);
startForeground(NOTIFICATION_ID, builder.build());
startService(new Intent(this, InnerService.class));
}
}

public class InnerService extends Service{
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
//发送与KeepLiveService中ID相同的Notification,然后将其取消并取消自己的前台显示
Notification.Builder builder = new Notification.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);
startForeground(NOTIFICATION_ID, builder.build());
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
stopForeground(true);
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.cancel(NOTIFICATION_ID);
stopSelf();
}
},100);

}
}
}

在没有采取前台服务之前,启动应用,oom_adj值是0,按下返回键之后,变成9(不同ROM可能不一样)

在采取前台服务之后,启动应用,oom_adj值是0,按下返回键之后,变成2(不同ROM可能不一样),确实进程的优先级有所提高。

3、相互唤醒
相互唤醒的意思就是,假如你手机里装了支付宝、淘宝、天猫、UC等阿里系的app,那么你打开任意一个阿里系的app后,有可能就顺便把其他阿里系的app给唤醒了。这个完全有可能的。此外,开机,网络切换、拍照、拍视频时候,利用系统产生的广播也能唤醒app,不过Android N已经将这三种广播取消了。

如果应用想保活,要是QQ,微信愿意救你也行,有多少手机上没有QQ,微信呢?或者像友盟,信鸽这种推送SDK,也存在唤醒app的功能。
拉活方法

4、JobSheduler
JobSheduler是作为进程死后复活的一种手段,native进程方式*大缺点是费电, Native 进程费电的原因是感知主进程是否存活有两种实现方式,在 Native 进程中通过死循环或定时器,轮训判断主进程是否存活,当主进程不存活时进行拉活。其次5.0以上系统不支持。 但是JobSheduler可以替代在Android5.0以上native进程方式,这种方式即使用户强制关闭,也能被拉起来,亲测可行。

JobSheduler@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class MyJobService extends JobService {
@Override
public void onCreate() {
super.onCreate();
startJobSheduler();
}

public void startJobSheduler() {
try {
JobInfo.Builder builder = new JobInfo.Builder(1, new ComponentName(getPackageName(), MyJobService.class.getName()));
builder.setPeriodic(5);
builder.setPersisted(true);
JobScheduler jobScheduler = (JobScheduler) this.getSystemService(Context.JOB_SCHEDULER_SERVICE);
jobScheduler.schedule(builder.build());
} catch (Exception ex) {
ex.printStackTrace();
}
}

@Override
public boolean onStartJob(JobParameters jobParameters) {
return false;
}

@Override
public boolean onStopJob(JobParameters jobParameters) {
return false;
}
}

5、粘性服务&与系统服务捆绑
这个是系统自带的,onStartCommand方法必须具有一个整形的返回值,这个整形的返回值用来告诉系统在服务启动完毕后,如果被Kill,系统将如何操作,这种方案虽然可以,但是在某些情况or某些定制ROM上可能失效,我认为可以多做一种保保守方案。

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_REDELIVER_INTENT;
}

START_STICKY
如果系统在onStartCommand返回后被销毁,系统将会重新创建服务并依次调用onCreate和onStartCommand(注意:根据测试Android2.3.3以下版本只会调用onCreate根本不会调用onStartCommand,Android4.0可以办到),这种相当于服务又重新启动恢复到之前的状态了)。

START_NOT_STICKY
如果系统在onStartCommand返回后被销毁,如果返回该值,则在执行完onStartCommand方法后如果Service被杀掉系统将不会重启该服务。

START_REDELIVER_INTENT
START_STICKY的兼容版本,不同的是其不保证服务被杀后一定能重启。

相比与粘性服务与系统服务捆绑更厉害一点,这个来自爱哥的研究,这里说的系统服务很好理解,比如NotificationListenerService,NotificationListenerService就是一个监听通知的服务,只要手机收到了通知,NotificationListenerService都能监听到,即时用户把进程杀死,也能重启,所以说要是把这个服务放到我们的进程之中,那么就可以呵呵了

@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public class LiveService extends NotificationListenerService {

public LiveService() {

}

@Override
public void onNotificationPosted(StatusBarNotification sbn) {
}

@Override
public void onNotificationRemoved(StatusBarNotification sbn) {
}
}

但是这种方式需要权限

<service
android:name=”.LiveService”
android:permission=”android.permission.BIND_NOTIFICATION_LISTENER_SERVICE”>
<intent-filter>
<action android:name=”android.service.notification.NotificationListenerService” />
</intent-filter>
</service>

所以你的应用要是有消息推送的话,那么可以用这种方式去欺骗用户。

结束:
听说账号同步唤醒APP这种机制很不错,用户强制停止都杀不起创建一个账号并设置同步器,创建周期同步,系统会自动调用同步器,这样就能激活我们的APP,局限是国产机会修改*短同步周期(魅蓝NOTE2长达30分钟),并且需要联网才能使用。在国内各大ROM”欣欣向荣”的大背景下,关于进程保活,不加入白名单,我也很想知道有没有一个应用永活的方案,这种方案性能好,不费电,或许做不到,或许有牛人可以,但是,通过上面几种措施,在*大部分的机型下,*大部分用户手机中,我们的进程寿命确实得到了提高。

Android适配不同分辨率手机屏幕的开发

我们经常会遇见这样的场景,做一款简单的App,在自己的手机上设计了layout的xml文件,UI做的还挺好看,挺合适的。但是换了一个手机就会发现变的巨丑,可能只是集中在局部(原来的分辨率低,新的机器分辨率高),或者是手机屏幕放不下了(原来的分辨率高,心的机器分辨率低)。

解决办法就是:

我们可以在res文件路径下新建适配不同分辨率的手机的layout文件,与layout同级

命名为layout-分辨率,请注意大数在前,所以一般就是高x宽,举例layout-2244×1080(HUAWEI P20分辨率)。
在新建的文件下存放的资源和layout下是一致的,不过需要自己做适配,结合不同的显示比例来调整里面的资源文件。手机会根据自己的分辨率来选择不同的layout资源文件,不需要自己在写代码选择,比较智能。

注1:在一个xml资源文件做了修改之后,记得刚更新到所有的layout文件中,否则换个测试机就会出现空指针异常,很无脑的bug,别问我为什么,因为我犯过。

注2:对不不同分辨率的适配,还有其他的解决办法,可能会觉着这样加了很多的文件,会增大安装包,但是就我的测试来看,这样的*美观的,因为我们针对不同的分辨率做了不同的适配,可以做很多微调(只关注常用机型,很冷门的可以放弃,机器找不到合适自己的就会委屈一下去选择相似分辨率的或者是直接用layout,不会报错,只会丑 -_-! )

android 手机屏幕密度等级和屏幕逻辑尺寸

在 android 开发中常常会使用到手机屏幕密度和屏幕逻辑尺寸来进行屏幕适配,这里就列出常见手机的屏幕参数列表:

像素密度等级:是 rom 厂商设定的值,一般是取实际屏幕密度*接近的屏幕密度等级,但是也可以自主设定,目前 android sdk 中支持的等级有 ldpi、mdpi、tvdpi、hdpi、xhdpi、xxhdpi、xxxhdpi

等级像素密度:像素密度等级对应的像素密度

逻辑像素密度:是 rom 厂商设定的值,一般是取实际屏幕密度*接近的屏幕密度等级对应的值,但是也可以自主设定,通过系统 api,getResources().getDisplayMetrics().densityDpi 可以获取到该值

像素:就是屏幕的实际像素单元个数

尺寸:就是屏幕的实际尺寸大小

逻辑尺寸:实际像素数*160/逻辑像素密度,这也是 px 转 dp 的公式

真实像素密度:利用勾股定理算对角线上像素数/对角线尺寸

dp,dp 翻译过来叫做设备无关像素,对于真实像素密度等于像素密度等级上的逻辑值的设备,1dp = 1/160 inch,对于不等于逻辑值的设备,比如上述表格第二个设备,180.27 != 160 这个差值 android 操作系统会进行等比缩放来弥补。比如在这个设备上用的 160dp 到*后真正在屏幕上用多少个像素表示呢,这其实经过了 2 个步骤:
dp 转程序中的 px,按照 dp 转 px 的公式,在该设备上 160dp=160px,160dp 和我们程序里面用 160px 完全等价
程序中的 px 转显示屏上的 px,程序中160px 在该设备显示屏上用的是 180 px。160px -> 180px 这个过程是android操作系统自动完成的,我们不需要关心

Android 万能适配方案和UI屏幕适配 不同分辨率 *全面 *易懂的

屏幕尺寸
屏幕尺寸指屏幕的对角线的长度,单位是英寸,1英寸=2.54厘米(下面有图文介绍)

比如常见的屏幕尺寸有2.4、2.8、3.5、3.7、4.2、5.0、5.5、6.0等

 

屏幕分辨率

屏幕分辨率是指在横纵向上的像素点数,单位是px,1px=1个像素点。一般以纵向像素*横向像素,如1960*1080。

屏幕像素密度

屏幕像素密度是指每英寸上的像素点数,单位是dpi,即“dot per inch”的缩写。屏幕像素密度与屏幕尺寸和屏幕分辨率有关,在单一变化条件下,屏幕尺寸越小、分辨率越高,像素密度越大,反之越小。

当设备的物理尺寸存在差异的时候,dp就显得无能为力了。为4.3寸屏幕准备的UI,运行在5.0寸的屏幕上,很可能在右侧和下侧存在大量的空白。而5.0寸的UI运行到4.3寸的设备上,很可能显示不下。
一句话,总结下,dp能够让同一数值在不同的分辨率展示出大致相同的尺寸大小。但是当设备的尺寸差异较大的时候,就无能为力了。适配的问题还需要我们自己去做,于是我们可能会这么做:

上述代码片段来自网络,也就是说,我们为了优质的用户体验,依然需要去针对不同的dpi设置,编写多套数值文件。

可以看出,dp并没有能解决适配问题。下面看百分比。

 

我们再来看看一些适配的tips

多用match_parent
多用weight
自定义view解决
其实上述3点tip,归根结底还是利用百分比,match_parent相当于100%参考父控件;weight即按比例分配;自定义view无非是因为里面多数尺寸是按照百分比计算的;

 

 

dp:

Density-independent pixel (dp)独立像素密度。标准是160dip.即1dp对应1个pixel,计算公式如:px = dp * (dpi / 160),屏幕密度越大,1dp对应 的像素点越多。
上面的公式中有个dpi,dpi为DPI是Dots Per Inch(每英寸所打印的点数),也就是当设备的dpi为160的时候1px=1dp;

 

 

计算值

ppi的运算方式是:

PPI = √(长度像素数² + 宽度像素数²) / 屏幕对角线英寸数

dp:Density-independent pixels,以160PPI屏幕为标准,则1dp=1px,

dp和px的换算公式 :
dp*ppi/160 = px。比如1dp x 320ppi/160 = 2px。

 

 

 

屏幕尺寸、分辨率、像素密度三者关系(以上三者的关系)

 

像素密度的公式

 

一部手机的分辨率是宽x高,屏幕大小是以寸为单位,那么三者的关系是:

 

假设一部手机的分辨率是1080×1920(px),屏幕大小是5寸,问密度是多少?

像素密度计算公式

屏幕多少英寸指的是对角线的长度。像素密度是指(以1920×1080,5英寸为例),1920和1080的平方和开根号(就是直角三角形斜边长的算法),开出来等于2202.9,除以5英寸就得到ppi441左右
例:”HTC One(32GB/单卡/国际版)
4.7英寸屏幕,分辨率1920×1080 求解像素密度?
解:√(1920^2+1080^2)=2202.9071
2202.9/5=468.7021(ppi)≈469ppi
答:此屏幕像素密度约是469ppi.
屏幕多少英寸指的是对角线的长度。像素密度是指(以1920×1080,5英寸为例),1920和1080的平方和开根号(就是直角三角形斜边长的算法),开出来等于2202.9,除以5英寸就得到ppi441左右
例:”HTC One(32GB/单卡/国际版)
4.7英寸屏幕,分辨率1920×1080 求解像素密度?
解:√(1920^2+1080^2)=2202.9071
2202.9/5=468.7021(ppi)≈469ppi
答:此屏幕像素密度约是469ppi.

<span style=”color:#2f2f2f”>因为ui设计师给你的设计图是以px为单位的,Android开发则是使用dp作为单位的,那么我们需要进行转换:</span>

为了保证用户获得一致的用户体验效果:使得某一元素在Android不同尺寸、不同分辨率的手机上具备相同的显示效果

在进行开发的时候,我们需要把合适大小的图片放在合适的文件夹里面。下面以图标设计为例进行介绍。

 

1).使用自动拉伸位图

 

2).请务必使用 sp 指定文字大小:

 

3).使用布局别名

 

*小宽度限定符仅适用于 Android 3.2 及更高版本。因此,如果我们仍需使用与较低版本兼容的概括尺寸范围(小、正常、大和特大)。例如,如果要将用户界面设计成在手机上显示单面板,但在 7 英寸平板电脑、电视和其他较大的设备上显示多面板,那么我们就需要提供以下文件:

res/layout/main.xml: 单面板布局

res/layout-large: 多面板布局

res/layout-sw600dp: 多面板布局

后两个文件是相同的,因为其中一个用于和 Android 3.2 设备匹配,而另一个则是为使用较低版本 Android 的平板电脑和电视准备的。

要避免平板电脑和电视的文件出现重复(以及由此带来的维护问题),您可以使用别名文件。例如,您可以定义以下布局:

res/layout/main.xml,单面板布局

res/layout/main_twopanes.xml,双面板布局

然后添加这两个文件:

 

res/values-large/layout.xml:

res/values-sw600dp/layout.xml:

后两个文件的内容相同,但它们并未实际定义布局。它们只是将 main 设置成了 main_twopanes 的别名。由于这些文件包含 large 和 sw600dp 选择器,因此无论 Android 版本如何,

系统都会将这些文件应用到平板电脑和电视上(版本低于 3.2 的平板电脑和电视会匹配 large,版本高于 3.2 的平板电脑和电视则会匹配 sw600dp)。

 

图片的适配方案:

 

什么叫*适合的图片?比如我的手机屏幕密度是xxhdpi,那么drawable-xxhdpi文件夹下的图片就是*适合的图片。

 

缩放原理:

 

例如,一个启动图标的尺寸为48×48 dp,这表示在 MDPI 的屏幕上其实际尺寸应为 48×48 px,在 HDPI 的屏幕上其实际大小是 MDPI 的 1.5 倍 (72×72 px),

在 XDPI 的屏幕上其实际大小是 MDPI 的 2 倍 (96×96 px),依此类推。

虽然 Android 也支持低像素密度 (LDPI) 的屏幕,但无需为此费神,系统会自动将 HDPI 尺寸的图标缩小到 1/2 进行匹配。

放大系数的关系:和屏幕像素密度有关

mdpi密度的*高dpi值是160,而xxhdpi密度的*高dpi值是480,因此是一个3倍的关系。放大系数和文件夹的密度有关系!

 

xxxhdpi密度的*高dpi值是640,480是它的0.75倍

 

正常Xhdpi的图片,如果放在了xxhdpi里面,图片会缩小,放在hdpi的里面图片会放大!

 

 

一张原图片被缩小了之后显示其实并没有什么副作用,但是一张原图片被放大了之后显示就意味着要占用更多的内存了。

但是一张原图片被放大了之后显示就意味着要占用更多的内存了。因为图片被放大了,像素点也就变多了,而每个像素点都是要占用内存的。

 

我们仍然可以通过例子来直观地体会一下,首先将android_logo.png图片移动到drawable-xxhdpi目录下,运行程序后我们通过Android Monitor来观察程序内存使用情况:

 

 

 

可以看到,程序所占用的内存大概稳定在19.45M左右。然后将android_logo.png图片移动到drawable-mdpi目录下,重新运行程序,结果如下图所示:

 

 

 

现在涨到23.40M了,占用内存明显增加了。如果你将图片移动到drawable-ldpi目录下,你会发现占用内存会更高。

通过这个例子同时也验证了一个问题,我相信有不少比较有经验的Android程序员可能都遇到过这个情况,就是当你的项目变得越来越大,

有的时候加载一张drawable-hdpi下的图片,程序就直接OOM崩掉了,但如果将这张图放到drawable-xhdpi或drawable-xxhdpi下就不会崩掉,其实就是这个道理。

那么经过上面一系列的分析,答案自然也就出来了,图片资源应该尽量放在高密度文件夹下,这样可以节省图片的内存开支,而UI在设计图片的时候也应该尽量面向高密度屏幕的设备来进行设计。

就目前来讲,*佳放置图片资源的文件夹就是drawable-xxhdpi。那么有的朋友可能会问了,不是还有更高密度的drawable-xxxhdpi吗?干吗不放在这里?

这是因为,市面上480dpi到640dpi的设备实在是太少了,如果针对这种级别的屏幕密度来设计图片,图片在不缩放的情况下本身就已经很大了,

 

 

因为分辨率不一样,所以不能用px;因为屏幕宽度不一样,所以要小心的用dp,那么我们可不可以用另外一种方法来统一单位,不管分辨率是多大,屏幕宽度用一个固定的值的单位来统计呢?

 

百分比适配方法,对于控件的宽和高!

 

<TextView
android:layout_width=”@dimen/x360″
android:layout_height=”@dimen/x360″
android:background=”@color/colorAccent”
android:gravity=”center”
android:text=”360×360″/>

<TextView
android:layout_width=”@dimen/x180″
android:layout_height=”@dimen/x180″
android:background=”@color/colorPrimaryDark”
android:gravity=”center”
android:text=”180×180″/>
<dimen name=”x356″>356px</dimen>
<dimen name=”x357″>357px</dimen>
<dimen name=”x358″>358px</dimen>
<dimen name=”x359″>359px</dimen>
<dimen name=”x360″>360px</dimen>
<dimen name=”x358″>537px</dimen>
<dimen name=”x359″>538.5px</dimen>
<dimen name=”x360″>540px</dimen>
<dimen name=”x361″>541.5px</dimen>
<dimen name=”x362″>543px</dimen>
<dimen name=”x363″>544.5px</dimen>
 

总结: 不同分辨率的文件夹有不同x360的px值,是通过程序计算自动生成的!

 

适配缺点:

所以说,这个方案虽然是一劳永逸,但是由于实际上还是使用的px作为长度的度量单位,所以多少和google的要求有所背离,不好说以后会不会出现什么不可预测的问题。

其次,如果要使用这个方案,你必须尽可能多的包含所有的分辨率,因为这个是使用这个方案的基础,如果有分辨率缺少,会造成显示效果很差,甚至出错的风险,而这又势必会增加软件包的大小和维护的难度,

所以大家自己斟酌,择优使用。

 

对于没有考虑到屏幕尺寸,可能会出现意外的情况;
apk的大小会增加;
注意的问题:

要有一个通用的布局,不然有些手机会报错!

 

 

 

虚拟按键的问题

 

设计高度 = 屏幕高度 – 状态栏高度
布局高度 = 屏幕高度 – 状态栏高度 – 虚拟按键高度

要解决这个问题,其实很简单,我说过,这不是一个技术问题,因此不必使用 fitsSystemWindows 属性,也避免了副作用。

只需要在布局时,正确理解设计师的意图,比如,如果一个按钮在*底部,你应该用 layout_gravity=”bottom” 而不是用 marginTop 或者其他方式来把它撑到底部。

 

 

我开始以为他们是导航栏上的虚拟按键,因为确实我将按键隐藏后可以成功适配,然后我就在网上通过如下代码来获取他们的真实分辨率

 

/**
* @param context
* @return 获取屏幕原始尺寸高度,包括虚拟功能键高度
*/
public static int getTotalHeight(Context context) {
int dpi = 0;
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = windowManager.getDefaultDisplay();
DisplayMetrics displayMetrics = new DisplayMetrics();
@SuppressWarnings(“rawtypes”)
Class c;
try {
c = Class.forName(“android.view.Display”);
@SuppressWarnings(“unchecked”)
Method method = c.getMethod(“getRealMetrics”, DisplayMetrics.class);
method.invoke(display, displayMetrics);
dpi = displayMetrics.heightPixels;
} catch (Exception e) {
e.printStackTrace();
}
return dpi;
}

/**
* @param context
* @return 获取屏幕内容高度不包括虚拟按键
*/
public static int getScreenHeight(Context context) {
WindowManager wm = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
return outMetrics.heightPixels;
}
 

这里我用了三部华为手机通过上面的方法获取分辨率
华为荣耀8 1080 1920 得到1080 1812
华为畅享5 720 1280 得到720 1184
华为畅玩5x 108 1920 得到 1080 1776

通过这两个方法我们可以得到手机的分辨率高度和手机去除虚拟按键的高度,两者相减就是手机的虚拟按键的高度
调用后得到的结果是 总高度 : 2560 内容高度 : 2408 虚拟按键 : 152
如果想要适配该机型,其实也很简单,只需要把原来的values-2560×1440文件夹复制一份重新名为values-2408×1440即可,

http://blog.csdn.net/c15522627353/article/details/52452490

 

Google的 百分比布局库(percent-support-lib)

 

PercentRelativeLayout、PercentFrameLayout,不过貌似没有LinearLayout,有人会说LinearLayout有weight属性呀。但是,weight属性只能支持一个方向呀~~哈,没事,刚好给我们一个机会去自定义一个PercentLinearLayout。

 

<android.support.percent.PercentRelativeLayout
xmlns:android=”http://schemas.android.com/apk/res/android”xmlns:app=”http://schemas.android.com/apk/res-auto”android:layout_width=”match_parent”android:layout_height=”match_parent”>

<Viewandroid:id=”@+id/top_left”android:layout_width=”0dp”android:layout_height=”0dp”android:layout_alignParentTop=”true”android:background=”#ff44aacc”app:layout_heightPercent=”20%”app:layout_widthPercent=”70%” />

<Viewandroid:id=”@+id/top_right”android:layout_width=”0dp”android:layout_height=”0dp”android:layout_alignParentTop=”true”android:layout_toRightOf=”@+id/top_left”android:background=”#ffe40000″app:layout_heightPercent=”20%”app:layout_widthPercent=”30%” />

<Viewandroid:id=”@+id/bottom”android:layout_width=”match_parent”android:layout_height=”0dp”android:layout_below=”@+id/top_left”android:background=”#ff00ff22″app:layout_heightPercent=”80%” />
</android.support.percent.PercentRelativeLayout>
 

 

Android UI适配小结:

 

1.一般情况下,输出几套,各为多少分辨率不同的质量的切图?
2.一般情况下,输出几套分辨率不同的标注图(如常说的主流四套分辨率,1080*1920,720*1280,480*800.240*320), 标注单位为px还是自己计算的dp(这个单位您一般是自己计算还是交给程序,如果自己计算的话比较好的方法),创建画布时*版从多少分辨率的画布开始设计。
3.是否考虑带有虚拟按键的适配不同方法?
4.遇到与主流分辨率相近但长宽比不同的分辨率时的适配方式。
5.字号的适配方式,字号单位。
6.与程序适配沟通时需要注意的要点。
7.其它我未提及或者没考虑到的但您觉得非常重要的问题。

 

 

1.一般只输出主流尺寸的切图,如480*800 1280*720
2.开发写标准尺寸是按照320P来的,得按照你自己的设计稿来标注,不过得算好比例:比如你出了480*800的设计稿,你所有的标注的所有尺寸都得除以1.5;要是出了720*1280的设计稿,你所有的标注的所有尺寸都得除以2,以此类推
3.无需考虑,系统自适应
4.只要宽度分辨率一样,适配一样
5.字号跟第2点一样道理
6.程序员对UI实现都不严谨(因为他们觉得UI不重要),要时时刻刻盯着

 

主流APP:

对于平板来说:都是要单独一套App的

 

而分辨率可以以1280*720或者是1960*1080作为主要分辨率进行设计。

 

1280:720    的屏占比     16:9

1960*1280的屏占比      49:32

 

效果图一套(720p),切图也是 720p (有时候要求1080p,看公司要求高不高)。

 

UI切图都是以1280*720 开发写标准尺寸是按照320P来的

假若还是不适配的话也可以根据不适配的机型来设定特别的值。

 

为什么Web页面设计人员从来没有说过,尼玛适配好麻烦?

其实就是一个原因,网页提供了百分比计算大小。

 

自定义控件以PX为单位计算的,那么怎么适配呢!
接下来会讲

Android bind其他或第三方APK Service方法

有时候我们会使用其他模块Service接口,这里介绍一个Bind其他APK service的方法。

假设A要bind B的Service,这里B我们假设是“com.android.music.MediaPlaybackService”.

1.检查B的Service是否允许其他模块引用

–打开B的Manifest,只有带有android:exported=”true”属性才可以:

<service android:name=”com.android.music.MediaPlaybackService”
android:exported=”true” />

======================================

2.将B的aidl复制一份到A中

–B/com/android/music/IMediaPlaybackService.aidl 复制到A/com/android/music/IMediaPlaybackService.aidl

–即B的aidl文件放到A后,需要保持和B中一样目录结构和内容。

–如果用android studio编译,注意检查是否build.gradle是否配置了对应的aidl文件夹(如果没有添加上),形如:

sourceSets {
main {
manifest.srcFile ‘src/main/AndroidManifest.xml’
java.srcDirs = [‘src/main/java’]
resources.srcDirs = [‘src/main/res’]
res.srcDirs = [‘src/main/res’]
assets.srcDirs = [‘src/main/assets’]
aidl.srcDirs = [‘src/main/java’]
}

======================================

3.A中实现Bind

–定义一个接口变量mMusicService

IMediaPlaybackService mMusicService = null;

–定义一个ServiceConnection变量osc

private ServiceConnection osc = new ServiceConnection() {
public void onServiceConnected(ComponentName classname, IBinder obj) {
mMusicService = IMediaPlaybackService.Stub.asInterface(obj);
Log.e(TAG, “MusicServiceg Connect!!”);
}
public void onServiceDisconnected(ComponentName classname) {
Log.e(TAG, “MusicServiceg DisConnect!!”);
mMusicService = null;
}
};

–通过intent bind B的Service

Intent playBackIntent = new Intent(Intent.ACTION_MAIN);
ComponentName componentName = new ComponentName(
“com.android.music”,
“com.android.music.MediaPlaybackService”);
playBackIntent.setComponent(componentName);
Log.e(TAG, “MusicService start bind!!”);
mContext.bindService(playBackIntent, osc, 0);

======================================

 

至此已经可以bind到MediaPlaybackService了,可以正常调用B的接口

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速