乐观锁:总是假设最好的情况,在读取数据的使用不会发生并发问题,但在更新的时候比较原数据是否被其他线程发生了改变。主要通过通过版本号机制或CAS算法实现,适用于读多写少的应用场景。
版本号机制:在数据库表中加一个版本号version字段,表示数据被修改的次数,在修改数据前先读取该表中的版本号字段,在修改的使用对比是否是自己读取出来的版本号如果是则进行更新操作并版本号(version)加1如果不是则重新执行进行更新操作直到更新成功为止。
CAS:compare and swap 顾名思义就是比较与替换。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。
内存位置的值是否与预期原值一样如果一样则用新值更新,如果不则重试。CAS算法实现会导致ABA问题的产生。
ABA问题:A线程某一时刻获取内存位置的值为10,与其原值也为10,在这个过程中假设B线程对内存位置的值进行了修改,修改我为12,下一毫米又对内存中的值进行了修改,修改为10。这是A线程把值修改为13修改成功。这就是ABA问题。通俗易懂的来讲就是:你大爷是你大爷,你大妈已经不是你大妈了。乐观锁通常使用版本号机制来避免ABA问题。
未使用乐观锁(实现存钱取钱)
A操作人员 | B操作人员 |
---|---|
查询余额(100¥) | 查询余额(100¥) |
存入10¥(100¥+10¥) | 喝咖啡中 |
喝咖啡中 | 取出10¥(100¥-10¥) |
查询余额 (90¥) | 喝咖啡中 |
查询余额 (90¥) | 查询余额 (90¥) |
以使用乐观锁(实现存钱取钱)
A操作人员 | B操作人员 |
---|---|
查询余额(100¥)version=1 | 查询余额(100¥)version=1 |
存入10¥(100¥+10¥)把查询余额中的version进行对比匹配version+1充值成功 | 喝咖啡中 |
喝咖啡中 | 取出10¥(100¥-10¥)把查询余额中的version进行对比,结果不匹配 ,取出不成功,重新进行取钱操作,查询余额(110¥)version=2,把查询余额中的version进行对比匹配version+1取出成功 (110¥-10¥) |
查询余额 (100¥) | 喝咖啡中 |
查询余额 (100¥) | 查询余额 (100¥) |
本章会设计到 springboot整合mybatisPlus整合使用,springboot整合swagger2整合使用,需要的可以看我其他的博文
package cloud.xingzhe.springbootmybatisplus;
import com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
@MapperScan("cloud.xingzhe.springbootmybatisplus.mapper")
public class SpringbootMybatisPlusApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootMybatisPlusApplication.class, args);
}
/**
*乐观锁插件
*/
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInterceptor();
}
}
package cloud.xingzhe.springbootmybatisplus.model;
import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.Version;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import javax.validation.Valid;
/**
* @author 行者
* @since 2020-05-08
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="Bank对象", description="")
public class Bank implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
@ApiModelProperty(value = "余额")
private Integer amount;
@ApiModelProperty(value = "版本号")
@Version
private Integer version;
@ApiModelProperty(value = "用户id")
private Integer userId;
}
特别说明:
支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
整数类型下 newVersion = oldVersion + 1
newVersion 会回写到 entity 中
仅支持 updateById(id) 与 update(entity, wrapper) 方法
在 update(entity, wrapper) 方法下, wrapper 不能复用!!!
CREATE TABLE bank
(
id
int(11) NOT NULL,
amount
int(10) NULL DEFAULT NULL COMMENT ‘余额’,
version
int(5) NULL DEFAULT NULL COMMENT ‘版本号’,
user_id
int(11) NULL DEFAULT NULL COMMENT ‘用户id’,
PRIMARY KEY (id
) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO bank
VALUES (1, 100, 0, 2);
package cloud.xingzhe.springbootmybatisplus.controller;
import cloud.xingzhe.springbootmybatisplus.model.Bank;
import cloud.xingzhe.springbootmybatisplus.service.IBankService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
/**
*
* 前端控制器
*
* @author 行者
* @since 2020-05-08
*/
@Api(tags = "银行管理")
@RestController
@RequestMapping("/sys/bank")
public class BankController {
@Autowired
private IBankService bankService;
@ApiOperation(value="存取测试乐观锁")
@RequestMapping(value = "/access",method = RequestMethod.GET)
@ResponseBody
public String access(String userId,Integer money) throws InterruptedException {
QueryWrapper<Bank> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("user_id", userId);
Bank bank = bankService.getOne(queryWrapper);
Integer amount= bank.getAmount();
bank.setAmount(amount+money);
Thread.sleep(5000);
boolean b = bankService.updateById(bank);
if (b){
return "更新成功";
}
return "更新失败,余额已被其他操作人员修改,请重试";
}
}
注意:开两个swagger2 页面进行测试 ,测试的money的参数最好不一样 (有些浏览器接口和参数完全一样会等待接口数据放回才再次请求)
数据库中的version字段会每次都会加一 可以观察看看
源码地址: https://github.com/xingzhewenzi/springboot-examples.git
用到的博文地址:
springboot整合mybatis-plus(1) mybatis魂斗罗兄弟p2 实现单表curd零sql
springboot整合mybatisPlus代码生成器 快速生成controller service mapper
springboog整合swagger2 实现便捷高效的接口文档