锁
锁是为了解决高并发产生的多线程对共享资源进行并发访问时,由于后端接口『来不及』处理线程请求的数据,导致最终出现数据不一致或并非预想的结果,比如常见的『抢购商品超卖』、『手机号重复注册』等等。
以这个方法为例展示抢购逻辑:
@Transactional(rollbackFor = Exception.class)
public void robWithZKLock(BookRobDto dto) throws Exception{
//创建ZooKeeper互斥锁组件实例,需要将CuratorFramework实例、精心构造的共享资源 作为构造参数
InterProcessMutex mutex=new InterProcessMutex(client,pathPrefix+dto.getBookNo()+dto.getUserId()+"-lock");
try {
//采用互斥锁组件尝试获取分布式锁-其中尝试的最大时间在这里设置为15s
if (mutex.acquire(15L, TimeUnit.SECONDS)){
//真正的核心处理逻辑
//根据书籍编号查询记录
BookStock stock=bookStockMapper.selectByBookNo(dto.getBookNo());
//统计每个用户每本书的抢购数量
int total=bookRobMapper.countByBookNoUserId(dto.getUserId(),dto.getBookNo());
//商品记录存在、库存充足,而且用户还没抢购过本书,则代表当前用户可以抢购
if (stock!=null && stock.getStock()>0 && total<=0){
log.info("---处理书籍抢购逻辑-加ZooKeeper分布式锁---,当前信息:{} ",dto);
//当前用户抢购到书籍,库存减一
int res=bookStockMapper.updateStock(dto.getBookNo());
//更新库存成功后,需要添加抢购记录
if (res>0){
//创建书籍抢购记录实体信息
BookRob entity=new BookRob();
//将提交的用户抢购请求实体信息中对应的字段取值
//复制到新创建的书籍抢购记录实体的相应字段中
entity.setUserId(dto.getUserId());
entity.setBookNo(dto.getBookNo());
//设置抢购时间
entity.setRobTime(new Date());
//插入用户注册信息
bookRobMapper.insertSelective(entity);
}
}else {
//如果不满足上述的任意一个if条件,则抛出异常
throw new Exception("该书籍库存不足!");
}
}else{
throw new RuntimeException("获取ZooKeeper分布式锁失败!");
}
}catch (Exception e){
throw e;
}finally {
//TODO:不管发生何种情况,在处理完核心业务逻辑之后,需要释放该分布式锁
mutex.release();
}
}
可以看到,mutex
的存在让同一时刻只能有一个线程进入逻辑,解决了超卖的问题。
要是单机的话可以使用 Java 语言层面的并发原语入 sync 解决,但是只能解决单机的问题,多机还是要靠分布式锁。所以,直接使用分布式锁应该是一步到位了。
分布式锁
你可以会问,用 synchronized
这样的 Java 锁不行么?在单机时代,可以,但是现在,不行。因为现在都是多实例服务,synchronized 这样的锁只能保证一个 JVM 上在同一时刻只能有一个线程进入。但是在分布式架构下,资源共享不再是传统的线程共享,而是跨 JVM 进程之间资源的共享了,只能使用 ZK 这样的分布式锁来解决。
事务
还是上面的代码,你会看到 @Transactional
这个 Spring 事务,这也是单机的事务,保证这个方法下的 ACID。
分布式事务
那分布式事务呢?你可以看到,这个方法下都是直接调用数据库,而不是调用别人的微服务,这就可以只使用单机事务。试想一下,如果你要调用存库服务、支付服务,而不是调用数据库,这时候就只能使用分布式事务了。
网友评论