悲观锁和乐观锁

悲观锁和乐观锁(转载)原文
发表于 2018-12-12 | 分类于 并发
在介绍悲观锁和乐观锁之前,我们先看一下一个案例,简单介绍一下超反现象。

超反现象案例
eg:购买产品
CREATE TABLE product (
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(60) DEFAULT NULL COMMENT ‘名称’,
stock int(10) DEFAULT NULL COMMENT ‘库存’,
price decimal(16,2) DEFAULT NULL COMMENT ‘单价’,
version int(10) DEFAULT NULL COMMENT ‘版本’,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
CREATE TABLE orders (
id int(11) NOT NULL AUTO_INCREMENT,
userId int(10) DEFAULT NULL COMMENT ‘用户编号’,
productId int(10) DEFAULT NULL COMMENT ‘产品编号’,
price decimal(16,2) DEFAULT NULL COMMENT ‘单价’,
number int(10) DEFAULT NULL COMMENT ‘数量’,
totalPrice decimal(16,2) DEFAULT NULL COMMENT ‘总价’,
order_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT ‘购买时间’,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=55 DEFAULT CHARSET=utf8;
核心代码

@Override
@Transactional
public boolean purchase(int userId, int productId, int num) {
// 无锁
ProductModel model = productService.getById(productId);
if (model == null){
// 无该产品
return false;
} else {
if (model.getStock() < num){
// 库存不足
return false;
}

        // 修改库存
        boolean des = productService.decreaseProduct(model.getId(), num);

        // 初始化购买记录
        if (des){
            OrderModel orderModel = new OrderModel();
            orderModel.setUserId(userId);
            orderModel.setNumber(num);
            orderModel.setPrice(model.getPrice());
            orderModel.setTotalPrice(model.getPrice().multiply(new BigDecimal(num)));
            orderModel.setProductId(productId);
            orderModel.setOrderTime(LocalDateTime.now());
            boolean bo = super.save(orderModel);
            if (bo){
                // 购买成功
                return true;
            }
        }
    }

    return false;
}

js购买:

$("#purchase").click(function(){
var params = {
userId: 1,
productId: 1,
num: 1
};
// 通过POST请求后端,这里的JavaScript会采用异步请求
$.post(“http://localhost:8080/product/order-model/purchase”, params, function (result) {
console.log(result.message)
});
})
测试结果:
test

然而很多时候,在抢购的时候,都会出现并发,这里我们用简单的JS来做模拟

$("#purchase").click(function(){
for (var i=1; i<=1000; i++) {
var params = {
userId: 1,
productId: 1,
num: 1
};
// 通过POST请求后端,这里的JavaScript会采用异步请求
$.post(“http://localhost:8080/product/order-model/purchase”, params, function (result) {
console.log(result.message)
});
}
})
测试之前,我们看一下库存
stock
测试结果
test result
这个时候的库存
test stock

SELECT MIN(order_time),MAX(order_time),COUNT(1) FROM orders WHERE productId = 1;
test result
可以看到产品表的库存库存从500变到了-1,插入了501条订单。500件商品,却卖出了501件,什么原因呢?这就是超反现象。我们来分析一下

时刻 线程1 线程2 备注
T1 读取库存为1 可购买
T2 读取库存为1 可购买
T3 扣减库存 库存为0
T4 扣减库存 库存为-1、
T4 读取库存为-1 库存不足,不可购买
为了解决高并发时的超发现象,企业级开发提出了悲观锁、乐观锁和Redis等多种方案。

悲观锁
具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

处理流程
1.在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)
2.如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常
3.如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了
4.其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常

实现方式
select…for update

依然是对上面的例子我们做一个修改

@Select(“select * from product where id = #{productId} for update”)
ProductModel selectModelById(int productId);
悲观锁结果
这个时候我们可以看到,数据是正常了,但是加锁比加锁时,性能降低了

乐观锁
大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

乐观锁结果
从数据结果我们可以看到,解决了超反问题,因为没有独占资源和阻塞任何线程的并发,所以乐观锁也称为非独占锁或无阻塞锁但是出现了大量购买失败的数据。接下来我们需要解决这个问题,在更新库存失败的时候,尝试重新请求,但是这样就增加了吞吐量。可以采用,在某一时间范围内失败了,就重复请求,或者重试指定次数。由于时间来重入,不确定性太大,建议使用次数限制

@Override
@Transactional
public boolean purchase(int userId, int productId, int num) {
for (int i=0; i < 3; i++){
// 无锁
ProductModel model = productService.getById(productId);
// 乐观锁
//ProductModel model = productService.selectModelById(productId);
if (model == null){
// 无该产品
return false;
} else {
if (model.getStock() < num){
// 库存不足
return false;
}

            // 修改库存
            //productService.decreaseProduct(model.getId(), num);
            boolean des = productService.decreaseProduct2(model.getId(), num, model.getVersion());

            // 初始化购买记录
            if (des){
                OrderModel orderModel = new OrderModel();
                orderModel.setUserId(userId);
                orderModel.setNumber(num);
                orderModel.setPrice(model.getPrice());
                orderModel.setTotalPrice(model.getPrice().multiply(new BigDecimal(num)));
                orderModel.setProductId(productId);
                orderModel.setOrderTime(LocalDateTime.now());
                boolean bo = super.save(orderModel);
                if (bo){
                    // 购买成功
                    return true;
                }
            }
        }

    }
    return false;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值