0%

Handler消息机制是怎样的过程?

Looper.loop()流程:

  1. 通过MessageQueue.next() 取Message,没有消息就阻塞当前线程。
  2. 拿到Message返回给Looper.loop的调用处,调用Message绑定的Handler的dispatchMessage处理消息。
  3. 处理结束又回到第1步,无限循环。

每个线程都有各自的Looper,每个Looper有一个MessageQueue。

消息是链表,按时间when排序。

等待新消息阻塞线程和有新消息产生恢复线程的过程是怎样的?

调用 MessageQueue.next() 方法的时候会调用 Native 层的 nativePollOnce() 方法进行精准时间的阻塞。在 Native 层,将进入 pullInner() 方法,使用 epoll_wait 阻塞等待以读取管道的通知。如果没有从 Native 层得到消息,那么这个方法就不会返回。此时主线程会释放 CPU 资源进入休眠状态。

当我们加入消息的时候,会调用 MessageQueue.enqueueMessage() 方法,添加完 Message 后,如果消息队列被阻塞,则会调用 Native 层的 nativeWake() 方法去唤醒。它通过向管道中写入一个消息,结束上述阻塞,触发上面提到的 nativePollOnce() 方法返回,好让加入的 Message 得到分发处理。

Looper.loop()里面有死循环,为什么没有阻塞主线程?

loop()里会从消息队列取消息,取不到消息就阻塞当前线程,释放CPU给其他线程使用,有消息时会唤醒阻塞等待的线程。

IdleHandler是干什么的?

IdleHandler只有一个方法boolean queueIdle()。

此方法在MessageQueue.next()中调用,MessageQueue.next()又在Looper.loop()中调用。

queueIdle()调用时机:

当前消息队列没有可以处理的消息,进入空闲状态,在阻塞等待新的消息前(nativePollOnce方法会阻塞等待新消息),先调用IdleHandler的queueIdle()。

返回true表示下一轮处理完消息后还会回调。

返回false表示这是单次回调,这次回调后不会再回调了。

有新消息时系统会调用MessageQueue的enqueueMessage(),enqueueMessage()调用nativeWake(),唤醒线程。

可以向MessageQueue添加多个IdleHandler。

IdleHandler有什么应用场景?

可以作为View绘制完成的回调,做启动时间优化。

Activity的onCreate,onStart,onResume中耗时较短但非必要的代码可以放到IdleHandler中执行,减少启动时间。

Activity的onResume是在绘制View之前发生的。

因为在ActivityThread的handleResumeActivity()中,调用performResumeActivity()对应Activity的onResume(),然后调用wm.addView(decor,l)对应绘制。

postDelayed(Runnable r, long delayMillis)是如何实现的?

首先从消息池获取一个Message对象,将Runnable对象放入Message的callback属性中;

再通过当前时间和延迟时间计算Runnable执行的时间,调用MessageQueue的enqueueMessage()把消息插入消息链表,执行时间会存入Message对象的when属性中,消息链表是按Message的执行时间升序排序的,插入也会插入到符合顺序的位置。

sendMessage(Message msg)和post(Runnable r)有什么区别?

post()会将Runnable放入一个Message对象的callback属性中,还是会转换为Message,本质上没有区别,只不过post要写的参数更少,使用更方便。

使用Handler有什么要注意的?

要避免内存泄露,自定义Handler要定义静态内部类,并且用弱引用引用外部对象,避免外部对象在消息池中一直被引用而不能垃圾回收进而导致内存泄露。

quit和quitSafely的区别?

quit() 和 quitSafely() 的本质就是让消息队列的 next() 返回 null,以此来退出Looper.loop()。

quit() 调用后直接终止 Looper,不在处理任何 Message,所有尝试把 Message 放进消息队列的操作都会失败,比如 Handler.sendMessage() 会返回 false,但是存在不安全性,因为有可能有 Message 还在消息队列中没来的及处理就终止 Looper 了。

quitSafely() 调用后会在所有消息都处理后再终止 Looper,所有尝试把 Message 放进消息队列的操作也都会失败。

同步消息和异步消息有什么区别?同步屏障(Barrier)是干什么的?

Handler创建时可以传递一个async的布尔值参数,带这个参数的构造函数只有系统才能调用,我们创建Handler时async传的是false,通过Handler进行sendMessage或post都是同步消息,如果async传true,则通过Handler发送的都是异步消息。

Handler的sendMessage、post、postDelayed最终都会调用enqueueMessage,这里会判断如果构造Handler时的async传了true,就设置Message.setAsynchronous(true)。

同步消息和异步消息在没有往消息队列插入同步屏障时没有区别,插入同步屏障后,执行的优先级会有变化。

MessageQueue里的消息都是按照时间升序排序的,执行也是按时间由小到大的Message依次执行,如果有一个高优先级的消息需要立即执行,如果把新消息时间设置为当前时间,可能有好几个消息都是这样,原来的按时间顺序执行的机制就没办法保证执行的先后顺序,这就需要另外的机制来保证,即同步屏障机制。

同步屏障也是一个Message对象,但是没有target(没有绑定Handler),通过MessageQueue的postSyncBarrier(long when)方法向消息队列添加。

在MessageQueue的next()方法中,如果从消息队列取出的是同步屏障的Message(target==null),则从消息队列后面找一个异步消息来执行,如果没找到则一直阻塞等待异步消息。

应用场景:

app层无法调用同步屏障,在系统源码里有使用,如ViewRootImpl的schedualeTraversals,向MessageQueue中添加了内存屏障,保证了measure、layout、draw能够优于普通的Message而得到立即执行。

为什么 View.post 里可以拿到 View 的宽高信息呢?

View.post 和 Handler.post 的区别就是:

  1. 如果在 performTraversals 前调用 View.post,则会将消息进行保存,之后在 dispatchAttachedToWindow 的时候通过 ViewRootImpl 中的 Handler 进行调用。
  2. 如果在 performTraversals 以后调用 View.post,则直接通过 ViewRootImpl 中的 Handler 进行调用。

因为 View.post 的 Runnable 执行的时候,已经执行过 performTraversals 了,也就是 View 的 measure layout draw 方法都执行过了,自然可以获取到 View 的宽高信息了。

View绘制整个流程

Activity走到onResume时,会调用ActivityThread的handleResumeActivity。

在这里会创建DecorView,通过WindowManager添加到PhoneWindow中。

这里也会创建ViewRootImpl,把DecorView的parent指定为ViewRootImpl。

