![MySQL中选择+更新处理并发更新问题解决方案共享 MySQL中选择+更新处理并发更新问题解决方案共享](/rjstyle/noimg/105.webp)
假设MySQL数据库有一个成员表vip_member(InnoDB表),和结构如下:
当会员欲继续购买会员(只需1个月、3个月或6个月)时,必须符合以下条件:
如果end_at比现在的时间早,为当前时间设置为当前时间加上start_at,购买月数end_at。
如果end_at等于或晚于当前时间,通过end_at = end_at +买的月数集
继续购买后,active_status必须是1(即激活)
问题分析:
针对以上情况,我们通常检查记录的第一选择,然后记录end_at和更新start_at和end_at据记载。伪代码如下(UID为1001的会员1个月)。
复制代码代码如下所示:
vipmember =选择*从vip_member uid = 1001,极限1 uid 1001 #检查成员
如果vipmember.end_at <现在():
更新vip_member集start_at =现在(),end_at = date_add(现在的(),间隔1个月),active_status = 1,updated_at =现在()在uid = 1001
其他的:
更新vip_member集end_at = date_add(end_at,间隔1个月),active_status = 1,updated_at =现在()在uid = 1001
如果有两个线程同时执行上面的代码,那么很明显,存在一个数据覆盖问题,即一个要持续1个月,一个要持续2个月,但它可能只持续2个月,而不是增加到3个月。
解uff1a
首先,我想把选择和更新与SQL结合起来,如下所示:
复制代码代码如下所示:
更新vip_member
集
start_at =案例
当end_at <现在()
然后现在()
其他start_at
结束,
end_at =案例
当end_at <现在()
然后date_add(现在的(),区间#时间:整数#月)
其他date_add(end_at,间隔时间:#整数#月)
结束,
active_status = 1,
updated_at =现在()
在UID = # UID:bigint #
限制1;
很容易!
B,第二种方案:事务,即用事务包装上面的选择+更新操作。
那么包装上的交易是否一切都会没事的。
显然不是。因为如果两笔交易都选择同一vip_member记录在同一时间,然后同样会发生数据覆盖的问题。所以,我们能做什么来解决它你想设置事务隔离级别为可序列化的,考虑到现实的表现。
我们知道,InnoDB支持行锁。看MySQL官方文档(InnoDB锁定读)理解InnoDB可以添加两个锁时,读取行数据:读写互斥锁和共享锁。
读共享锁是通过以下SQL获得的:
复制代码代码如下所示:
选择*从父母那里的名字= 'jones'lock共享模式;
如果事务A获得第一读和共享锁,事务b仍然可以读取带有共享共享锁的行数据,但在等待事务提交或回滚之后,它可以使用读共享锁更新或删除行数据。
复制代码代码如下所示:
选择counter_field从child_codes更新;
更新child_codes集counter_field = counter_field + 1;
如果事务A首先获得一行的写共享锁,则事务b必须等待事务提交或回滚访问行数据。
很显然,要解决会员身份更新问题,不能添加读取共享锁,只能添加一个共享锁,前面的SQL重写如下:
复制代码代码如下所示:
vipmember =选择*从vip_member哪里UID = 1001限1更新1001 # UID检查成员
如果vipmember.end_at <现在():
更新vip_member集start_at =现在(),end_at = date_add(现在的(),间隔1个月),active_status = 1,updated_at =现在()在uid = 1001
其他的:
更新vip_member集end_at = date_add(end_at,间隔1个月),active_status = 1,updated_at =现在()在uid = 1001
此外,我想提醒您,更新/删除SQL应该尽可能地在条件下设置索引过滤条件,否则将锁定表,性能可以想象它有多糟糕。
c、第三种方案:乐观锁、类CAS机制
第二种锁定方案是悲观锁机制,并选择…对于更新不是很常见,它与CAS实现的乐观锁定机制相关联,所以我想到了第三种解决方案:乐观锁。
具体来说,它是相当简单的,首先,选择SQL不做任何修改,然后将选择出的vip_memer在更新SQL WHERE条件的end_at条件如下:
复制代码代码如下所示:
vipmember =选择*从vip_member uid = 1001,极限1 uid 1001 #检查成员
cur_end_at = vipmember.end_at
如果vipmember.end_at <现在():
更新vip_member集start_at =现在(),end_at = date_add(现在的(),间隔1个月),active_status = 1,updated_at =现在()的地方,active_status = 1。
其他的:
更新vip_member集end_at = date_add(end_at,间隔1个月),active_status = 1,updated_at =现在()在uid = 1001
这样,我们就可以根据更新的返回值来判断更新是否成功。如果返回值为0,则表示有并发更新。那么我们只需要重试它。
方案的比较:
对这三种方案的优缺点,可能有不同的意见,只是说我自己的意见。
第一种方案使用复杂的SQL来解决问题,不利于维护,因为SQL中的具体业务组合,在修改业务之后不仅需要读取此SQL,也可能会更改为更复杂的SQL。
第二种方案写一个独占锁,可以解决这个问题,但不常用。
第三个解决方案应该是一个平庸的解决方案,即使没有交易,我个人也推荐它。
此外,乐观锁和悲观锁的选择一般都是这样的(参照第二篇文章的结尾):
如果对阅读的响应非常高,比如证券交易系统,则使用乐观锁是合适的,因为悲观锁会阻塞读。
如果读得远多于写,它也适合使用乐观锁,因为悲观锁会导致少量的写阻塞大量的阅读。
如果写频繁,冲突的比例很高,则适合悲观的写锁。