【mysql】springboot中使用 mybatis 实现乐观锁,支持并发更新,数据一致

【乐观锁】

相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。

乐观并发控制相信事务之间的数据竞争(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方法,做加一操作。

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页