几个典型场景

场景1

电商系统通常会有秒杀这种业务场景,例如:一件商品在0点开启秒杀,库存只有5件,某程序员这样来实现:

  • 一个购买请求过来,先从数据库查询库存,如果库存大于等于1时则可以购买,库存减1,生成订单,如果库存小于等于0,则购买失败提示秒杀已经结束。

由于查询库存和库存减1这两步操作并不是原子操作,所以当大量请求涌入的时候,就会出现“超卖”的情况,A请求进来查询库存还有1件,于是执行扣库存操作,在查询和扣库存操作前,B请求也进来,查询库存也是1,于是B也执行扣库存操作,此时库存其实只有一件,如果并发的量更大,超卖的会更多。

场景2

活动领取奖励,一个用户只能领取一次,用户很快地点了两次,第一次点击领取领取了奖品,领取记录落入db,第二次几乎和第一次同时(别以为不可能,在实际中会有很多这种情况出现,无论是用户操作还是恶意为之,我们都必须避免)。程序员的实现逻辑是:查询db该用户是否已经领取奖励,如果未领取则可以领取,发放奖励,如果已经领取,则提示无法重复领取。看似无懈可击的逻辑在数据库是主从的架构下便变得很脆弱,就算插入数据成功,在很短的时间内也是有可能查询不到数据。这个问题在大多数主从架构下都是存在的,如主从的数据库,主从的redis。

几个解决方案

1、redis

利用redis 的incr(incrBy)的原子性,利用返回值判断是否操作成功。
库存可以预先存入redis,每次请求则减1,减到0之后便不能再购买,incrBy的原子性,单线程只有主库可写保证了只有一个线程在操作redis的值,不会出现超卖的情况。领取奖励也是同理,领取前对一个key进行incr,如果得到的是1则说明是第一次领取,如果不是1则说明已经领取过了。

2、mysql

秒杀问题可以先查询出库存,如果库存大于等于1的情况下,执行语句

1
update product_item set left_num = left_num - 1 where left_num = #left_num#

如果执行成功则表示扣除库存成功,反之则失败。

领取奖励可以建立一个lock表,该表只有一个name字段,且该字段建立一个唯一索引。
领取时像该表插入一个跟该用户绑定的name,如果插入成功则表示成功,如果插入失败,说明已经领取过奖励。

3、zookeeper

秒杀问题类似mysql的实现,zookeeper天生自带version,所以在修改数据的时候带上version,如果version不对则会更新失败
领取奖励可以使用创建节点的方式来实现,创建节点成功说明未领取过,再次创建相同节点则会报错,说明奖励已经领取。

几个抽象封装

针对奖励领取问题,我抽象出一个分布式锁,这种判断是否已经领取过可以理解为能否获取一个锁,如果获取到则可以继续操作,如果未获取到,则操作失败。
于是用java,分别针对redis,mysql,zookeeper实现了一个业务通用的分布式锁(顺便还实现了一个本地文件的版本,不过这个不能用在分布式场景下),地址在这里
redis客户端用jedis,mysql用的mybatis框架,zookeeper用的curator客户端。实现起来很简单,只是权当做练习罢了。