再调用DecorView的requestLayout,requestLayout会层层的调用parent的requestLayout,最后走到ViewRootImpl的requestLayout。

然后走到ViewRootImpl.scheduleTraversals(),注册垂直同步监听。

当垂直同步信号来临时,去调用ViewRootImpl.doTraversal() .

再调用ViewRootImpl.performTraversals(),然后依次调用performMeasure()、performLayout()、performDraw()。


View.invalidate()也会层层调用parent的invalidateChildInParent,最后调用到ViewRootImpl的invalidateChildInParent,然后调用ViewRootImpl.scheduleTraversals()。

等待垂直同步信号来临时,调用ViewRootImpl.doTraversal(),由于没有给View设置FORCE_LAYOUT的flag,所以不会走measure和layout,只会performDraw(),并且只绘制dirty区域。

首次 View 的绘制流程是在什么时候触发的?

ActivityThread.handleResumeActivity 里触发的。

ActivityThread.handleResumeActivity 里会调用 wm.addView 来添加 DecorView,wm 是 WindowManagerImpl

最终通过 WindowManagerImpl.addView -> WindowManagerGlobal.addView -> ViewRootImpl.setView -> ViewRootImpl.requestLayout 就触发了第一次 View 的绘制。

ViewRootImpl 创建的时机?

ActivityThread.handleResumeActivity -> WindowManagerImpl.addView -> WindowManagerGlobal.addView 中创建ViewRootImpl

DecorView的创建时机?

ActivityThread.handleResumeActivity()中ActivityClientRecord.window.getDecorView();

PhoneWindow.getDecorView()调用installDecor() -> generateDecor()去new DecorView()

ViewRootImpl 和 DecorView 的关系是什么?

在 ViewRootImpl.setView 里,通过 DecorView.assignParent 把 ViewRootImpl 设置为 DecorView 的 parent。

所以 ViewRootImpl 和 DecorView 的关系就是 ViewRootImpl 是 DecorView 的 parent。

因为 DecorView 是我们布局的顶层,现在我们就知道层层调用 requestLayout 等方法是怎么调用到 ViewRootImpl 里的了。

Activity、PhoneWindow、DecorView、ViewRootImpl 的关系?

  • PhoneWindow 其实是 Window 的唯一子类,是 Activity 和 View 交互系统的中间层。
  • DecorView 是整个 View 层级的最顶层。
  • ViewRootImpl 是 DecorView 的 parent,但是他并不是一个真正的 View,只是继承了 ViewParent 接口,用来掌管 View 的各种事件,包括 requestLayout、invalidate、dispatchInputEvent 等等。

如何触发重新绘制?

View的 requestLayout 和 invalidate

View.requestLayout()流程?

层层调用 parent 的 requestLayout ,一直到ViewRootImpl.requestLayout()

ViewRootImpl.requestLayout() 调用 scheduleTraversals() -> doTraversal() -> performTraversals() 开启绘制流程。

在 performTraversals 里,就是熟悉的 performMeasure -> performLayout -> performDraw 三个流程了。

在performDraw View 的绘制过程中,我们可以看到,只有 flag 被设置为 PFLAG_DIRTY_OPAQUE 才会进行绘制(这里划重点)。这也就是大家经常说的 requestLayout 不会引发 draw。

View.invalidate()流程?

invalidate -> invalidateInternal -> parent.invalidateChild

invalidateChild的while 循环里,会层层计算 parent 的 dirty 区域,最终会调用到 ViewRootImpl.invalidateChildInParent -> ViewRootImpl.invalidateRectOnScreen -> ViewRootImpl.scheduleTraversals -> ViewRootImpl.performDraw -> ViewRootImpl.draw -> DecorView的draw()

View.draw 方法,根据 PFLAG_DIRTY_OPAQUE flag 去决定是否重新绘制。

requestLayout 和 invalidate 的区别?

requestLayout 和 invalidate 都会触发整个绘制流程。但是在 measure 和 layout 过程中,只会对 flag 设置为 FORCE_LAYOUT 的情况进行重新测量和布局,而 draw 只会重绘 flag 为 dirty 的区域。

requestLayout 是用来设置 FORCE_LAYOUT 标志,invalidate 用来设置 dirty 标志。所以 requestLayout 只会触发 measure 和 layout,invalidate 只会触发 draw。

requestLayout 一定会触发onMeasure和onLayout吗?

不一定。

ViewRootImpl.performMeasure,最终调用的是 View.measure。

measureSpec 和 oldMeasureSpec 不相符的时候才会onMeasure。

ViewRootImpl.performLayout():

位置有变化才去onLayout。

PFLAG_DIRTY_OPAQUE是什么意思?

不透明,实心。

实心控件:控件的onDraw方法能够保证此控件的所有区域都会被其所绘制的内容完全覆盖。换句话说,通过此控件所属的区域无法看到此控件之下的内容,也就是既没有半透明也没有空缺的部分。

什么时候View的flag 被设置为 PFLAG_DIRTY_OPAQUE?

invalidate 会调用 parent.invalidateChild,在这里被赋值的。

事件是如何从屏幕点击最终到达 Activity 的?

CANCEL 事件什么时候会触发?

  1. View 收到 ACTION_DOWN 事件以后,上一个事件还没有结束(可能因为 APP 的切换、ANR 等导致系统扔掉了后续的事件),这个时候会先执行一次 ACTION_CANCEL。
  2. 子 View 之前拦截了事件,但是后面父 View 重新拦截了事件,这个时候会给子 View 发送 ACTION_CANCEL 事件。

如何解决滑动冲突?

  • 通过重写父类的 onInterceptTouchEvent 来拦截滑动事件。
  • 通过在子类中调用 parent.requestDisallowInterceptTouchEvent 来通知父类是否要拦截事件。

Touch事件分发流程

Activity -> Window -> DecorView -> ViewGroup -> View.dispatchTouchEvent()

注意点:

  • 如果在执行ACTION_DOWN时返回false,后面一系列的ACTION_MOVE、ACTION_UP事件都不会执行。
  • 如果在最上层的View的onTouchEvent在DOWN时间返回true,虽然ViewGroup的onInterceptTouchEvent()对DOWN事件返回了false,但后续的事件(MOVE、UP)依然会传递给它的onInterceptTouchEvent()
  • onInterceptTouchEvent()一旦返回一次true,就再也不会被调用了。
  • 当dispatchTouchEvent()事件分发时,只有前一个事件(如ACTION_DOWN)返回true,才会收到后一个事件(ACTION_MOVE和ACTION_UP)
  • dispatchTouchEvent()、 onTouchEvent() 消费事件、终结事件传递(返回true)
  • onInterceptTouchEvent 并不能消费事件,它相当于是一个分叉口起到分流导流的作用,对后续的ACTION_MOVE和ACTION_UP事件接收起到非常大的作用。

