MySQL中SELECT+UPDATE处理并发更新问题解决方案

这篇文章主要介绍了MySQLSELECT+UPDATE处理并发更新问题解决方案分享,需要的朋友可以参考下。

问题背景

假设MySQL数据库有一张会员表vip_member(InnoDB表),结构如下:

当一个会员想续买会员(只能续买1个月、3个月或6个月)时,必须满足以下业务要求:

  • 如果end_at早于当前时间,则设置start_at为当前时间,end_at为当前时间加上续买的月数
  • 如果end_at等于或晚于当前时间,则设置end_at=end_at+续买的月数
  • 续买后active_status必须为1(即被激活)

问题分析

对于上面这种情况,我们一般会先SELECT查出这条记录,然后根据查出记录的end_at再UPDATE start_at和end_at,伪代码如下(为uid是1001的会员续1个月):

vipMember = SELECT * FROM vip_member WHERE uid=1001 LIMIT 1 # 查uid为1001的会员
 if vipMember.end_at < NOW():
     UPDATE vip_member SET start_at=NOW(), end_at=DATE_ADD(NOW(), INTERVAL 1 MONTH), active_status=1, updated_at=NOW() WHERE uid=1001
 else:
     UPDATE vip_member SET end_at=DATE_ADD(end_at, INTERVAL 1 MONTH), active_status=1, updated_at=NOW() WHERE uid=1001

假如同时有两个线程执行上面的代码,很显然存在“数据覆盖”问题(即一个是续1个月,一个续2个月,但最终可能只续了2个月,而不是加起来的3个月)。

解决方案

A、我想到的第一种方案是把SELECT和UPDATE合成一条SQL

如下:

