乐观锁是一种用于解决并发冲突的机制,它基于假设在大多数情况下没有并发冲突的原则。与悲观锁不同,乐观锁不会对数据加锁,而是通过一定的方式来检测并处理并发冲突。
在实现乐观锁时,通常会使用版本号或时间戳作为标识。当多个线程同时访问同一个数据时,每个线程都会读取到数据的当前版本号或时间戳。在更新数据时,线程会比较当前的版本号或时间戳与自己读取到的版本号或时间戳是否一致。如果一致,则执行更新操作;如果不一致,则表示有其他线程已经修改了数据,当前线程的操作可能存在并发冲突,需要进行相应的处理(如重试、回滚等)。
乐观锁的实现有两种
- CAS
- 版本号控制
CAS
当多个线程尝试同时修改同一个内存位置时,先比较当前内存位置的值与期望值是否相等,如果相等,则将新值写入内存,否则不进行任何更改。在这个过程中,所有的操作都是原子的,即每次只能有一个线程执行这个操作。这样就避免了同时修改内存位置带来的竞争和冲突问题。
private AtomicInteger value=new AtomicInteger(0);
@Override
@Transactional(rollbackFor = Exception.class)
public String generateTradeOrder(String tradinId, String buyerId) throws InterruptedException {
// 获取发布详情
TradinPost tradinPost = tradinPostMapper.selectOne(new QueryWrapper<TradinPost>()
.eq("tradin_id", tradinId));
// 判断是否有人已经下单了
if (tradinPost.getStatus() != 0){
// 有人下单了
return null;
}
// 乐观锁基于cas更新
// 获取原子类值
int current=value.get();
int expect=current+1;
// 已经给人抢先下单了,慢了一步返回null
if (!value.compareAndSet(current,expect)){
return null;
}
//更新商品数量
int update = tradinPostMapper.update(null, new UpdateWrapper<TradinPost>()
.setSql("status=status+1")
.eq("tradin_id", tradinId));
if (update < 1){
throw new BusinessException("更新失败");
}
// 生成订单
String orderId= StringUtil.generateShortId();
// 。。。。。。。
// 生成订单的业务
// 。。。。。。。
return orderId;
}
版本号控制
当多个线程同时对同一数据进行更新时,每个线程在读取数据之后都会获取该数据的当前版本号。在执行更新操作时,线程会比较自己读取到的版本号与实际数据的当前版本号是否一致。如果一致,表示数据未被其他线程修改过,可以执行更新操作,并将版本号加一;如果不一致,表示数据已经被其他线程修改过,当前线程的操作可能存在并发冲突,需要进行相应的处理。
// 乐观锁基于版本号更新
// 更新版本号
int update = tradinPostMapper.update(null, new UpdateWrapper<TradinPost>()
.setSql("status=status+1")
.eq("tradin_id", tradinId)
.eq("status", tradinPost.getStatus()));
if (update < 1){
// 更新版本号失败,已经给人抢先下单了,慢了一步返回null
return null;
}
原理:当有多个线程同时执行update只有一个线程执行成功,只有版本相同的对应的数据才能更新成功,返回1
如果需要要重试的场景,可以使用自旋调用自身重试
// 乐观锁基于版本号更新
// 更新版本号
int update = tradinPostMapper.update(null, new UpdateWrapper<TradinPost>()
.setSql("status=status+1")
.eq("tradin_id", tradinId)
.eq("status", value.get()));
if (update < 1){
// 更新版本号失败,自旋重试
this.generateTradeOrder(tradinId,buyerId);
}