Android中数据库相关类

  • SQLiteOpenHelper:管理SQLite的帮助类,提供获取SQLIteDatabase实例的方法,它会在第一次使用数据库时调用获取实例方法时创建SQLiteDatabase实例,并且处理数据库版本变化,开发人员在实现ContentProvider时都要实现一个自定义的SQLiteOpenHelper类,处理数据的创建、升级和降级。
  • SQLiteDatabase:代表一个打开的SQLite数据库,提供了执行数据库操作的接口方法。如果不需要在进程之间共享数据,应用程序也可以自行创建这个类的实例来读写SQLite数据库。
  • SQLiteSession:SQLiteSession负责管理数据库连接和事务的生命周期,通过SQLiteConnectionPool获取数据库连接来执行具体的数据库操作。
  • SQLiteConnectionPool:数据库连接池,管理所有打开的数据库连接(Connection)。所有数据库连接都是通过它来打开,打开后会加入连接池,在读写数据库时需要从连接池中获取一个数据库连接来使用。
  • SQLiteConnection:代表了数据库连接,每个Connection封装了一个native层的sqlite3实例,通过JNI调用SQLite动态库的接口方法操作数据库,Connection要么被Session持有,要么被连接池持有。
  • CursorFactory:可选的Cursor工厂,可以提供自定义工厂来创建Cursor。
  • DatabaseErrorHandler:可选的数据库异常处理器(目前仅处理数据库Corruption),如果不提供,将会使用默认的异常处理器。
  • SQLiteDatabaseConfiguration:数据库配置,应用程序可以创建多个到SQLite数据库的连接,这个类用来保证每个连接的配置都是相同的。
  • SQLiteQuery和SQLiteStatement:从抽象类SQLiteProgram派生,封装了SQL语句的执行过程,在执行时自动组装待执行的SQL语句,并调用SQLiteSession来执行数据库操作。这两个类的实现应用了设计模式中的命令模式。

使用SQLiteOpenHelper的原因

之所以需要使用SQLiteOpenHelper,而不是调用Context的方法来直接得到SQLiteDatabase,主要是因为它有两个好处:

  1. 自动管理创建:当需要对数据库进行操作的时候,不用关心SQLiteOpenHelper所关联的SQLiteDatabase是否创建,SQLiteOpenHelper会帮我们去判断,如果没有创建,那么就先创建该数据库后,再返回给使用者。
  2. 自动管理版本:当需要对数据库进行操作之前,如果发现当前声明的数据库的版本和手机内的数据库版本不同的时候,那么会分别调用onUpgrade和onDowngrade,这样使用者就可以在里面来处理新旧版本的兼容问题。

# SQLiteOpenHelper跟数据库连接池的关系是怎样的?

一个SQLiteOpenHelper对象存有一个SQLiteDatabase对象

一个SQLiteDatabase对象存有一个SQLiteConnectionPool对象

一个SQLiteConnectionPool对象

  1. 在journal-mode下最多存有1个SQLiteConnection
  2. 在wal-mode下最多存有2个SQLiteConnection

SQL 语句的执行过程

创建SQLiteStatement对象,把SQL语句传给SQLiteStatement构造函数,调用SQLiteStatement的executeInsert()

SQLiteStatement.executeInsert()内部的增删改查方法都会先getSession()获取SQLiteSession,再调用SQLiteSession的execute方法

继续追查可以发现SQLiteSession是在SQLiteDatabase里以ThreadLocal形式存储,也就是每个线程只能有一个SQLiteSession

SQLiteSession的execute方法里会从数据库连接获取一个连接,每个SQLiteSession对象有一个SQLiteConnection对象

SQLiteSession存在的价值

给数据库连接代理了一层,管理事务和连接的生命周期。

在事务执行过程中一直持有数据库连接。

由于数据库连接数量是有限的,所以连接用完后要及时释放连接。

事务开启后,从连接池获取连接。

事务结束后,把连接归还到连接池。

数据库连接是什么意思?

一个连接对应一个事务操作

如果一个数据库连接始终不关闭会有什么影响?

SQLite连接数有最大限制,不关闭,会导致别的进程无法连接数据库。

SQLiteConnectionPool连接池大小是怎么定的?

目前Android系统的实现中,如果以非WAL模式打开数据库,连接池中只会保持一个数据库连接,如果以WAL模式打开数据库,连接池中的最大连接数量则根据系统配置决定,默认配置是两个。

数据库连接池只提供最多1个连接会有什么影响?

如果应用程序中有大量的并发数据库读和写操作的话,每个操作的时长都可能受到影响,所以数据库操作应放在工作线程中执行,以免影响UI响应。

因为每个事务都是在SQLiteSession中执行的,执行事务前会从连接池获取连接,调用的是SQLiteConnectionPool.acquireConnection(),方法内如果获取不到Connection就会阻塞线程等待,用链表存储阻塞的线程,直到连接被释放,也就是别的事务执行完成。

我们平时在多线程中的数据库操作都是串行的。

Android里开启事务,事务模式用的是哪个?

  • SQLiteDatabase.beginTransaction()开启的是EXCLUSIVE。
  • SQLiteDatabase.beginTransactionNonExclusive()开启的是IMMEDIATE。

注意这两个事务模式只针对回滚日志模式下的事务。

SQLite官网说在WAL日志模式下,EXCLUSIVE和IMMEDIATE是一样的。

事务是什么?

事务定义了一组数据库操作的边界,这组操作要么全部执行,要么全部不执行,这也是事务的原子性特征

为什么要发明事务?事务存在的价值是什么?

在操作数据库的情况下,需要考虑的特别的场景处理特别多,例如并发操作、系统崩溃等,发明事务这个模型,就可以用来简化讨论,把复杂的场景归类为少数的几个特定类型的场景,降低处理成本。

数据库操作例如增删改查、建立索引、建立约束等。

事务有哪些特性?

ACID

Atomicity 原子性

事务内的操作要么全部执行,要么全部不执行,不可再拆分

Consistency 一致性

