当前位置: 首页 > 科技观察

从Linux内核看InnoDB同步机制的实现(下)

时间:2023-03-15 15:19:40 科技观察

1:简介InnoDB符合MVCC(Multi-VersionConcurrencyControl)规范。通俗的说就是写锁,读锁,读写冲突(有些情况下不符合MVCC,比如隔离级别为serializable时,有读写冲突,这个是相关的到autocommit参数的值,所以我不会在这里谈论它)。只有这样的前提,mysql才能有很好的并发处理能力,但是很多时候我们不希望多个线程同时修改同一个数据。我们的做法是设置隔离级别来保证数据的一致性。接下来我们来讨论一下InnoDB中的同步机制。二:为什么需要同步机制?比如我们在mysql-server上与客户端建立了三个连接。某时刻,三个连接同时发送了一个请求,需要给名为“bugall”的用户增加10000的钱。这时,你不希望其他连接修改这个值。通常,我们会对“bugall”用户的数据加一个排他锁,这样所有修改“bugall”对应数据的请求都会被阻塞。序列化,当修改“bugall”时,其他修改请求会被屏蔽。这个阻塞过程是如何实现的?或者这个同步机制是如何实现的?我们往下看。三:内存模型内存模型决定了CPU如何访问内存,并发情况下CPU之间的影响。但是内存模型不包括虚拟地址转换,因为CPU访问的内存的物理地址是内存模型主要关注的是CPU和内存之间数据和物理地址的传输。不同硬件之间内存模型的差异在于硬件执行加载和存储指令的顺序。更改执行顺序可能会提高性能。另外,内存模型还规定了多个处理器访问同一个内存地址的行为,最简单的内存模型是顺序内存模型(sequentialmemorymodel),也称为强排序,在这种模型下,所有的load和store指令都被执行根据程序运行的顺序。load%r1,A//将内存地址A的值放入寄存器r1load%r2,B//将内存地址B的值放入寄存器r2add%r3,%r1,%r2//将寄存器r1和r2相加valueandputitintotheregisterr3store%r3,c//将寄存器r3中的值写入内存地址c。在数列内存模型下,执行的顺序是按照程序运行的先后顺序进行的。如果内存地址A中的值还没有取到,则不能进行从内存地址B取值的操作。顺序内存模型除了要求内存操作的顺序与程序运行的顺序一致外,还要求CPU或I/O设备的读写操作是原子的,即一旦启动,这些操作不能被其他内存操作打断。四:临界区和互斥虽然顺序内存模型的执行顺序是根据程序的运行顺序来的,但是多个CPU对同一个内存地址的访问顺序确实是不确定的,正是由于导致竞争条件发生的较少访问。为了说明这个问题,假设有一个全局计数器counter,CPU每运行一次该值就加1,要求该计数器非常准确的显示当前CPU运行的次数。load%r1,counter//将计数器counter的值读到寄存器r1中add%r1,1//将寄存器r1中的值加1store%r1,counter//存入计数器counter中接下来,有是两个CPU序列累加操作的执行顺序是这样的,两个CPU的执行时间是交错的,没有出现racecondition,所以得到的值符合前面的预期。然而,还有另一种可能。可以发现,如果两个CPU同时进行加载操作,最终会产生错误的结果。因为每个CPU在自增前读取的数据都是0,所以不管后面的运算顺序是什么,结果永远是1,正确的值应该是2。更新两个或多个CPU之间的共享数据结构指令序列将产生竞争条件。指令序列本身称为临界区,要操作的数据称为临界资源。例如,上面代码中的三个指令序列就可以看作是一个临界区。为了消除多个CPU并发访问临界区造成的竞争条件,需要限制只有一个CPU同时执行临界区,这就是互斥。(互斥排除)。五:原子操作为了保证同一时间只允许一个CPU执行临界区,目前的硬件都提供了基于原子的read-modify-write操作。read-modify-write操作允许CPU读取一个值,修改该值,并将修改后的值写回内存作为一个原子总线操作,它是CPU中的一条特殊指令,仅在需要同步时才使用。对于具体的修改操作,每个实现标准可能不同,但一般来说,目前的CPU都支持test-and-set(TAS)指令,即从内存中读取一个字节或一个字(4字节),然后与0进行比较,并无条件地将其在内存中的值设置为1,所有这些操作都是原子操作。一旦CPU执行测试和设置操作,其他CPU和I/O设备就不能使用总线。通过test-and-set指令,操作系统或数据库系统可以构造更高层次的同步操作,如自旋锁(spinlock)、信号量(semaphore)。六:自旋锁在TAS的基础上可以实现很多互斥数据结构,而自旋锁是应用最广也是最简单的互斥结构。自旋锁用来相互排斥短期临界区的数据结构。需要注意的是,自旋锁互斥使用的临界区代码应该比较少,即代码一般可以快速执行,释放自旋。锁,因为自旋锁会导致其他需要获取锁的线程进入忙等待,占用CPU。为了在多CPU环境下使用test_and_set指令实现进程互斥,还需要硬件提供进一步的支持,以保证test_and_set指令执行的原子性。这种支持目前以“总线锁定”的形式提供。由于test_and_set指令对内存的两次操作都需要经过总线,所以在执行test_and_set指令前锁定总线,执行test_and_set指令后锁定总线。可以保证test_and_set指令执行的原子性。七:自旋锁实现最基本的TAS指令是使用swap-atomic操作。该操作只是将寄存器中的值与内存中的值进行交换。Swap-atomic可用于构建测试和设置操作。首先将寄存器中的值设置为1,然后执行原子交换,***和register中的值进行比较。inttest_and_set(volatileint*addr){intold_value;old_value=swap_atomic(addr,1);if(old_value==0){return0;}return1;}变量addr的类型是init,表示要操作的单位是字.volatile修饰符告诉编译器从内存中读取addr的值,因为即使这个操作不修改addr的值,其他CPU也可能修改该值,所以在这种情况下,可能会导致test_and_set执行出错结果。swap_atomic函数执行swap-atomic硬件指令,返回交换前内存中addr的值。测试和设置操作是将两个独立的操作组合成一条指令。第一阶段是将addr中的值设置为1,第二阶段比较之前得到的结果。初始化时,将其值设置为0。typedefinitlock_tvoidinitlock(volatilelock_t*lock_status){*lock_status=0;}使用前面的TAS方法锁定一个自旋锁对象voidlock(volatilelock_t*lock_statue){while(test_and_set(lock_status)==1);}当lock_status的值为0时,test_and_set返回的结果为0,加锁成功。如果对象已经被使用过,那么就需要在while中循环(自旋),直到对象释放锁。这也是自旋锁名称的由来。