社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  DATABASE

MySQL 事务与锁(二):锁的应用 - MySQL 悲观锁乐观锁、基于 Redis 的分布式锁

金正皓 • 7 年前 • 885 次点击  

1.数据库锁的使用

1.1 锁的副作用

1.1.1 锁等待

#正在执行的事务
SELECT * from information_schema.INNODB_TRX;
#当前出现的锁等待
SELECT * from information_schema.INNODB_LOCK_WAITS;
#出现锁等待的锁的详细信息
SELECT * from information_schema.INNODB_LOCKS;
#查看全部线程,辅助定位客户端的主机ip,连接用户名等
show full processlist;
#如果活跃事务少,会显示当前活跃的事务详细信息,多的话只显示概要;最近一次死锁的信息
show engine innodb status;

1.1.2 死锁

死锁都是由加锁顺序不一致导致的,最常见的update死锁,insert也能造成死锁,有兴趣的可以自行了解

TransactionA TransactionB
start transaction; start transaction;
update t_test_01 set name=“name8” where status=8;#持有status=8的锁
update t_test_01 set name=“name9” where status=9;#持有status=9的锁
update t_test_01 set name=“name9” where status=9;#等待status=9的锁
update t_test_01 set name=“name8” where status=8;#等待status=8的锁
commit Commit
start TRANSACTION;
update t_test_01 set name="name8" where status=8;
select sleep(10);
update t_test_01 set name="name9" where status=9;
commit;
start transaction;
update t_test_01 set name="name9" where status=9;
update t_test_01 set name="name8" where status=8
commit;

1.1.3 减少死锁锁等待

1.小事务

事务加锁范围不宜过大,如果比较大,业务上能分割的尽量分割。

例如:订单定时完成的批量,查出一批需要完成的订单,每个订单单独的事务处理,而不是放到一个大的事务里。

2.统一加锁顺序

3.update对应的查询走索引

1.2 悲观锁

1.2.1 一个例子

/**
 * 订单退款
 * @param orderId
 */
public void refund(Long orderId) {
   //select * from order where id={orderId};
    Order order = orderMapper.get(orderId);
    if (order.getStatus() == "已付款") {
        //第三方退款
        thirdPartyRefund();
        order.setStatus("已退款");
        orderMapper.update(order);
    } else {
        throw new Exception();
    }
}

有什么问题?

如果是客服给客人退款,不小心点了两次,或者退款比较慢点完又点了一次,如果退款走的是转账……

最简单的解决方案:

/**
 * 订单退款
 * @param orderId
 */
@Transactional
public void refund(Long orderId) {
   //select * from order where id={orderId} for update;
    Order order = orderMapper.getAndLock(orderId);
    if (order.getStatus() == "已付款") {
        //第三方退款
        thirdPartyRefund();
        order.setStatus("已退款");
        orderMapper.update(order);
    } else {
        throw new Exception();
    }
}

即使同一个订单退款同时出发了两次,由于X锁,第二次请求会阻塞。

这就是悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。

1.2.2 使用场景

1.存在需要控制并发的场景。

2.加锁的对象并发量不大。例如:对一个订单来说,并发主要来自用户对这个订单的操作,量并不大。

3.加锁的范围不能太大。

两方面:

数据库层面:建议只对主键加锁,例如:如果我对order里的userId加锁,影响范围就比较大了。

业务层面:如果订单系统由用户表(user),对用户表里的主键加锁对业务的影响。

1.3 乐观锁

1.3.1例子

下单减库存

t_stock

条数 400W

列名 类型 说明
id bigint 产品id主键 primary key
amount int 库存数
version integer 版本
@Transactional
public int decAmount(long id) {
    boolean updateFail = true;

    int i = 0;
    for (; i<=5 ; i++) {
      	//select * from t_stock where id={id}
        StockEntity stock = stockMapper.get(id);
        stock.setAmount(stock.getAmount() - 1);
      //update t_stock set amount={stock.amount} where id={stock.id} and version = {stock.version};
        int affectCount = stockMapper.update(stock);
        if (affectCount > 0) {
            logger.info("i : {}", i);
            updateFail = false;
            break;
        }
    }
    if (updateFail) {
        logger.error("i: {}", i );
    }
    return i;
}