保证数据库从一个正确的状态转变到另一个正确的状态,正确的状态指的是当前数据库中的数据都满足预定的约束条件。

AID是保证一致性的必要条件,但不是充分条件,因为数据库作为通用的技术,不可能知道具体业务场景的正确逻辑,所以正确的逻辑需要由用户决定应该使用怎样的约束

Isolation 隔离性

事务的执行不受其他事务的干扰,事务之间的操作只能串行的执行,保证任何事务都不可能读取到其他任何事务内部执行的中间状态,否则会产生数据混乱

Durability 持久性

事务一旦提交,它对数据库中的数据改变就是永久性的,接下来的其他操作或故障都不会影响本次事务提交的结果

事务的原子性是如何实现的?

SQLite事务的实现是依赖于名为rollback journal文件,借助这个临时文件来完成原子操作和回滚功能。

回滚日志文件,用于实现数据库的原子提交和回滚。 此文件和数据库文件总是在同一个目录,并且有相同的文件名,但是在文件名中添加了一个-journal 字符串。此文件一般在transaction开始时创建,transaction结束时删除。

如果系统crash,Rollback journals文件将被保留,下次打开数据库文件时,系统会检查有没有Rollback journals文件存在,如果有就用它来恢复数据库。

创建回滚日志详细过程是怎样的?

  1. 写数据库前获取保留锁
  2. 创建回滚日志文件,将要修改的页的原始数据写到缓存中
  3. 在用户空间修改页数据
  4. 回滚日志的缓存刷盘到回滚日志文件中
  5. 获取未决锁,等待共享锁都释放后,提升为排它锁
  6. 将用户空间修改过的数据写入数据库文件,会先写到操作系统的缓存中,再刷盘到磁盘上
  7. 删除rollback journal文件
  8. 释放排它锁

创建rollback journal file

将要修改的pages的原始数据写入rollback journal file,以page为单位写入。

rollback journal file中绿色的部分是header,header中会包括数据库文件的原始大小(即包括多少个page)。每一个page保存到rollback journal中时前边四字节会保存该page的page number。

  • rollback journal创建之后并没有实际落盘,只是保存在操作系统的缓存中
  • rollback journal以page为单位,但每个page前四字节会有一个page number的记录,后四字节有一个checksum

在用户空间修改数据库文件

修改用户进程空间内的数据库文件,注意不同的用户进程有自己的私有内存空间,因此此时的修改并不影响其他进程的读取操作。

将rollback journal落盘

该步骤是保持原子性很关键的一步,保证即使掉电或者操作系统crash,Sqlite也能恢复到原来的状态。

(进行到该步也能看出reserved lock的作用,这种锁是一个中间状态,既能为即将写入做准备,又不影响其他进程的读取操作,提高并行度)。

该步需要刷两次盘,第一次将rollback journal的内容刷到磁盘,第二次在header中记录第一步中刷到磁盘的page个数,然后将header刷盘。

获取排他锁

在实际写入数据库文件之前,需要获取一个排他锁.获取过程分两步
• 获取一个pending lock
• 将pending lock升级为排他锁
获取到pending lock之后,在数据库文件上已经获取到共享锁的进程可以继续读取,但不允许其他进程继续获取共享锁.该锁存在的意义在于防止write starvation(即有大量的读取连接时,一直有新的共享锁产生,导致获取不到排他锁).当所有已经存在的共享锁都释放后,此时该pending lock即可以升级为排他锁。

写入数据库文件

获取到排他锁后,说明已经没有其他进程在读取该数据库文件.此时可以安全的写入数据库文件.注意也只是写入操作系统的缓存中,并没有落盘。

删除rollback journal

因为数据库文件已经安全落盘,此时可以删除掉rollback journal.若删除之前系统crash或者掉电,则重启后会恢复到事务开始前的状态,如果删除之后系统crash或者掉电,因为数据库文件已经落盘,相当于事务已经执行完成.(那么会不会在删除一半时系统crash或者掉电呢?注意上文中关于硬件的一些假设,删除操作必须是原子性的,即不会发生这种情况)

因为删除一个文件也是一个耗时的操作,因此Sqlite提供了两种方式减少删除过程的耗时.

  • 将一个文件truncate为0
  • 将journal file header清0.清0操作并不是原子性的,但只要header中有一个byte被清0,该文件就会被识别为无效的格式

参考:

热日志是什么意思?

每次打开数据库文件,或者从数据库读取页时,都要简单的做一致性检查。

如果发现存在回滚日志文件,但是数据库中没有保留锁,那么创建日志文件的肯定崩溃了或者系统死机了,因为正常情况下,获得保留锁后才会创建回滚日志。

这种情况下日志被称为“热日志”,数据库可能处于不一致的状态(不满足约束条件),要让一切正常需要回滚日志,将数据库还原到初始的被中断的事务之前的状态。

参考《SQL权威指南(第2版)》159页 第5章 SQLite设计与概念 - 写事务

SQLite中事务为什么还要分不同的类型?分别有什么作用?

是为了解决死锁

SQLite的事务分为

  1. deferred
  2. immediate
  3. exclusive

begin [ deferred | immediate | exclusive ] transaction

deferred

  1. 未指定事务模式时默认的选择
  2. 开始事务时处于未锁定状态
  3. 实际访问数据库时才试图加锁
  4. 第一个读数据库操作试图获取共享锁
  5. 第一个写数据库操作试图获取预留锁

immediate

  1. 事务开始时试图获取预留锁
  2. 获取预留锁成功后,其他连接中已执行的事务无法再获取到预留锁
  3. 新的连接开始immediate和exclusive的事务也会失败,并返回SQLITE_BUSY错误
  4. 最后提交事务时,预留锁会提升到未决锁
  5. 等待其他连接中的事务释放共享锁
  6. 阻止新的连接获取共享锁
  7. 如果其他事务一直没释放共享锁,会返回SQLITE_BUSY错误

exclusive

  1. 开始事务时尝试获取排它锁
  2. 获取成功后阻止所有的读写操作
  3. 在事务内可以对数据库进行任意的读写

参考《SQLite权威指南(第2版)》118页 第4章 SQLite中的高级SQL - 事务的类型

SQLite WAL是什么?

SQLite在3.7.0开始引入了WAL技术

Write-Ahead Log

预先写日志

默认的 rollback journal 模式工作原理大致为:写操作进行前进行数据库文件拷贝,然后对数据库进行写操作。如果发生 Crash 或者 rallback 则将日志中的原始内容回滚到数据库中进行恢复操作,否则在 Commit 完成时删除日志文件。

