索引和事务
1. 什么是索引?底层实现原理?
索引:在关系数据库中,索引是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构,它是某个表中一列或若干列值的集合和相应的指向表中物理标识这些值的数据页的逻辑指针清单。索引的作用相当于图书的目录,可以根据目录中的页码快速找到所需的内容。
在一个或者一些字段需要频繁用作查询条件,并且表数据较多的时候,创建索引会明显提高查询速度,因为可由全表扫描改成索引扫描。(无索引时全表扫描也就是要逐条扫描全部记录,直到找完符合条件的,索引扫描可以直接定位)
实现底层原理:当一列索引创建成功后,oracle系统会这个列复制一个份放入缓存,并按照一定规则做排序和去重,从而减少检索数据的范围。但是索引不是创建的越多越好,因为一个索引的创建,就需要在缓存中开辟一个空间,过多的索引反而会加重数据库的检索速度。
2. 索引的创建是不是越多越好?为什么?
索引并不是越多越好,太多索引会占用很多的索引表空间,甚至比存储一条记录更多。
3. 数据库事务ACID
概念:数据库事务是由一系列数据库操作(一个或多个操作)组成的程序执行单元。
典型的数据库事务:
BEGIN TRANSACTION //事务开始
SQL1 //操作1
SQL2 //操作2
COMMIT/ROLLBACK //事务提交或回滚
事务四特性(ACID):
(1)原子性(A):事务所有操作不可分割,要么全部成功,要么全部失败回滚。
(2)一致性(C):事务操作使数据库从一个一致的状态变换到另一个一致状态。
(3)隔离性 (I):并发执行的事务操作数据库时,互不影响,相互隔离的。
(4)持久性(D):事务一旦提交,对数据库的更新是永久的。
事务并发三个问题:
(1)脏读:事务A读取事务B更新的数据,然后B回滚操作,A读取到的是脏数据;
(2)不可重复读:事务 A 多次读取同一数据,事务 B 在事务A读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致;
(3)幻读:系统管理员A更改数据库中学生成绩分数为ABCDE等级,但管理员B在这时仍然插入一条分数的记录,A更改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读(某一事务更改的时候其他事务也操作了,更改事务对新操作的数据不起作用)。
事务并发小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。
4. 数据库的隔离级别
(1)读取未提交:一个事务可以读取另一个未提交事务的数据。产生问题:脏读。
(2)读取提交:只有一个事务提交以后,其他事务才能读取。可避免脏读。
(3)可重复读:事务一旦读取后不可再修改。避免不可重复读问题。
(4)可串行化:可以避免脏读、不可重复读与幻读所有问题,但效率低下,不用。
5. 锁机制
悲观锁:悲观锁悲观的认为每一次操作都会造成更新丢失问题,在每次查询时加上排他锁,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
Select * from xxx for update;
缺点:因为只能保证一个连接进行操作,所以效率低
乐观锁: 乐观锁会乐观的认为每次查询都不会造成更新丢失,利用版本字段控制
重入锁: 重入锁也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但外层函数不受内层函数影响,例如当内部释放锁(unlock)后,外部不会释放。在JAVA环境下 ReentrantLock 和synchronized 都是可重入锁。
读写锁:两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写(读-读能共存,读-写不能共存,写-写不能共存)。这就需要一个读/写锁来解决这个问题。Java5在java.util.concurrent包中已经包含了读写锁。
CAS无锁机制:原子类底层实现保证线程安全就是通过CAS实现。
CAS算法的过程是这样:它包含三个参数CAS(V,E,N): V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。对应java内存模型,V相当于是主内存,E相当于本地内存,如果(V=E),本地内存与主内存一致,说明变量没有被修改过那么就将V要更新的变量的值设置成N新值,如果不相等,说明本地内存被修改,需要将主内存的值刷新到本地内存中去,再进行V和E进行比较,然后再设置成新值N。
一个来自码农翻身的例子:
(1)从内存中读取value值,假设为10,称之为A
(2)B=A+1,得到B=11
(3)用A的值和内存的值相比,如果相等(过去的一段时间内,没人修改过A),就把B写入内存,如果不相等的,说明A在这段时间内被修改了,就放弃这次修改,返回第一步CAS存在一个很明显的问题,即ABA问题。
问题:如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗?
如果在这段期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。针对这种情况,java并发包中提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。
自旋锁:
自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时 才能进入临界区。
当一个线程 调用这个不可重入的自旋锁去加锁的时候没问题,当再次调用lock()的时候,因为自旋锁的持有引用已经不为空了,该线程对象会误认为是别人的线程持有了自旋锁使用了CAS原子操作,lock函数将owner设置为当前线程,并且预测原来的值为空。unlock函数将owner设置为null,并且预测值为当前线程。当有第二个线程调用lock操作时由于owner值不为空,导致循环一直被执行,直至第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。
分布式锁:如果想在不同的jvm中保证数据同步,使用分布式锁技术。有数据库实现、缓存实现、Zookeeper分布式锁