天天看点

Restful API实现乐观锁,应返回409还是412?

近些年Restful API变得很流行,一个重要的原因是其充分利用HTTP协议标准,这样API Consumer消费Restful API的成本就小很多,API开发人员也更加有据可循。

Restful API复用HTTP协议的方法和状态码来指代不同的行为,比如POST代表创建一条资源,创建成功用201表示,请求校验失败用400表示;GET代表获取一条或多条资源,获取成功用200表示;DELETE代表删除一条资源,删除成功用204表示,删除记录不存在用404表示。

个人觉得,Restful API相比传统RPC(比如:SOAP)的区别,就可类比于面向对象编程和面向过程编程。

这里讨论一个问题,对于Restful API的PUT操作,在并发环境下,两个Request更新同一条资源,可能会出现更新内容丢失的情况。针对这个问题,通常可以“加锁”来解决,那么加什么锁呢?一般来说就是:乐观锁或者悲观锁。

假设PUT请求的处理逻辑是先校验资源存不存在;然后存在的话更新资源到数据库。这个逻辑对应就是下面两条sql语句:

select * from product where productid = 110;
update product set stock = 49 where productid = 110;
           

如果是加悲观锁的话,就是在执行第一条select语句时加一个排他锁(select for update),在update语句执行完了才释放锁,这样两个PUT请求只能一个先执行一个后执行,就不会出现更新内容丢失的情况。

使用悲观锁,由于在相应记录上加了排他锁,并且锁的范围相对较大,会对读操作产生一定影响;其次,如果索引建得不合适,容易导致锁住整个表,进而影响系统吞吐量。

悲观锁有很多应用场景,之前我写过一篇文章(liquibase和flyway中分布式锁实现的区别?)介绍liquibase和flyway,其中flyway就是利用悲观锁实现了分布式锁。

和悲观锁不同,使用乐观锁,其实并不是在相应的记录或者表上加锁,而是在update的时候加一个version的比对。

回到上面例子,select语句查询到对应product的version,然后把version添加到update语句比对条件,如下:

update product set stock = 49 where productid = 110 and version = 5;           

如果当前数据库中记录的version是5,则update语句执行成功,version增加;如果当前数据库中记录version不是5,则update语句执行失败,返回相应状态码提示用户请求执行失败。

可以看到,乐观锁并没有添加额外的锁,所以在某些情况下,性能会好过悲观锁;但是,在高并发频繁更新的情况下,可能会导致很多请求失败,对用户体验很不好,用户需要重试很多次。

上面提到update执行失败,返回相应状态码提示用户请求执行失败,那么对于Restful API,应该返回什么状态码呢?

根据HTTP规范,有两个状态码可以使用:409和412。

从409的规范可以看出,当某一个资源的state发生了变化,导致request不能完成,可以返回409,提示用户解决冲突,重新提交请求。

The request could not be completed due to a conflict with the current state of the resource. This code is only allowed in situations where it is expected that the user might be able to resolve the conflict and resubmit the request.

https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.10

从412的规范可以看出,412是前置条件不符合,并且前置条件须是位于HTTP的header。

References

  • https://stackoverflow.com/questions/3620203/http-status-412-precondition-failed-and-database-versioning?rq=1