WAL模式采用了相反的的做法,

在准备写数据库文件前,会先复制数据库文件的中的原始数据到日志文件,写操作也只更新日志文件,原有数据库文件不变动;

如果事务失败,WAL中的数据被忽略;

如果事务成功,在随后的某个时间点,将WAL中的数据写回到数据库文件,这个时间点称为checkpoint。

WAL使用检查点将修改写回数据库,默认情况下,当WAL文件发现有1000页修改时,将自动调用检查点。这个页数大小可以自行配置。

读数据时,SQLite在WAL中搜索,找到最后一个写入点,并记录,读时忽略这个写入点后面的信息,如果读取在WAL中读取不到数据,则去数据库文件中读取;写数据时继续往记录点后追加写入数据,这样保证了读和写可以同时进行;由于同一时刻只能有一个连接写数据库,写与写之间是互斥的,所以不会产生写入点后面写数据时指针冲突。

由于每个读操作都会搜索整个WAL文件,所以在共享内存加建立了一个wal-index索引,加速查找,避免扫描整个WAL文件。

WAL提升了哪方面的性能?优点是什么?解决了什么问题?

参考:《SQLite权威指南(第2版)》第11章 sqlite内部机制及新特性 - wal的优缺点


  1. WAL is significantly faster in most scenarios.
  2. WAL provides more concurrency as readers do not block writers and a writer does not block readers. Reading and writing can proceed concurrently.
  3. Disk I/O operations tends to be more sequential using WAL.
  4. WAL uses many fewer fsync() operations and is thus less vulnerable to problems on systems where the fsync() system call is broken.

参考:https://sqlite.org/wal.html

WAL的缺点是什么?

  1. 所有的数据库操作必须都在同一台机器上进行,并且该机器的操作系统需要支持 VFS 特性。
  2. 当连接处于 WAL 模式中时我们无法修改页大小
  3. 为满足 Wal 和相关共享内存的需要,使用 WAL 引入了两个额外的半持久性文件 -wal 和 -shm 该文件需要占用一定的存储空间。
  4. 数据库读性能会比 rollback journal 模式略差 (大概慢 1% ~ 2% ),另外写操作也会间歇性的性能下降。
  5. 读操作的性能会比 rollback journal 模式出现部分下降,因为它需要额外对 -wal 文件进行一次检索,而且 Checkpoint 本身就比较耗时且会对读操作进行阻塞。
  6. 频繁 Checkpoint 变得频繁又会影响写操作的性能指标,而且频繁的同步操作也会增加数据库损坏的概率


参考:《SQLite权威指南(第2版)》第11章 sqlite内部机制及新特性 - wal的优缺点


  1. All processes using a database must be on the same host computer; WAL does not work over a network filesystem.
  2. Transactions that involve changes against multiple ATTACHed databases are atomic for each individual database, but are not atomic across all databases as a set.
  3. WAL might be very slightly slower (perhaps 1% or 2% slower) than the traditional rollback-journal approach in applications that do mostly reads and seldom write.
  4. There is an additional quasi-persistent “-wal” file and “-shm” shared memory file associated with each database, which can make SQLite less appealing for use as an application file-format.

参考:https://sqlite.org/wal.html

Android中如何开启WAL?

SQLiteDatabase.enableWriteAheadLogging()

根据注释描述,Android默认没有开启WAL。

SQLite锁的粒度到哪里?

SQLite锁的是整个数据库文件,不支持页锁、表锁和行锁,粒度较粗。

当一个连接要写数据库文件时,所有其他的连接都会阻塞,直到写数据的连接事务结束。

SQLite 3.7.0 新增 Write-Ahead Log 机制改变了事务行为,读写可以并发。

为什么SQLite不支持表锁和行锁呢?

支持高度的写并发会带来很大的复杂性,这将使SQLite的简单性无法保持。

同时复杂性增大会增加运行设备电量的损害,对于手机这类这种电量敏感的设备比较重要。

SQLite锁机制是怎样的?

有多个数据库连接同时访问同一个数据库,就产生了资源竞争,需要锁机制保证资源竞争的正确性

数据库操作无非就是读和写,按道理普通的读写锁就可以实现,但高并发下读写锁仍然有优化的空间,为了提高整体的吞吐量,SQLite使用了锁逐步提升的机制

SQLite共有5种锁状态:

  1. 无锁(unlocked)
  2. 共享(shared)
  3. 预留(reserved)
  4. 未决(pending)
  5. 排它(exclusive)

每个数据库连接同一时刻只能处于其中一个状态

无锁:

  1. 即使事务已经开始,在没有读写数据库前,都是无锁状态

共享:

  1. 读数据库必须获得共享锁
  2. 多个数据库连接可以同时获得共享锁,即允许多个连接同时读数据库
  3. 写数据库前必须要释放所有的共享锁

预留:

  1. 写数据需要先获取到预留锁
  2. 持有预留锁后就可以立即把数据写入缓存中,而不用竞争数据库文件,把可以不占用的数据库文件可以完成的事情提前完成,这样最大程度的减少独占数据库文件的时间
  3. 预留锁可以与共享锁共存
  4. 预留锁不影响其他已经持有共享锁的连接继续读数据库
  5. 预留锁不阻止其他连接获得新的共享锁,因为并不确定是否要立即写入数据库文件,可能之后还要修改数据库内容,提交事务的时候才知道后面不会再修改数据库了
  6. 一个数据库同时只能有一个预留锁存在

未决:

  1. 当连接提交事务时,就要把缓存里的数据往数据库文件中写入,以保证事务的持久性
  2. 对数据库文件写数据前,需要保证没有连接再持有共享锁
  3. 但此时可能还有其他连接持有共享锁,需要等待这些连接释放共享锁后,才能写数据
  4. 同时要阻止新的连接获取共享锁,否则写数据一直得不到执行
  5. 所以从预留锁要转变为未决锁,来阻止新的连接获取共享锁

排它:

  1. 所有共享锁释放后,未决锁提升为排他锁
  2. 排它锁和未决锁一样,会阻止新的连接获取共享锁,阻止读数据
  3. 获得排它锁后,就可以把缓存中的数据写入数据库文件

参考资料:

《SQLite权威指南(第2版)》116页 第4章 SQLite中的高级SQL - 数据库锁