思想是CAS(Compare and Swap),JUC下面的Atomic包利用的就是CPU的CAS操作。

我就在不同隔离级别,不同的索引类型下做了试验:

Jmeter 1秒内1000并发,记录i值。i表示经过了几次CAS操作,0表示1次,1表示2次,以此类推,6表示超过定义的最大循环次数更新失败退出了。

注意:全局修改隔离级别后需要重启应用,否则连接池里的连接还是用的修改前的隔离级别,或者直接在连接参数里修改隔离级别。

1.3.2 RC级别下高并发结果

Jmeter 1S 1000个的并发

  1. id主键索引

{0=1, 1=999, 2=0, 3=0, 4=0, 5=0, 6=0}

  1. idx_id_version普通索引

{0=1, 1=999, 2=0, 3=0, 4=0, 5=0, 6=0}

  1. primary_id_version主键索引

{0=64, 1=61, 2=57, 3=63, 4=41, 5=47, 6=667}

  1. id唯一索引

{0=1, 1=999, 2=0, 3=0, 4=0, 5=0, 6=0}

  1. id普通非唯一索引

{0=31, 1=40, 2=47, 3=23, 4=28, 5=21, 6=810}

1.结果

期望的结果更新次数在1~6之间都有分布的,RC级别下只有id和version是主键索引,或者id是非唯一的普通索引的时候才符合预期。其他情况除了一条是1次更新成功,其他都是第二次更新成功。

2.分析

id主键索引/id version 联合索引 /id唯一索引的情况下,第一次循环里的update t_stock set amount={stock.amount} where id={stock.id} and version = {stock.version};,即使version不对也会对记录加锁,1000个请求过来,只有一个请求获取了锁并更新成功,其他锁加入等待队列;等到第一个请求更新成功,后面某个获取到锁的请求必然在第一个循环里更新失败,但并不会释放锁,第二次循环会更新成功。

primary_id_version是联合主键的时候update t_stock set amount={stock.amount} where id={stock.id} and version = {stock.version};锁主键的时候如果version不对主键并不存在,所以不会锁记录。

id是普通非唯一索引的时候update t_stock set amount={stock.amount} where id={stock.id} and version = {stock.version};会锁stock.id对应的记录,单发现不符合where条件里的version会立即释放锁,参考MySQL事务与锁-2.4.2 RC级别下update … where 加锁后释放锁里的场景。

1.3.3RR级别下高并发结果

  1. id主键索引

{0=11, 1=0, 2=0, 3=0, 4=0, 5=0, 6=989}

  1. id主键索引+idx_id_version普通索引

{0=13, 1=0, 2=0, 3=0, 4=0, 5=0, 6=987}

  1. primary_id_version主键索引

{0=304, 1=0, 2=0, 3=0, 4=0, 5=0, 6=696}

  1. id 唯一索引

{0=13, 1=0, 2=0, 3=0, 4=0, 5=0, 6=987}

1.结果

如果第一次没有更新成功,后面就不会更新成功

2.分析

RR级别下MVCC的一致读导致第一次循环如果没有更新成功,即使加了锁,第二次的快照读的结果和第一次还是一样,这样获取的version还是第一次的version,后面的更新都不会更新上。

1.3.4乐观锁的变体

@Transactional
public void decAmount(long id) {
  //update t_stock set amount=amount-1 where id={stock.id} and amount >= 1;
  int affectCount = stockMapper.decAmount(stock);
  if (affectCount > 0) {
    //扣减成功
  } else {
    //扣减失败
  }
}

利用amount替换version,没有仿CAS的操作,其实也不算乐观锁了。

1.3.5总结

