并发场景下数据写入功能的实现

网站建设4年前发布
74 0 0

在并发场景下,实现数据的正确写入,主要需理解“锁”相关的原理和技术。,并发时写数据,需要考虑要不要上锁,根本原因是,数据存在共享且数据会发生变化,即多线程会同时读写同一数据。 若数据不存在共享,即不同的线程读写不同的数据,不需要上锁; 若数据共享,所有线程对数据只读不写,也不需要上锁 若数据共享,有的线程读数据,有的线程写数据,则需要上锁。,当多个线程同时访问同一数据,并且至少有一个线程会写这个数据,这种情况被称之为“数据竞争”。,并发场景下,锁的作用,是并发改为串行,以保证数据的一致性(更具体而言,通过上锁,解决了并发执行程序时的原子性、可见性和有序性问题,有兴趣的同学可以近一步深入相关理论,本文以实战为主,不在展开)。,故,在并发场景下读写数据,首先要分析是否存在“数据竞争”问题,若存在,需要“上锁”。数据竞争如果发生在本地应用中,则用本地锁;如果发生在分布式服务间中,则使用分布式锁;本地锁和分布式锁在原理上相同,不用分别讨论。,锁相关技术,有“悲观锁”和“乐观锁”两种。,我们通常说的锁,如无特殊说明,就是指“悲观锁”。它是通过一些技术手段,实现线程或服务间的互斥和同步,其使用时,有显示(或隐式)的锁持有/释放操作。 由于上锁本身要损耗性能,上锁后并发处理变成串行,故上锁是比较影响系统性能的操作;且锁的应用不当,会潜在死锁/活锁风险;故悲观锁的使用要慎重。,乐观锁通常又叫“无锁技术”,它不是通过“上锁”把并发改串行的方式保证数据一致性,而是通过CAS(Compare And Swap)方式来实现,由于CAS通常很快,该过程也不用“上锁”,性能损耗少。不过,通过CAS并发写数据时,通常伴有“自旋”,即当出现多个写并发时,只有一个能写入成功,其他要自旋后再次写入,直至写入成功或因超时/超过重试次数失败。 自旋会带来性能开销,频繁自旋的性能开销会超过上锁。故乐观锁通常用在并发不太激烈的场景中,且在该场景下性能比悲观锁要好,而在高并发场景下,建议使用悲观锁。,本文主题是介绍并发写数据的几种方案,为此,我们先确立几个常用的业务场景,并做简单分析。,写数据,我们讨论最常见的把数据写入数据库的场景,主要包括新建数据和修改数据两种具体场景,两个场景不完全一致,分别讨论。,往数据库里写信数据时,如果数据直接相互独立,即不存在“数据竞争”,则按照1.1节的理论,此时不需要考虑锁的问题。,对于存在“数据竞争”的场景,我们考虑写入流水码的场景:假设创建数据有个编码字段,形如“CON_0001”,其后半段的“0001”是流水码,需要根据当前最大的流水码+1 来计算待创建数据的流水码。这里存在数据范围的竞争,并发创建数据时,如果不做并发控制,会创建多个编码相同的数据。,并发更新数据库记录时,如果可以确保各并发请求要更新的数据各不相同,则不存在“数据竞争”,不需要上锁;而并发更新同一数据记录时,如果不做并发控制,可能出现一个写请求覆盖另一个写请求的情况,导致最终结果错误。,这里我们考虑“访问量+1”的场景,即设计一个访问量表,每次请求给访问量+1,如当前访问量为5,若5个并发请求同时为该记录+1,正确的结果为10,但如果不加并发控制,结果通常会<10。,业务逻辑如下:,代码实现即:,该业务在并发场景下,主要存在原子性隐患,即 addEntity() 中的代码,需要作为一个整体全部执行完,若多个线程交替执行执行逐行代码,某个线程读取到最新流水码后,该码被其他线程改了,本线程不可知,导致写入错误数据。,故本业务场景的并发中,主要避免原子性和可见性问题,最直接的方式,是通过上锁解决。,在分布式系统中,lock可以用redisson或curator提供的分布式锁进行实例化。,锁除了可以在代码中用,也可以直接用到数据库上, select ... for update 语句即可在数据库中上写锁,另由于业务执行的原子性问题,需要把 addEntity() 中的逻辑放到同一个事务中去。,这里的事务隔离级别,选择RC或RR均可。,注,这个实现中,addEntity()中对 ConcurrentEntity 的查询,改成了加锁读的方式:,对于1000个并发请求,三种方法性能对比如下:,本业务场景中,由于流水码的计算,存在数据竞争问题,所以并发时需要上锁,如果能避免数据竞争,就可以避免并发问题。针对本案例,可以把流水码获取的逻辑放到redis中去,redis本身是单线程的,避免了流水码的数据竞争,进而避免了上锁的开销,而redis本身又是高性能的,故这个方案理论上比上述方案的性能只高不低。,并发更新数据库中的访问量时,存在的“数据竞争”问题,也是“原子性”隐患。如果更新本身是一个原子操作,则不存在并发问题;如果更新操作分两步,先读取当前数据,然后+1后重新写入,则该操作不是原子的,需要上锁。,使用乐观锁时,需要一个递增的版本字段( version ),每次update 成功时,version都要 +1,version要作为更新前的compare字段,若当前读到的version与数据库中的version不一致,则更新失败。,使用乐观锁时,compare不一致,会更新失败,这时需要自旋重试,故上述代码可以优化为:,发起10000个并发更新操作,结果如下:,可以看到:

© 版权声明

相关文章