UPDATE vip_member
SET
    start_at = CASE
        WHEN end_at < NOW()
            THEN NOW()
        ELSE start_at
        END,
    end_at = CASE
        WHEN end_at < NOW()
            THEN DATE_ADD(NOW(), INTERVAL #duration:INTEGER# MONTH)
        ELSE DATE_ADD(end_at, INTERVAL #duration:INTEGER# MONTH)
        END,
   active_status=1,
   updated_at=NOW()
WHERE uid=#uid:BIGINT#
LIMIT 1;

B、第二种方案:事务,即用一个事务来包裹上面的SELECT+UPDATE操作

那么是否包上事务就万事大吉了呢?

显然不是。因为如果同时有两个事务都分别SELECT到相同的vip_member记录,那么一样的会发生数据覆盖问题。那有什么办法可以解决呢?难道要设置事务隔离级别为SERIALIZABLE,考虑到性能不现实。

我们知道InnoDB支持行锁。查看MySQL官方文档(innodb locking reads)了解到InnoDB在读取行数据时可以加两种锁:读共享锁和写独占锁。

读共享锁是通过下面这样的SQL获得的:

SELECT * FROM parent WHERE NAME = ‘Jones‘ LOCK IN SHARE MODE;

如果事务A获得了先获得了读共享锁,那么事务B之后仍然可以读取加了读共享锁的行数据,但必须等事务A commit或者roll back之后才可以更新或者删除加了读共享锁的行数据。

SELECT counter_field FROM child_codes FOR UPDATE;
UPDATE child_codes SET counter_field = counter_field + 1;

如果事务A先获得了某行的写共享锁,那么事务B就必须等待事务A commit或者roll back之后才可以访问行数据。

显然要解决会员状态更新问题,不能加读共享锁,只能加写共享锁,即将前面的SQL改写成如下:

vipMember = SELECT * FROM vip_member WHERE uid=1001 LIMIT 1 FOR UPDATE # 查uid为1001的会员
if vipMember.end_at < NOW():
    UPDATE vip_member SET start_at=NOW(), end_at=DATE_ADD(NOW(), INTERVAL 1 MONTH), active_status=1, updated_at=NOW() WHERE uid=1001
else:
    UPDATE vip_member SET end_at=DATE_ADD(end_at, INTERVAL 1 MONTH), active_status=1, updated_at=NOW() WHERE uid=1001

另外这里特别提醒下:UPDATE/DELETE SQL尽量带上WHERE条件并在WHERE条件中设定索引过滤条件,否则会锁表,性能可想而知有多差了。

C、第三种方案:乐观锁,类CAS机制

第二种加锁方案是一种悲观锁机制。而且SELECT...FOR UPDATE方式也不太常用,联想到CAS实现的乐观锁机制,于是我想到了第三种解决方案:乐观锁。

具体来说也挺简单,首先SELECT SQL不作任何修改,然后在UPDATE SQL的WHERE条件中加上SELECT出来的vip_memer的end_at条件。如下:

vipMember = SELECT * FROM vip_member WHERE uid=1001 LIMIT 1 # 查uid为1001的会员
cur_end_at = vipMember.end_at
if vipMember.end_at < NOW():
    UPDATE vip_member SET start_at=NOW(), end_at=DATE_ADD(NOW(), INTERVAL 1 MONTH), active_status=1, updated_at=NOW() WHERE uid=1001 AND end_at=cur_end_at
else:
    UPDATE vip_member SET end_at=DATE_ADD(end_at, INTERVAL 1 MONTH), active_status=1, updated_at=NOW() WHERE uid=1001 AND end_at=cur_end_at

这样可以根据UPDATE返回值来判断是否更新成功,如果返回值是0则表明存在并发更新,那么只需要重试一下就好了。

方案比较

三种方案各自优劣也许众说纷纭,只说说我自己的看法:

  • 第一种方案利用一条比较复杂的SQL解决问题,不利于维护,因为把具体业务糅在SQL里了,以后修改业务时不但需要读懂这条SQL,还很有可能会修改成更复杂的SQL
  • 第二种方案写独占锁,可以解决问题,但不常用
  • 第三种方案应该是比较中庸的解决方案,并且甚至可以不加事务,也是我个人推荐的方案

此外,乐观锁和悲观锁的选择一般是这样的(参考了文末第二篇资料):

  • 如果对读的响应度要求非常高,比如证券交易系统,那么适合用乐观锁,因为悲观锁会阻塞读
  • 如果读远多于写,那么也适合用乐观锁,因为用悲观锁会导致大量读被少量的写阻塞
  • 如果写操作频繁并且冲突比例很高,那么适合用悲观写独占锁
时间: 2024-12-15 01:50:47

MySQL中SELECT+UPDATE处理并发更新问题解决方案的相关文章

MySQL中select * for update锁表的范围

MySQL中select * for update锁表的问题 由于InnoDB预设是Row-Level Lock,所以只有「明确」的指定主键,MySQL才会执行Row lock (只锁住被选取的资料例) ,否则MySQL将会执行Table Lock (将整个资料表单给锁住). 举个例子: 假设有个表单products ,里面有id跟name二个栏位,id是主键. 例1: (明确指定主键,并且有此笔资料,row lock) SELECT * FROM products WHERE id='3' F

mysql中timestamp字段,在更新这个字段其他数据时候,不更新timestamp的时间

mysql中timestamp字段,在更新这个字段其他数据时候,不更新timestamp的时间 执行这条SQL ALTER TABLE  XXXX(表名)  CHANGE TIME TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; 列如如下所示语句: ALTER TABLE  ai_manager.message_task CHANGE TIME TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP

在MySQL中阻止UPDATE语句没有添加WHERE条件的发生

如果在生产环境中使用UPDATE语句更新表数据,此时如果忘记携带本应该添加的WHERE条件,那么..Oh,no…后果可能不堪设想.那么有没有什么办法可以阻止这样的事情发生,又不使用任何的审核工具呢...办法当然是有的 sql_safe_updates sql_safe_updates这个MySQL自带的参数就可以完美的解决我们的问题,并且该参数是可以在线变更的哦~当该参数开启的情况下,你必须要在UPDATE语句后携带WHERE条件,否则就会报出ERROR.. 举个栗子 # sql_safe_up

Mysql中select into from用法

原文:http://www.111cn.net/database/mysql/60705.htm 在mysql中用select into from一直报错, 错误:#1327 - Undeclared variable: target_test select into from要求目标表target_table不存在,因为在插入时会自动创建 后来在网上看资料说Mysql不支持select into from语句,所以用其他方法来替代select into from语句: Create targe

数据库:Mysql中“select ... for update”排他锁分析

Mysql InnoDB 排他锁 用法: select … for update; 例如:select * from goods where id = 1 for update; 排他锁的申请前提:没有线程对该结果集中的任何行数据使用排他锁或共享锁,否则申请会阻塞. for update仅适用于InnoDB,且必须在事务块(BEGIN/COMMIT)中才能生效.在进行事务操作时,通过“for update”语句,MySQL会对查询结果集中每行数据都添加排他锁,其他线程对该记录的更新与删除操作都会

MySQL中select * for update锁表的问题(转)

由于InnoDB预设是Row-Level Lock,所以只有「明确」的指定主键,MySQL才会执行Row lock (只锁住被选取的资料例) ,否则MySQL将会执行Table Lock (将整个资料表单给锁住). 举个例子: 假设有个表单products ,里面有id跟name二个栏位,id是主键. 例1: (明确指定主键,并且有此笔资料,row lock) SELECT * FROM products WHERE id='3' FOR UPDATE; SELECT * FROM produc

MySQL中select * for update锁表的范围(转)

由于InnoDB预设是Row-Level Lock,所以只有「明确」的指定主键,MySQL才会执行Row lock (只锁住被选取的资料例) ,否则MySQL将会执行Table Lock (将整个资料表单给锁住). 举个例子: 假设有个表单products ,里面有id跟name二个栏位,id是主键. 例1: (明确指定主键,并且有此笔资料,row lock) SELECT * FROM products WHERE id='3' FOR UPDATE; SELECT * FROM produc

MySQL中的多表插入更新与MS-SQL的对比

MySQL多表插入: INSERT INTO tdb_goods_cates (cate_name) SELECT goods_cate FROM tdb_goods GROUP BY goods_cate; --INSERT INTO 表名 (列名)查询对应的列 MS SQL 多表插入: 与MySQL 的一样 MySQL的多表关联更新 UPDATE dtb_goods INNER JOIN tdb_goods_cates ON goods_cate = cate_name SET goods_

mysql中select into 和sql中的select into 对比

现在有张表为student,我想将这个表里面的数据复制到一个为dust的新表中去. answer 01: create table dust select * from student;//用于复制前未创建新表dust的情况下 answer 02: insert into dust select * from student;//已经创建了新表dust的情况下 现在使用select..into..语句实现以上东东. MySQL不支持Select Into语句直接备份表结构和数据,一些种方法可以代