MySQL底层用锁实现的,想实现无锁化的乐观锁并不现实,使用起来也有坑,并不推荐使用。

2 分布式锁

2.1 Redis实现的分布式锁

redis中有个命令setNX,是一种CAS操作,不同于一般的set命令直接覆盖原值,setNx在更新的时候会判断当前key是否存在,如果存在返回false,如果不存在设置value并返回true

下面的代码利用这个CAS操作写简单的乐观锁:

/**
 * 获取锁
 *
 * @param key 锁id
 * @return 锁结果
 */
public boolean tryLock(String key) {
        try {
        	#setNx
            if (redisTemplate.opsForValue().setIfAbsent(key, "")) {
               redisTemplate.expire(key, 5000, TimeUnit.MILLISECONDS);
               return true;
            }
          }
        } catch (Exception e) {
            LOGGER.error("get lock {} error", key, e);
        }
        return false;
    }

上面只是简单演示基本原理,实际使用中需要考虑很多问题。如redis失效和expire失败导致锁不释放(redis 2.8版本支持setnx命令支持设置失效时长;reids是单线程,也可以用eval执行lua脚本的方式实现)。

redis方案的分布式锁推荐RedissonLock,实现java.util.concurrent.Lock接口,用起来很方便;内部用redis的eval命令执行lua脚本,可以参看https://github.com/angryz/my-blog/issues/4。

2.2 使用分布式锁

扣减库存的例子




    
@Transactional
public void decAmount(long id) {
    Lock redissonLock = redissonClient.getLock(id);
    if(redissonLock.tryLock(1, TimeUnit.SECONDS)) {
        try {
            //select * from t_stock where id={id}
            StockEntity stock = stockMapper.get(id);
            stock.setAmount(stock.getAmount() - 1);
            //update t_stock set amount={stock.amount} where id={stock.id};
            stockMapper.update(stock);
        } catch (Exception e) {
            //todo
        } finally {
            redissonLock.unlock();
        }
    }
}

存在的问题:强依赖redis,如果redis挂了怎么办。

修改:

@Transactional
public void decAmount(long id) {
    Lock redissonLock = redissonClient.getLock(id);
    if(boolean lockSuccess = redissonLock.tryLock(1, TimeUnit.SECONDS)) {
        try {
            //select * from t_stock where id={id}
            StockEntity stock = stockMapper.get(id);
            stock.setAmount(stock.getAmount() - 1);
            //update t_stock set amount={stock.amount} where id={stock.id} and version = {stock.version};
            int size = stockMapper.updateByVersion(stock);
            boolean success = size > 0;
        } catch (Exception e) {
            //todo
        } finally {
            if (lockSuccess) {
                redissonLock.unlock();
            }
        }
    }
}

2.2.1 Redis锁+数据库锁双重保障的方式

2.2.2 相比只有数据库加锁的优点

1.redis锁生效的时候,数据库没有锁等待。

2.redis失效的时候

可以考虑服务降级:例如上面的乐观锁,去掉循环之后,更新一次如果失败就返回失败;

或者服务不降级:用数据库锁扛着。

2.2.3 相比只用redis加锁的优点

不强依赖redis

2.3 使用场景

2.3.1 只想在特定的操作加锁

例如:同一用户每次只允许下一单,如果用数据库锁,可能会锁住用户相关的所有操作;这时候用分布式锁没有问题,因为锁对象(redis里的key值)定义很自由。用户退款可以定义为:lock:user:refund:{userId},用户下单可以定义为:lock:user:order:{userid}

2.3.2 锁对象的并发量很大

高并发的时候如果使用数据库锁,会有很长的锁等待队列,数据库连接也被占;虽然锁等待超时会抛异常,放弃等待,等待时间也很难控制。

经典场景:秒杀,对单个产品对象的并发。

2.2.3 考虑好锁失效的场景和处理方案



今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/nXHBcNuRIh
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/11314
 
885 次点击