SQLite锁为什么搞这么多状态?

  • 由于写数据时是排他的,其他连接无法读数据库,所以要尽可能的减少写数据时的耗时,才能提高系统整体的吞吐量。
  • 所以写数据时先获取预留锁,把可以在不占用数据库文件的事情提前做好。
  • 那些需要占用数据库文件才能做的事情留到获取到排他锁时再做。
    • 获取排它锁前需要保证没有连接再获取共享锁。
    • 所以先从预留锁提升到未决锁,用来阻止新的共享锁的获取。
    • 等待已经获取的共享锁的连接执行完后释放了所有的共享锁,最后获取到排它锁写数据库文件。

锁的状态转移过程是怎样的?

为什么事务的开始都要先获取未决锁,再获取共享锁?

因为如果已经获得了未决锁,说明要占用数据库文件进行写操作,就不允许读了,而读数据库需要获得共享锁,这里就阻止别的连接获取共享锁。

有了预留锁,为什么还需要一个未决锁?未决锁存在的作用是什么?没有未决锁会发生什么?

获得预留锁时,读写可以并发执行,但是真正写数据库需要阻止共享锁获取。

这样做可以提升系统整体的吞吐量。

SQLite锁机制需要注意什么?

注意死锁的发生

SQLite什么情况下会发生死锁?

SQLite发生死锁时会抛出database is locked的异常信息。

参考《SQL权威指南(第2版)》117页 SQLite中的高级SQL - 死锁

如何避免死锁?

用正确的事务类型来开启事务

存在并发写数据,就开启immediate或exclusive事务,提供了同步机制

SQLite有三种不同的事务类型:

  1. DEFERRED(推迟)
  2. MMEDIATE(立即)
  3. EXCLUSIVE(排它)

事务类型在BEGIN命令中指定。

一个deferred事务不获取任何锁,直到它需要锁的时候。而且BEGIN语句本身也不会做什么事情——它开始于UNLOCK状态;默认情况下是这样的。如果仅仅用BEGIN开始一个事务,那么事务就是DEFERRED的,同时它不会获取任何锁,当对数据库进行第一次读操作时,它会获取SHARED LOCK;同样,当进行第一次写操作时,它会获取RESERVED LOCK。

由BEGIN开始的Immediate事务会试着获取RESERVED LOCK。如果成功,BEGIN IMMEDIATE保证没有别的连接可以写数据库。但是,别的连接可以对数据库进行读操作,但是RESERVED LOCK会阻止其它的连接BEGIN IMMEDIATE或者BEGIN EXCLUSIVE命令,SQLite会返回SQLITE_BUSY错误。这时你就可以对数据库进行修改操作,但是你不能提交,当你COMMIT时,会返回SQLITE_BUSY错误,这意味着还有其它的读事务没有完成,得等它们执行完后才能提交事务。

Exclusive事务会试着获取对数据库的EXCLUSIVE锁。这与IMMEDIATE类似,但是一旦成功,EXCLUSIVE事务保证没有其它的连接,所以就可对数据库进行读写操作了。 上面那个例子的问题在于两个连接最终都想写数据库,但是他们都没有放弃各自原来的锁,最终,shared 锁导致了问题的出现。如果两个连接都以BEGIN IMMEDIATE开始事务,那么死锁就不会发生。在这种情况下,在同一时刻只能有一个连接进入BEGIN IMMEDIATE,其它的连接就得等待。BEGIN IMMEDIATE和BEGIN EXCLUSIVE通常被写事务使用。就像同步机制一样,它防止了死锁的产生。 基本的准则是:如果你在使用的数据库没有其它的连接,用BEGIN就足够了。但是,如果你使用的数据库在其它的连接也要对数据库进行写操作,就得使用BEGIN IMMEDIATE或BEGIN EXCLUSIVE开始你的事务。

Android中对SQLite开启的哪一种事务类型?

SQLiteDatabase开启事务有两个方法:

  1. beginTransaction()开启exclusive事务类型
  2. beginTransactionNonExclusive()开启immediate事务类型

锁的状态存储在哪里?

数据库文件是独立于进程的,多个进程可以访问同一个文件,所以数据库锁是存在数据库文件中的。

参考《SQL权威指南(第2版)》159页 第5章 SQLite设计与概念 - 写事务

SQLite支持多线程吗?

SQLite支持多线程,但是是有条件的支持,也就是:

  • 同一个连接不能在多线程中使用,不同连接才可以在多线程中使用,这个是最宏观的SQLite多线程准则。
  • SQLite的文件锁是粗颗粒的,也就是以数据库文件为维度加锁,涉及到5种锁状态。

为了确保数据库安全,SQLite 内部抽象了两种类型的互斥锁(锁的具体实现和宿主平台有关)来应对线程并发问题:

  • fullMutex
    可以理解为 connection mutex,和连接句柄(上问描述的 sqlite3 结构体)绑定。
    保证任何时候,最多只有一个线程在执行基于连接的事务。
  • coreMutex
    当前进程中,与文件绑定的锁。
    用于保护数据库相关临界资源,确保在任何时候,最多只有一个线程在访问。

SQLite三种线程模型

  • single-thread
    • coreMutex 和 fullMutex 都被禁用。
    • 用户层需要确保在任何时候只有一个线程访问 API,否则抛出异常。
  • multi-thread
    • coreMutex 保留,fullMutex 禁用。
    • 可以多个线程基于不同的连接并发访问数据库,但单个连接在任何时候只能被一个线程访问。
    • 单个 connection,如果并发访问,会抛出异常。
      • 报错信息:illegal multi-threaded access to database connection。
  • serialized
    • coreMutex 和 fullMutex 都保留。

同一个数据库连接多线程访问会有什么问题?

一个数据库连接对应一个事务操作,多线程访问一个连接会造成事务不原子化,事务执行结果会混乱。

不同的连接同时写数据库会发生什么?

就走入了SQLite本身的锁机制。

写数据会去获取保留锁、独占锁。

如果还有连接要写入,就会返回SQLITE_BUSY。

Android中的SQLite的Thread Mode是什么?

SQLite官网说默认线程模式是serialized

Android的SQLiteDatabase类的setLockingEnabled(boolean lockingEnabled)文档说,默认线程模式是Multi-thread。

API 16 setLockingEnabled() 方法又废弃了。

不管怎么说肯定不是single-thread,不是multi-thread就是serialized。

参考:
What is the Default Threading mode of SQLite in Android?

索引的作用?

