• mysql的并发处理机制_下篇


        MySQL的并发处理机制,有MVCC及锁机制来处理,上篇简要说明了 MVCC及隔离级别:mysql的并发处理机制_上篇 ,这篇来说说mysql下的锁。

        温馨提示:下文有几个表格长度较长,右下角的博文导航目录会挡道,浏览时,可以点击 导航目录的左下角按钮收缩目录:
     


     
        如果转载,请注明博文来源: www.cnblogs.com/xinysu/   ,版权归 博客园 苏家小萝卜 所有。望各位支持!
     


     1 Innodb的锁

        在innodb中,有4种类型的锁:IX、X、IS及S锁,其说明如下:
     
    类型 说明 场景
    S 共享锁 针对于RS隔离级别的查询或者添加Lock in share mode的SELECT查询而产生的锁
    X 排它锁 针对于update、delete、insert操作而产生的锁
    IS 意向共享锁 表级别的锁,在添加S锁之前对表格添加IS锁
    IX 意向排他锁 表级别的锁,在添加X锁之前对表格添加IX锁

    1.1 锁定兼容情况

      四个锁之间的兼容性,需要分成两种情况来讨论,锁粒度小于表级别的锁的兼容情况,表级的锁兼容情况。
    1. 锁粒度小于表级别的锁的兼容情况
      • 对于这两行锁的兼容说明如下:
        • 假设有一行数据,添加了行锁S锁,那么这个行数据,可以提供给其他事务进行S锁的申请和添加,但是不支持其他事务对这一行进行X锁的申请和添加。比如,事务A,对 pk100 这一行进行了 查询操作并添加了S锁,那么其他事务仍然可以对这一行数据进行查询,但是不能对这行数据进行 UPDATE 跟 DELETE 操作,会处于锁等待情况,直到该事务A结束并释放S锁;
        • 假设有一行数据,添加了X锁,那么这个行数据,不允许其他事务对这一行数据进行加锁。比如,事务A,对pk100这一行进行了UPDATE操作,那么其他事务在事务A没有结束之前,都无法对这一行数据申请 S锁。
    2. 表级的锁兼容情况
      • 对于表级别的锁兼容性如下:
        • 当一个表格持有S表锁时,不需要其他事务对该表格申请X锁跟IX锁,但是允许申请S跟IS锁。比如,事务A对表格tba全表读,加了S表锁,期间支持其他事务对tba全表读(申请S表锁成功)、支持其他事务对tba行数据查询(申请IS表锁成功),但是不支持对表格全表的修改操作(申请X表锁等待)跟不支持对表格行数据修改操作(申请IX表锁等待);
        • 当一个表格持有X表锁时,持有锁期间,不支持其他所有锁的申请;
        • 当一个表格持有IS表锁时,允许申请 S表锁、IS表锁、IX表锁,但是不支持X表锁申请。
          • 比如,事务A对表格tba 查询了 id = 10(id为主键)这一行数据,这个时候,表格tba持有IS表锁,id = 10 这一行持有 S 行锁,期间,支持其他事务对 tba 全表查询(申请表锁S成功)或者 基于索引查找(申请表锁IS成功)
          • 如果需要对行 id = 20 进行数据修改,则会先申请 tba 的表锁 IX(申请成功),然后再申请id=20行锁X (申请成功);如果需要对 id = 10 这一行数据进行修改,则会申请 tba的表锁 IX(表锁申请成功),然后申请 id = 10 的行锁X(申请堵塞,因为 id = 10 正持有S锁);
          • 如果需要对表格进行全表修改,需要申请表锁(X锁),这个时候,IS锁的优势来了,当查看表格是否有其他事务在访问操作时,一看表锁IS就知道有其他事务对表格内部某些数据持有S锁,并且还没有释放,那么这个时候,申请X锁就会处于等待状态,而不需要一行一行去查询每一行数据有没有被其他事务持有锁,可以大规模的减少查询 锁申请情况;
        • 当一个表格持有IX表锁时,支持申请IS、IX表锁,但是不兼容S、X表锁。
          • 比如,事务A对表格 tba 中 id=10 (id为主键)进行进行 数据修改,这个时候,会对表格 tba 先申请一个 IX 表锁(申请成功),然后申请 id =10 的 X 行锁,申请成功,则 事务A 持有 IX 表锁、id=10的X 行锁,此时事务B 查询 id=20的行,申请表锁 IS 成功,申请 id=20的 S 行锁成功;事务C 修改 id=30的行数据,申请表锁 IX 成功,申请 id=30的行锁 X成功;但是,事务D中,对整个表格发起update或者全表SELECT操作,需要申请 X表锁或者S表锁,正常情况下,应该要对表格的每一行数据进行查看,确保每一行数据的行锁情况,但是因为有了意向锁,事务D一看到 tba 持有 了IX锁,则明白,tba 中某些行持有X锁,则会不兼容其他事务对tba 表锁S ,表锁X的申请。
      • 为什么要引入意向表锁?
        • 在没有意向锁的时候,如果事务T 需要给表格 A 添加 一个S 表锁,那么就意味这这个表格内部的每一行数据,都不能有X锁,才能够申请 S 表锁成功,如果表格数据很多,一行行查找非常浪费加锁时间,这个时候,就出现了表格意向锁,当表格内部某些行发生 UPATE DELETE INSERT操作,则会对表格 加上 一个意向 IX 表锁,这样 事务T在申请 表格A的 S 表锁时,只需要检查 表格 A 是否有 IX表锁,如果有,则意味内部有 部分行数据持有X锁,则直接进入等待情况,如果表格没有 IX表锁,则直接申请S表锁成功,这是一个多么节约加锁时间的操作!

    1.2 锁的级别

    • Table Lock
      • 表锁,如果没有where条件、无可用索引或者获取的行记录过多,则会使用 table full scan,添加表锁
    • Record Lock
      • 记录锁,如果执行计划使用了索引,则会根据索引的查找情况添加行锁
    • Gap Lock
      • 在RR、RS隔离级别,发生在索引值之间,在连续的两个索引值之间添加锁,加锁后,这两个索引值之间,无法插入新的索引值,不包含行记录
    • Next-Key Lock
      • Record Lock 跟Gap Lock的组合,合体成为Next-KEY Lock
     
          表锁、行锁都相对好理解,这里尝试简单说明下 GAP LOCK。
          假设当前隔离级别为RR,表格 tbgap( id int auto_increment primary key not null , name varchar(50) , sort int , key ix_sort (sort)) engine=innodb; 
          表格数据如下:
          
          在索引ix_sort上,一共有7个间隙,分别为(-∞,(1,6)),((1,6),(2,5)),((2,5),(3,2)),((3,2),(5,4)),((5,4),(6,1)),((6,1),(7,3)),((7,3),+∞),而根据实际的隔离级别及锁申请情况,加在这些间隙上的锁,则成为 GAP LOCK 。   

    1.3 锁与隔离级别(不考虑 lock in shar mode跟for update )

    • RU,读未提交记录,不加锁读,正常写锁;
    • RC,快照读,无锁;当前读,加 Record Lock
    • RR,快照读,无锁;当前读,对读取到的记录加 Record Lock,同时为了确保where条件范围内的数据无变化,会增加Next key lock
    • RS,读写均为当前读,不支持快照读。包括select 在内,对读取到的记录加 Record Lock,同时为了确保where条件范围内的数据无变化,会增加Next key lock。

    2 锁的申请与释放过程

          看SQL语句的锁情况,需要结合隔离级别、执行计划、表结构等,同一个SQL,不同的隔离级别、表结构、执行计划,其锁情况不一定是一样的!
          本次模拟这3个表格,age列分别:无索引、有一般索引、有唯一索引。表结构结束及数据如下:
     
    CREATE TABLE tb_no_index ( id int primary key not null auto_increment, age int not null, name varchar(100) );
    CREATE TABLE tb_index ( id int primary key not null auto_increment, age int not null, name varchar(100) KEY ix_age(age) );
    CREATE TABLE tb_unique_index ( id int primary key not null auto_increment, age int not null,name varchar(100) UNIQUE KEY ix_age(age) );
     
    INSERT INTO tb_no_index(age) values(2),(9),(21),(4),(7),(25);
    INSERT INTO tb_index(age) values(2),(9),(21),(4),(7),(25);
    INSERT INTO tb_unique_index(age) values(2),(9),(21),(4),(7),(25);
      
    每个表格IX_age的索引行数就据如下图展示:
    age
    2
    4
    7
    9
    21
    25
    id
    1
    4
    5
    2
    3
    6
     
    每个表格主键上面的行数就据如下图展示:
    id
    1
    2
    3
    4
    5
    6
    age
    2
    9
    21
    4
    7
    25
    name
    null
    null
    null
    null
    null
    null
     

    2.1 Read Uncommitted

         所有事务隔离级别设置: set session transaction isolation level read Uncommitted ;
         RU是读未提交,不添加 LOCK IN SHARE MODE 跟 FOR UPDATE 的 SELECT 语句,均为读未提交,不加锁,存在脏读、不可重复读及幻读。
         所有UPDATE、DELETE、INSERT获取当前读记录,加锁。
     表格     SQL
    select * from tbname
    where age/id ...
    update tbname set name=...
    where id = 4
    update tbname set name=...
    where age = 21
    update tbname set name=...
    where age between 5 and 15
    tb_no_index
    读不加锁,读未提交数据
    可能有脏读、不可重复读及幻读
    当前读,根据主键修改数据
    tbname 加意向表锁 IX
    id=4 加 行锁 X
     
     
    表格的age列无索引,所以update过程中,全表加X锁
    支持semi-constent-read,如果有其他update语句修改其他行不堵塞,但是不支持 select ... for update
    同左
    tb_index
    表格的age列有索引,update过程中
    tb_index 加 表格意向锁 IX
    age索引上面,age=21 行添加行锁 X
    再在主键上,给id=3 这一行数据,添加行数 X
     
     
    表格的age列有索引,update过程涉及age=7,9 两行数据
    tb_index 加表格意向锁 IX
    age索引上面,age=7,age=9 行添加行锁 X
    再在主键上,给id=2,id=5 这一行数据,添加行数 X
     
    tb_unique_index
    同上
    同上
          

    2.2 Read Committed

    所有事务隔离级别设置: set session transaction isolation level read committed ;
         
         RC是读已提交,不添加 LOCK IN SHARE MODE 跟 FOR UPDATE 的 SELECT 语句,均为 快照读,不加锁,同个事务内读取同一个版本的数据,可能非最新数据,但是不存在脏读、不可重复读及幻读情况。
         所有UPDATE、DELETE、INSERT获取当前读记录,加锁。
     
         下表中,黄绿色 字体 是RC与RU隔离级别不同的地方,仔细阅读分析结果可以知道,在 RU 跟 RC 间,最大的区别在于 SELECT 的查询模式,RU 为 读未提交,而 RC 为快照读。UPATE/DELETE/INSERT的加锁模式类同。
      表格
                 SQL
    select * from tbname
    where age/id ...
    update tbname set name=...
    where id = 4
    update tbname set name=...
    where age = 21
    update tbname set name=...
    where age between 5 and 15
    tb_no_index
    快照读,不加锁
    读取的数据不一定是最新版本,但是事务内的所有查询读取数据都是同一版本的行数据,不存在脏读、不可重复读及幻读的情况
    当前读,根据主键修改数据
    tbname 加意向表锁 IX
    id=4 加 行锁 X
    表格的age列无索引,所以update过程中,全表加X锁
    支持semi-constent-read,如果有其他update语句修改其他行不堵塞,但是不支持 select ... for update
    同左
    tb_index
    表格的age列有索引,update过程中
    tb_index 加 表格意向锁 IX
    age索引上面,age=21 行添加行锁 X
    再在主键上,给id=3 这一行数据,添加行数 X
     
    表格的age列有索引,update过程涉及age=7,9 两行数据
    tb_index 加表格意向锁 IX
    age索引上面,age=7,age=9 行添加行锁 X
    再在主键上,给id=2,id=5 这一行数据,添加行数 X
     
     
    tb_unique_index
    同上
    同上
     
     

    2.3 Read Repeatable

    所有事务隔离级别设置: set session transaction isolation level repeatable read ;
     
          RR隔离级别中,SELECT操作支持快照读,所有的UPDATE/DELETE/INSERT加锁,锁类型会新增一个GAP LOCK。
       表格     SQL
    select * from tbname
    where age/id ...
    update tbname set name=...
    where id = 4
    update tbname set name=...
    where age = 21
    update tbname set name=...
    where age between 5 and 15
    tb_no_index
    快照读,不加锁
    读取的数据不一定是最新版本,但是事务内的所有查询读取数据都是同一版本的行数据,不存在脏读、不可重复读及幻读的情况
    当前读,根据主键修改数据
    tbname 加意向表锁 IX
    id=4 加 行锁 X
    表格的age列无索引,所以update过程中
    全表加X锁,期间全表堵塞UPDATEDELETEINSERT
    同左
    tb_index
    表格的age列有索引,update过程中
    tb_index 加 表格意向锁 IX
    age索引上面,age=21 行添加行锁 X
    再在主键上,给id=3 这一行数据,添加行数 X
     
    你以为结束了!并没有,这里有趣了!
    还会添加两个gap lock ((9,2) ,(21,3)),((21,3), (21,25))
     
    这里我们单独拎出小表格来分析。
    表格的age列有索引,update过程涉及age=7,9 两行数据
    tb_index 加表格意向锁 IX
    age索引上面,age=7,age=9 行添加行锁 X
    再在主键上,给id=2,id=5 这一行数据,添加行数 X
    同时会在索引 age的值上添加 3个 gap lock,分别为 
    ((4,4),(7,5))、((7,5),(9,2))、((9,2),(21,3))
     
    tb_unique_index
    以为跟上面的加锁范围一样,no no no
    唯一索引列上 每一个age都是唯一的,也就是age=21只有一个,不会再INSERT一个新的 age =21进来,故在这里不需要加gap lock,加锁情况如下:
    tb_index 加 表格意向锁 IX
    age=21 行添加行锁 X 
    age索引上面,age=21 行添加行锁 X
    再在主键上,给id=3 这一行数据,添加行数 X
    表格的age列有索引,update过程涉及age=7,9 两行数据
    tb_index 加表格意向锁 IX
    age索引上面,age=7,age=9 行添加行锁 X
    再在主键上,给id=2,id=5 这一行数据,添加行数 X
    同时会在索引 age的值上添加 3个 gap lock,分别为 
    ((4,4),(7,5))、((7,5),(9,2))、((9,2),(21,3))
    但是,范围查询添加到gap lock在其他情况下跟非唯一索引会有一些差别,可以看下表的例子。
     
    这里做亮点补充说明:
     

    2.3.1 RR下的非唯一索引加锁情况

          update tbname set name=...  where age = 21
           
          还记得上篇文章说过 RR隔离级别可以防止 幻读吗?因为在RR隔离级别中,加多了next-key = record lock + gap lock,gap lock是加在索引值之间的锁。也就是 当修改 age=21 的行数据时,除了 在 age=21 这一行添加 X record lock , 还在 ((9,2) ,(21,3)),((21,3), (21,25))这两个age值得范围内添加 gap lock。加锁的情况是:tb_index添加 IX意向锁,age索引上添加age=21的 x record lock,再在主键上的行记录 id=5 添加 X record lock,同时在 age 值上添加两个 gap lock,分别为((9,2) ,(21,3)),((21,3), (21,25))。
         注意这里有个误区,很多小伙伴会认为,那么这么加gap锁,则意味着,当update age=21这一列时, 9<age<25 ,这个范围内,是不允许进行 UPATE/DELETE/INSERT的。这种推测实际上是不完整的,因为它没考虑到跟主键!!!
     
         注意,每次写gap lock的时候,都是有加上主键值的。比如这里,当更新 age=21这列时,加了 ((9,2) ,(21,3)),((21,3), (21,25)) 这两个范围的 GAP LOCK,那么在当前update age=21的事务还没有结束的情况下,假设有两条修改SQL的语句:
    update tbname set age=9 where id = 1;
    update tbname set age=9 where age = 4;
     
        这两条SQL,是能够正常执行,还是堵塞呢?
        innodb中,索引按照二叉树排列顺序,而这两条SQL修改后在IX_AGE上的索引值分别为:(9,1)、(9,4),可以发现(9,1)在键值(9,2)的左边,不在GAP LOCK的范围内,所以,可以正常执行;而(9,4)在键值的右边,刚好在GAP LOCK的范围内,会被堵塞!总结:第一条UPDATE SQL,正常秩序;第二条UPDATE SQL会被堵塞。
     
        所以,考虑GAP LOCK的时候,一定要注意结合整个索引键值来分析,而索引键值=索引值+主键。

    2.3.2 RR下的唯一索引加锁情况

          update tbname set name=...  where age between .. and ... 
     
          因为唯一索引上面的索引键值都是唯一的,故不会出现重复值的插入的情况,下表罗列了同样的 范围查询修改语句,在唯一索引及非唯一索引上加 GAP_LOCK的情况。
          表格数据如下:
     
          
     
          加GAP_LOCK的情况如下(注意注意,方便查看,省略了主键值,实际上是需要添加上主键键值的):      
     
    update tbname set name=...
    where age between 1 and 7
    update tbname set name=...
    where age between 2 and 7
    update tbname set name=...
    where age between 5 and 10
    update tbname set name=...
    where age between 15 and 50
    tb_index
    (-∞,2),(2,4),(4,7),(7,9)
    (-∞,2),(2,4),(4,7),(7,9)
    (4,7),(7,9),(9,21)
    (9,21),(21,25),(25,+∞)
    tb_unique_index
    (-∞,2),(2,4),(4,7)
    (2,4),(4,7)
    (4,7),(7,9),(9,21)
    (9,21),(21,25),(25,+∞)
     

    2.4 Read Serializable

    所有事务隔离级别设置: set session transaction isolation level Serializable   ;
       表格     SQL
    select * from tbname
    where age/id ...
    update tbname set name=...
    where id = 4
    update tbname set name=...
    where age = 21
    update tbname set name=...
    where age between 5 and 15
    tb_no_index
    不支持快照读,所有SELECT都是当前读,所有SELECT操作都需要加S锁,除主键定值查找唯一索引定值查找外,其他基于索引或者主键的范围查找都会添加 S GAP LOCK。并发度是四个隔离级别中性能最差的。
    当前读,根据主键修改数据
    tbname 加意向表锁 IX
    id=4 加 行锁 X
    表格的age列无索引,所以update过程中
    全表加X锁,期间全表堵塞UPDATEDELETEINSERT
    同左
    tb_index
    表格的age列有索引,update过程中
    tb_index 加 表格意向锁 IX
    age索引上面,age=21 行添加行锁 X
    再在主键上,给id=3 这一行数据,添加行数 X
    在age索引上 添加两个gap lock ((9,2) ,(21,3)),((21,3), (21,25))
    表格的age列有索引,update过程涉及age=7,9 两行数据
    tb_index 加表格意向锁 IX
    age索引上面,age=7,age=9 行添加行锁 X
    再在主键上,给id=2,id=5 这一行数据,添加行数 X
    同时会在索引 age的值上添加 3个 gap lock,分别为
    ((4,4),(7,5))、((7,5),(9,2))、((9,2),(21,3))
    tb_unique_index
    唯一索引列上 每一个age都是唯一的,也就是age=21只有一个,不会再INSERT一个新的 age =21进来,故在这里不需要加gap lock,加锁情况如下:
    tb_index 加 表格意向锁 IX
    age=21 行添加行锁 X 
    表格的age列有索引,update过程涉及age=7,9 两行数据
    tb_index 加表格意向锁 IX
    age索引上面,age=7,age=9 行添加行锁 X
    再在主键上,给id=2,id=5 这一行数据,添加行数 X
    同时会在索引 age的值上添加 3个 gap lock,分别为 
    ((4,4),(7,5))、((7,5),(9,2))、((9,2),(21,3))
    但是,范围查询添加到gap lock在其他情况下跟非唯一索引会有一些差别,可以看下表的例子。
     
     
        这里详细的来分析下 RS 隔离级别下的SELECT操作加的锁:
         
    表格     SQL
    select * from tbname
    where id=5
    select * from tbname
    where id betwee 5 and 15
    select * from tbname
    where age=21
    select * from tbname
    where age betwee 5 and 9
    tb_no_index
    主键定值查找
    表格tbname 添加 IS 意向锁
    id=5 添加 S锁
    主键范围查找
    表格tbname 添加 IS 意向锁
    id=5,id=6 两行数据 添加 S锁
    同时添加2个 S GAP LOCK ,分别为 ((5,7),(6,25))跟((6,25),+∞)
    全表查找
    表格 tbname 添加 IS 意向锁
    由于全表查找,整个表格 再次添加 S 表锁
    全表查找
    表格 tbname 添加 IS 意向锁
    由于全表查找,整个表格 再次添加S 表锁
    tb_index
    ix_age索引查找
    表格tbname 添加 IS 意向锁
    索引上 age = 21 添加 S 行锁
    主键上 id=3 添加 S 行锁
    同时添加 2个 S GAP LOCK ,分别为 ((9,2) ,(21,3)),((21,3), (21,25))
    ix_age索引查找
    表格tbname 添加 IS 意向锁
    age索引上面,age=7,age=9 行添加行锁 S
    再在主键上,给id=2,id=5 这一行数据,添加行数 S
    同时会在索引 age的值上添加 3个 S gap lock,分别为
    ((4,4),(7,5))、((7,5),(9,2))、((9,2),(21,3))
    tb_unique_index
    ix_age索引查找
    表格tbname 添加 IS 意向锁
    索引上 age = 21 添加 S 行锁
    主键上 id=3 添加 S 行锁
    由于age列唯一,故不需要添加GAP LOCK
    ix_age索引查找
    表格tbname 添加 IS 意向锁
    age索引上面,age=7,age=9 行添加行锁 S
    再在主键上,给id=2,id=5 这一行数据,添加行数 S
    同时会在索引 age的值上添加 2 个 S gap lock,分别为
    ((4,4),(7,5))、((7,5),(9,2))
     
       至此,已说明了四个隔离级别是如何加锁,那么,释放锁呢?
     
       在MySQL INNODB中,遵循的是 strong strict 2-PL,也就是所有的write lock 跟read lock 都是在 事务 commit后才释放。

    3 SQL分析

         考虑到下文的例子,这里补充两个概念。
         ICP:
         MRR:
          
         假设表格 tb_lock ( id int primary key not null, age int,score int,name varchar(10), key ix_age_score ( age, score ) ) 数据修改如下 :
          
     
          假设MySQL当前的隔离级别为 RR,执行 UPDATE tb_index WHERE age between 5 and 22 and score between 1 and 10 and name is not null,其执行计划如下:
          
          那么,是如何加锁的呢?
          首先,可以看到是根据索引 ix_age_score 查找,那么分为两种情况来分析,第一种,数据库支持ICP;第二种,数据不支持ICP。

    3.1 支持ICP情况

          当数据库支持ICP的时候,根据复合索引第一列 age 查找 age between 5 and 22,然后在索引内部过滤 score between 1 and 10后,取得索引值后,如果数据库支持 MRR,则会把取得的索引值放到buffer中,对主键进行排序,然后可以根据顺序的主键值去 主键中查找行数据,如果不支持,则跳过这一步排序步骤,直接根据索引值内部的主键值,查找主键行数,最后过滤 name is not null 。
        
          加锁过程如下,tb_lock添加 IX 意向锁,在索引  ix_age_score 给索引值(7,10,5),(21,4,3)添加上 X record lock,并添加4个 X GAP LOCK,如图片红色素箭头展示,分别为((4,7,4), (7,10,5)),((7,10,5), (9,15,2)),((9,15,2),(21,4,3)),((21,4,3),(25,1,6)),最后在主键上给id=3及id=5 两行数就添加X record lock。

    3.2 不支持ICP情况

          当数据库不支持ICP的时候,根据复合索引第一列 age 选择 age between 5 and 22,然后根据筛选的索引值 (7,10,5),(9,15,2),(21,4,3)中的主键 5、2、3,找到对应的行数据,再在行数据中 过滤 score between 1 and 10 and name is not null。
        
          加锁过程如下,tb_lock添加 IX 意向锁,在索引  ix_age_score 给索引值(7,10,5),(9,15,2),(21,4,3)添加上 X record lock,并添加4个 X GAP LOCK,如图片红色素箭头展示,分别为((4,7,4), (7,10,5)),((7,10,5), (9,15,2)),((9,15,2),(21,4,3)),((21,4,3),(25,1,6)),最后在主键上给id=2、id=3、id=5 两行数就添加X record lock。
     
    参考文档:http://hedengcheng.com/?p=771#_Toc374698321
     
     
  • 相关阅读:
    Eclipse的tab键为4个空格完整方法 附:阿里代码开发规范书
    Linux系统 安装JDK和tomcat
    Window10安装linux
    oracle 查询表重复数据 并 删除保留一条
    Oracle 新增数据 insert into整理
    业需软需word小技巧
    oracle数据库创建表且主键自增
    eclipse更改jdk版本(1.6》1.7 以此类推)
    pytest框架优化——清理历史截图图片和allure报告文件
    allure定制化输出测试报告,让报告锦上添花!
  • 原文地址:https://www.cnblogs.com/xinysu/p/7710928.html
Copyright © 2020-2023  润新知