最近解决了一个小规模并发下单问题,来跟你们分享一下。mysql
如今有这么一个业务场景,线上经过手机app下单买祈福灯,支付成功后,线下寺庙点亮。存在多个 用户同时选择同一个灯的状况出现,以下图。此时,正常状况应为一个用户下单成功,其他显示灯已被选。因为,支付和下单是单独分开的,只要focus on下单就ok了。sql
简而言之,就是一个并发现单的问题。typescript
咱们能够想到的正常下单的流程,应该是这样的:数据库
1. 选择祈福灯时,先查询灯是否可用。 2. 选择祈福灯,例如图中的“D0000065”。 3. 下单业务逻辑,再次查询灯是否可用。 if(灯可用){ 该祈福灯状态设为已购买 生成订单记录 相关日志记录... }
在没有并发问题发生时,上面的流程近乎完美(really?),但是,多人下单时,同时去数据库中查询灯的状态时,结果都是可用的,接下来,emmmm,你懂的。markdown
那么,判断灯是否可用再下单,这样的逻辑是存在问题的。解决并发下单的常规思路不外乎两种,一是加锁,二是利用队列。这里,我主要是经过对数据库加锁的方式来解决这个问题的。session
在此以前,须要了解一些关于锁的概念。在本科的数据库原理课上。咱们接触到两个概念——共享锁和排它锁,如今又须要两个新的概念——乐观锁和悲观锁。并发
乐观锁(Optimistic Lock),想法乐观,认为本身在操做数据库时不会发生冲突,取数据时不加锁,更新数据是加锁,进行判断。app
悲观锁(Pessimistic Lock),想法悲观,每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了不少这种锁机制,好比行锁,表锁等,读锁,写锁等,都是在作操做以前先上锁。ui
显然乐观锁的相应速度快,悲观锁的时间消耗等都比较大。对于咱们这个案例,使用这两种都是能够解决的。spa
这里主要以mysql innodb为例。innodb自己是支持行锁的。
要使用悲观锁,咱们必须关闭mysql数据库的自动提交属性,由于MySQL默认使用autocommit模式,也就是说,当你执行一个更新操做后,MySQL会马上将结果进行提交。
set autocommit=0;
共享锁(s锁)
SELECT … LOCK IN SHARE MODE
SELECT … LOCK IN SHARE MODE
在读取的行上设置一个共享锁,其余的session能够读这些行,但在你的事务提交以前不能够修改它们。若是这些行里有被其余的尚未提交的事务修改,你的查询会等到那个事务结束以后使用最新的值。
排它锁(x锁)
SELECT … FOR UPDATE
索引搜索遇到的记录,SELECT … FOR UPDATE 会锁住行及任何关联的索引条目,和你对那些行执行 update 语句相同。其余的事务会被阻塞在对这些行执行 update 操做,获取共享锁,或从某些事务隔离级别读取数据等操做。一致性读(Consistent Nonlocking Reads)会忽略在读取视图上的记录的任何锁。(旧版本的记录不能被锁定;它们经过应用撤销日志在记录的内存副本上时被重建。)
注:普通 select 语句默认不加锁,而CUD操做默认加排他锁。
乐观锁也就是在更新时进行查询,一般用一个version字段来实现。
UPDATE ... WHERE... # 基于version的实现 SELECT ..., verison FROM [table] WHERE id = #{id} UPDATE [table] SET..., version = version + 1 where id = #{id} AND version = #{version}
固然,在ORM中也有相应的实现方式。具体能够参考细谈Hibernate之悲观锁和乐观锁解决hibernate并发
在项目中,因为时间关系没有使用基于version方式的乐观锁,而是直接采用了update ... where
的方式。直接对当前的灯号进行查询,若是可用就马上更新灯的状态为不可用,至关于加共享锁。若是发生并发的状况,同时用update
语句,数据库也会自动加上X锁,所以最终只有一个用户能够下单成功。
下单流程:
public int saveOrder(){ // 执行update ... where boolean isAvaliable; isAvaliable = denginfoService.updateDengAnyway(); if (isAvaliable) { //下单的业务逻辑 } } public boolean updateDengAnyway(String ccode, List<String> dengid) { //判断灯是否可用 String hql = "update DenginfoEntity set ordertype =2 where ccode=? and dengid=? and (ordertype=0 or (ordertype=1 and ordertime<?))"; Query query;int flag; try { for (String deng : dengid) { query = getSession().createQuery(hql); //参数化赋值 flag = query.executeUpdate(); logger.info("update--- " + flag); if (flag == 0) return false; } } catch (HibernateException | NullPointerException e) { e.printStackTrace(); return false; } return true; }