【乐观锁】
相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。
乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。
一般是在数据表中加入一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version指会加一。当线程A要重新更新数据值时,在读取数据的时候也会读取version值,在提交更新时,若刚才读取到的version值与当前数据库中的version值相等才更新,否则重新更新操作,直到更新成功。
【悲观锁】
之所以叫做悲观锁,是因为这是一种对数据的修改抱有悲观态度的并发控制方式。我们一般认为数据被并发修改的概率比较大,所以需要在修改之前先加锁。
悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。
但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;另外,还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。
默认关闭mysql的autocommit自动提交机制
begin
然后通过select for update锁定记录,然后再进行update,
最后commit
示例
schema
DROP TABLE IF EXISTS `voucher_publish`;
CREATE TABLE `voucher_publish` (
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
`user_id` BIGINT NOT NULL COMMENT '哪个用户发布的代金券信息',
`merchant` VARCHAR(32) NOT NULL COMMENT '商家',
`voucher_amount` BIGINT NOT NULL COMMENT '商家发布的代金券总金额',
`version` BIGINT DEFAULT 0 COMMENT '版本号,乐观锁',
`create_time` DATETIME DEFAULT NOW() COMMENT '创建时间',
`update_time` DATETIME DEFAULT NOW() COMMENT '上次更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商家发布的代金券';
某用户发布了某个商家的代金券,总金额,然后很多人来抢代金券,代金券的金额就得逐个减少,多人并发时,必须保证同一时间只有一个人能抢购成功,否则就会造成数据错误。
springboot集成tk.mybatis
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class VoucherPublishMapperTest {
@Resource
private VoucherPublishMapper voucherPublishMapper;
@Resource(name = "smallPool")
private ThreadPoolTaskExecutor tpte;
/**
* merchant字段使用
*/
private static String SPECIAL = "CUBETEST";
private static int COUNTER = 1;
private void insert() {
log.info("插入 {} 条数据", COUNTER);
for (int i = 0; i < COUNTER; i++) {
VoucherPublish vp = VoucherPublish.builder().merchant(SPECIAL + i).userId(1L).voucherAmount(1000L).build();
int temp = voucherPublishMapper.insertSelective(vp);
assertEquals(temp, 1);
// 保存后,id有数据
}
}
@Test
public void test() {
log.info("test");
insert();
Example example = new Example(VoucherPublish.class);
Criteria c = example.createCriteria();
c.andLike("merchant", "%" + SPECIAL + "%");
List<VoucherPublish> list = voucherPublishMapper.selectByExample(example);
VoucherPublish vp = list.get(0);
/**
* 并发更新 UPDATE voucher_publish SET voucher_amount = ?,version =
* ?,update_time = ? WHERE id = ? AND version = ?
*/
for (int i = 0; i < 10; i++) {
tpte.execute(() -> {
update(vp);
});
}
}
private void update(VoucherPublish vp) {
int counter = 0;
while (counter < 5) {
vp.setVoucherAmount(vp.getVoucherAmount() - RandomUtil.randomInt(10, 500));
// 设置null的目的是让update语句不要更新这些字段
vp.setCreateTime(null);
vp.setMerchant(null);
vp.setUserId(null);
int res = voucherPublishMapper.updateByPrimaryKeySelective(vp);
counter++;
if (res > 0) {
// 更新成功
break;
}
Example example = new Example(VoucherPublish.class);
Criteria c = example.createCriteria();
c.andLike("merchant", "%" + SPECIAL + "%");
List<VoucherPublish> list = voucherPublishMapper.selectByExample(example);
vp = list.get(0);
}
if (counter >= 5) {
log.error("数据更新失败");
}
}
@After
public void clearData() {
Example example = new Example(VoucherPublish.class);
Criteria c = example.createCriteria();
c.andLike("merchant", "%" + SPECIAL + "%");
int temp = voucherPublishMapper.deleteByExample(example);
log.info("清空测试数据 {}", temp);
}
完整的测试代码,核心就是代金券金额在减少时的执行SQL语句
UPDATE voucher_publish SET voucher_amount = ?,version = ?,update_time = ? WHERE id = ? AND version = ?
where后面携带条件version=?,该version为上一条查询语句查出的version,该方法类似JDK中的CAS操作,旧值比较成功后才会执行UPDATE
VoucherPublish类定义
public class VoucherPublish implements Serializable {
/**
* @Fields serialVersionUID : TODO(用一句话描述这个变量表示什么)
*/
private static final long serialVersionUID = 6925203830751524347L;
/**
* 主键
*/
@Id
@KeySql(useGeneratedKeys = true)
private Long id;
/**
* 用户id
*/
private Long userId;
/**
* 商家
*/
private String merchant;
/**
* 商家剩余代金券金额
*/
private Long voucherAmount;
/**
* 版本号,乐观锁
*/
@Version
private Long version;
/**
* 记录创建时间
*/
private Date createTime;
/**
* 记录更新时间
*/
private Date updateTime;
}
version增加Version注解,来自于tk.mybatis
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Version {
/**
* 下一个版本号的算法,默认算法支持 Integer 和 Long,在原基础上 +1
*
* @return
*/
Class<? extends NextVersion> nextVersion() default DefaultNextVersion.class;
}
默认执行是DefaultNextVersion类的nextVersion方法,做加一操作。