按某列的条件做查询后(where子句中访问的列),需要线性扫描全表太慢了。

用B+树实现索引可以在对数时间内完成查询。

索引为什么不用平衡二叉查找树,要用B树或B+树做索引?

B树又称多路平衡二叉查找树,一个结点存储多个值,可以降低树的高度,即降低了访问结点的次数,一次访问可以认为是一次IO,降低了IO次数就会提高整体的访问速度。

为什么不用哈希表做索引?查询时间复杂度不是O(1)?

  1. 哈希索引对于范围查询和排序却无法很好地支持,最终导致全表扫描
  2. 哈希索引不支持多列联合索引的最左匹配规则
  3. 如果有大量重复键值的情况下,哈希索引的效率会很低,因为存在哈希碰撞问题

为什么用B+树做索引而不用B树?

由于B+树的内部节点只存放键,不存放值,因此,一次读取,可以在内存页中获取更多的键,有利于更快地缩小查找范围。

B+树的叶节点由一条链相连,因此,当需要进行一次全数据遍历的时候,B+树只需要使用O(logN)时间找到最小的一个节点,然后通过链进行O(N)的顺序遍历即可。

而B树则需要对树的每一层进行遍历,这会需要更多的内存置换次数,因此也就需要花费更多的时间。

使用B树做索引的好处?

B树可以在内部节点同时存储键和值,因此,把频繁访问的数据放在靠近根节点的地方将会大大提高热点数据的查询效率。这种特性使得B树在特定数据重复多次查询的场景中更加高效。

索引有什么缺点?

  1. 占用空间,因为会把列的信息都复制一遍
  2. 插入、修改、删除时需要额外花时间更新索引

索引适用场景?

适用查询非常频繁而更新不频繁的列。

where子句中对列的访问

order by

当我们使用order by将查询结果按照某个字段排序时,如果该字段没有建立索引,那么执行计划会将查询出的所有数据使用外部排序(将数据从硬盘分批读取到内存使用内部排序,最后合并排序结果),这个操作是很影响性能的,因为需要将查询涉及到的所有数据从磁盘中读到内存(如果单条数据过大或者数据量过多都会降低效率),更无论读到内存之后的排序了。

但是如果我们对该字段建立索引alter table 表名 add index(字段名),那么由于索引本身是有序的,因此直接按照索引的顺序和映射关系逐条取出数据即可。而且如果分页的,那么只用取出索引表某个范围内的索引对应的数据,而不用像上述那取出所有数据进行排序再返回某个范围内的数据。(从磁盘取数据是最影响性能的)

join

对join语句匹配关系(on)涉及的字段建立索引能够提高效率

索引覆盖

如果要查询的字段都建立过索引,那么引擎会直接在索引表中查询而不会访问原始数据(否则只要有一个字段没有建立索引就会做全表扫描),这叫索引覆盖。因此我们需要尽可能的在select后只写必要的查询字段,以增加索引覆盖的几率。

为什么外键要加索引?

避免子表上的全表扫描。(外键在子表上,外键对应主表的主键)

假设删除departments主表id=10的记录,如果employees子表的department_id外键没有索引,那么就会全表扫描employees子表,以确认是否存在department id=10的记录。

联合索引是什么?

对多个字段联合创建的索引。

只有在查询条件中使用了这些字段的左边字段时,索引才会被使用,使用组合索引时遵循最左前缀集合。

通俗理解:

利用索引中的附加列,您可以缩小搜索的范围,但使用一个具有两列的索引 不同于使用两个单独的索引。复合索引的结构与电话簿类似,人名由姓和名构成,电话簿首先按姓氏对进行排序,然后按名字对有相同姓氏的人进行排序。如果您知道姓,电话簿将非常有用;如果您知道姓和名,电话簿则更为有用,但如果您只知道名不姓,电话簿将没有用处。

所以说创建复合索引时,应该仔细考虑列的顺序。对索引中的所有列执行搜索或仅对前几列执行搜索时,复合索引非常有用;仅对后面的任意列执行搜索时,复合索引则没有用处。

查英语字典、汉语字典也是这样。

联合索引的底层实现是怎样的?

索引的底层是一颗B+树,联合索引当然还是一颗B+树,只不过联合索引的健值数量不是一个,而是多个。

构建一颗B+树只能根据一个值来构建,因此数据库依据联合索引最左的字段来构建B+树。

例子:假如创建一个(a,b)的联合索引,那么它的索引树是这样的:

可以看到a的值是有顺序的,1,1,2,2,3,3,而b的值是没有顺序的1,2,1,4,1,2。所以b = 2这种查询条件没有办法利用索引,因为联合索引首先是按a排序的,b是无序的。

同时我们还可以发现在a值相等的情况下,b值又是按顺序排列的,但是这种顺序是相对的。所以最左匹配原则遇上范围查询就会停止,剩下的字段都无法使用索引。例如a = 1 and b = 2 a,b字段都可以使用索引,因为在a值确定的情况下b是相对有序的,而a>1and b=2,a字段可以匹配上索引,但b值不可以,因为a的值是一个范围,在这个范围中b是无序的。

联合索引会对最左边第一个字段排序,在第一个字段的排序基础上,然后在对第二个字段进行排序,以此类推。

所以如果要利用到联合索引中靠后列的索引,前面列就必须相等,如果前面列不相等(比如用范围查询),那么后面的列没办法保证顺序,后面列的顺序都是在前面列相等的情况下才保持顺序的。

为什么表必须有主键,并且推荐使用整型的自增主键?

不建主键不代表没有主键,没有建主键innodb会帮你选一个字段,一个可以标识唯一的字段,选为默认字段,如果这个字段唯一的话,不重复,可一键唯一索引的话,就会作为类似于唯一索引,用这个字段来作为唯一索引来维护整个表的数据。如果没有,mysql会生成一个唯一的列,类似于rowid,只不过你看不到,他会用生成的这个唯一列,维护B+Tree的结构,查数据的时候还是用B+Tree的结构去查找。

为什么推荐整型呢?

我们想象一下查找过程,是把节点load到内存然后在内存里去比较大小,也就是在查找的过程中要不断的去进行数据的比对。假设UUID,既不自增也不是整形。问一下,是整形的1<2比较的效率高还是字符串的“abc”和“abe”比较的效率高呢?显然是前者,因为字符串的比较是转换成ASICI一位一位的比,如果最后一位不一样,比到最后才比较出大小,就比整形比较慢多了,存储空间来说,整形更小。索引越节约资源越好。

表因为使用UUId(随机ID)作为主键,使数据存储稀疏,这就会出现聚簇索引有可能有比全表扫面更慢,

所以建议使用int的auto_increment作为主键。

主键的值是顺序的,所以每一条记录都存储在上一条记录的后面。当达到页的最大填充因子时,下一条记录就会写入新的页中。一旦数据按照这种顺序的方式加载,主键页就会近似于被顺序的记录填满。

为什么是自增的呢?

聚簇索引的数据的物理存放顺序与索引顺序是一致的,即:只要索引是相邻的,那么对应的数据一定也是相邻地存放在磁盘上的。如果主键不是自增id,那么可以想象,它会干些什么,不断地调整数据的物理地址、分页,当然也有其他一些措施来减少这些操作,但却无法彻底避免。但,如果是自增的,那就简单了,它只需要一 页一页地写,索引结构相对紧凑,磁盘碎片少,效率也高。

聚簇索引是什么?

聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。

数据库表数据是用B+树来存储组织的,那么这个B+树可以认为是一种索引,这就是聚簇索引。

索引是针对某一列而言的,一般的聚簇索引就是主键列。

聚簇索引确定了表数据的物理存储顺序,聚簇索引B+树的叶子结点存储的是整个行数据

由于聚簇索引规定数据在表中的物理存储顺序,因此一个表只能包含一个聚簇索引。

聚簇索引类似于电话簿,按姓氏排列数据。汉语字典也是聚簇索引的典型应用,在汉语字典里,索引项是字母+声调,字典正文也是按照先字母再声调的顺序排列。

当查询使用聚簇索引时,在对应的叶子节点,可以获取到整行数据,因此不用再次进行回表查询。

如果是普通的索引,B+树的叶子结点存储的行的主键,然后需要再去聚簇索引下去查询一遍,找到完整的行数据,有一个回表查询的过程,所以在聚簇索引上查询会少了回表查询的过程,查询速度快。

MySQL官方对聚簇索引的解释:

The InnoDB term for a primary key index. InnoDB table storage is organized based on the values of the primary key columns, to speed up queries and sorts involving the primary key columns. For best performance, choose the primary key columns carefully based on the most performance-critical queries. Because modifying the columns of the clustered index is an expensive operation, choose primary columns that are rarely or never updated.

注意标黑的那段话,聚簇索引就是主键的一种术语。

聚簇索引有什么用?

聚簇索引对于那些经常要搜索范围值的列特别有效。使用聚簇索引找到包含第一个值的行后,便可以确保包含后续索引值的行在物理相邻。例如,如果应用程序执行的一个查询经常检索某一日期范围内的记录,则使用聚集索引可以迅速找到包含开始日期的行,然后检索表中所有相邻的行,直到到达结束日期。这样有助于提高此类查询的性能。同样,如果对从表中检索的数据进行排序时经常要用到某一列,则可以将该表在该列上聚簇(物理排序),避免每次查询该列时都进行排序,从而节省成本。

在聚簇索引下,数据在物理上按顺序排在数据页上,重复值也排在一起,因而在那些包含范围检查(between、<、<=、>、>=)或使用group by或orderby的查询时,一旦找到具有范围中第一个键值的行,具有后续索引值的行保证物理上毗连在一起而不必进一步搜索,避免了大范围扫描,可以大大提高查询速度。

为什么非聚簇索引结构叶子节点存储的是主键值?

为了一致性和节省存储空间。已经维护了一套主键索引+数据的B+Tree结构,如果再有其他的非主键索引的话,索引的叶子节点存储的是主键,这是为了节省空间,因为继续存数据的话,那就会导致一份数据存了多份,空间占用就会翻倍。另一方面也是一致性的考虑,都通过主键索引来找到最终的数据,避免维护多份数据导致不一致的情况。

哪些列适合作为聚簇索引?

1、主键列,该列在where子句中使用并且插入是随机的。

2、按范围存取的列,如pri_order > 100 and pri_order < 200。

3、在group by或order by中使用的列。

4、不经常修改的列。

5、在连接操作中使用的列。

聚簇索引的优点和缺点?

优点

  1. 适合范围查询
  2. 适配排序
  3. 当通过聚簇索引查找目标数据时理论上比非聚簇索引要快,因为非聚簇索引只能定位到对应主键,然后要再回表查询聚簇索引,才能找到完整的行数据。

缺点

1.插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,我们一般都会定义一个自增的ID列为主键。

2.更新主键的代价很高,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新。

3.二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据。

二级索引的叶节点存储的是主键值,而不是行指针(非聚簇索引存储的是指针或者说是地址),这是为了减少当出现行移动或数据页分裂时二级索引的维护工作,但会让二级索引占用更多的空间。

4.采用聚簇索引插入新值比采用非聚簇索引插入新值的速度要慢很多,因为插入要保证主键不能重复,判断主键不能重复,采用的方式在不同的索引下面会有很大的性能差距,聚簇索引遍历所有的叶子节点,非聚簇索引也判断所有的叶子节点,但是聚簇索引的叶子节点除了带有主键还有记录值,记录的大小往往比主键要大的多。这样就会导致聚簇索引在判定新记录携带的主键是否重复时进行昂贵的I/O代价。

二级索引是什么?

表中的聚簇索引(clustered index )就是一级索引,除此之外,表上的其他非聚簇索引都是二级索引,又叫辅助索引(secondary indexes)。

除了聚簇索引以外的所有索引都称为二级索引,二级索引的叶子节点内容是主键的值。

二级索引没有存储全部的数据,假如二级索引满足查询需求,则直接返回,即为覆盖索引,反之则需要回表去主键索引(聚簇索引)查询。

何时使用聚簇索引与非聚簇索引?

索引的设计原则

(1)适合索引的列是出现在where子句中的列,或者连接子句中指定的列,即较频繁作为查询条件的字段才去创建索引

(2)取值离散小、查询中很少涉及、重复值比较多的列,索引效果较差,没有必要在此列建立索引

(3)使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间

(4)不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。

(5)更新频繁字段不适合创建索引。

什么情况下索引会失效?

  1. 不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描
  2. 存储引擎不能使用索引范围条件右边的列
  3. 尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),减少select *
  4. mysql在使用不等于(!=或者<>)的时候无法使用索引会导致全表扫描
  5. is null,is not null也无法使用索引
  6. like以通配符开头(’%abc…’)mysql索引失效会变成全表扫描的操作。