商城系统中常见的逻辑陷阱和优化方案(1)

和金钱相关的系统,都很有挑战性,是因为在这里,一切都很严肃

                                   ----by Someone you don't know


 伴随着用户群积累,社区的壮大,还有来自投资人对变现渴望的压力,似乎最容易想到的变现途径就是“我们也卖点东西吧”,如果直接给淘宝链接,会显得逼格太低,购买别人的系统,钱不少花,最后为了适应自己的需求,也要做相当多的工作,所以,越来越多不同的App里有了商城。当然根据不同的业务需求,复杂度也大相径庭。

笔者还没有能力“大话电商系统”,只是把实际开发过程中遇到过的逻辑陷阱阐述一下,并逐级优化,给出一个我认为的比较稳妥的方案,电商系统,博大精深,我提出的问题都比较小,并且很可能属于新手的坑。也欢迎读文章读你提出更多问题,或者更优方案。


问题1.  一个小保证,确保订单不会被恶意修改

20160119200737_Ynyfc.thumb.224_0

看了文字,标题肯定觉得懵逼了。举个例子吧,例如用户A的订单已支付,用户B却可能将它变成申请退款。

怎么可能?
如果这个系统存在漏洞,并且B是一个愿意尝试的程序员!

之前Review过一些团队小伙伴的代码,简单来说,修改订单状态被描述成如下流程:

1. 登陆验证等
2. 通过POST接受到Client传过来的OrderID
3. 修改订单
那么问题出现了,如果用户B利用技术手段发送了A订单的ID,会怎么样?如果系统没有做充分的校验工作,那么对不起,一个登录用户可以尝试所有数字,把所有订单都搞乱。
当然这是一件很简单的例子,当然优化的方案有很多,最简单的方案应该就是先获取订单

Order order = orderService.getOrderByIdAnOwner(orderId, ownerId)

这里获取订单同时增加了订单所有者的约束,以防止恶意更改别人的订单。
如果不涉及到其他的关于订单操作,也可以简简单单在更新的时候,确保订单所有者

扫描二维码关注公众号,回复: 2629607 查看本文章
Order order = orderService.updateOrderByIdAnOwner(orderId, ownerId)

 

2. 扣库溢出问题(超卖问题)

之前有个朋友遇到过这个问题,他说他们销售的某些商品比较热销,导致很多人去哄抢,在停止哄抢的时候,却发现商品库存是负数。这应该是典型的超卖了吧。

如果没有过多的思考,扣库存的过程很容易写成如下这个样子:

Product product = findProductById(productId)
//库存足够
if(product.availableAmount > 0){
    //做一些订单组装等工作
}
deduceProductAvailableAmount(product.id, buyAmount)

如果用户量很小,这段代码应该没有问题,如果用户变多,同一时刻有2个Tread同时运行这段代码,那么情况就很糟糕了,因为这段代码并不是线程安全的。

最简单的优化可能是

public synchronized void createOrder(userId, productId, buyAmount){

    Product product = findProductById(productId)
    //库存足够
    if(product.availableAmount > 0){
        //做一些订单组装等工作
    }
    deduceProductAvailableAmount(product.id, buyAmount) 
}

这样的做法牺牲效率,并且更严重的是,如果服务器分布式部署,那么还是不能解决问题。两台服务器也会并发遇到同样的问题。

方案一,一种比较常规的解决方案是利用数据库的行级锁,这里我们可以用 

Select for update

语法获得数据条目,当然两个前提要保证:
1. Service方法是Transactional
2. 获取数据的方式是SelectByPrimaryKey,如果不是,会导致整个表被锁住。

@Transaction(readOnly=false)
public void createOrder(userId, productId, buyAmount){

    Product product = findOrderByPrimaryKeyForUpdate(productId)
    //库存足够
    if(product.availableAmount > 0){
        //做一些订单组装等工作
    }
    deduceProductAvailableAmount(product.id, buyAmount) 
}

方案2. 当然也可以在更新库存的时候,通过多重保证不会出现超卖
例如如下Sql

update product set available_amount = available_amount - #{buyAmount} where product_id = #{id} and available_amount - #{buyAmount} >= 0

然后通过调用返回值,来决定是否要回滚。

@Transaction(readOnly=false)
public void createOrder(userId, productId, buyAmount){

    Product product = findOrderById(productId)
    //库存足够
    if(product.availableAmount > 0){
        //做一些订单组装等工作
    }
    if(deduceProductAvailableAmount(product.id, buyAmount) == 0){
       //内存不足 rollback
    }
}

两种方案其实都是为数据条目加锁,只不过在默认隔离级别读操作不会给条目加“写互斥锁”,这里通过Select For Update,将数据锁住而已。 而第二种方法,默认情况 Update操作,会给数据加“写互斥锁”,所以会保证不会出现超卖的情况。


 

3. 不要信任客户端

这个原则,似乎不仅仅对商城系统,应该是所有系统都适用。
举个例子
E333F3F5-4C8A-433F-AD4D-9F840D362346

截取了一张淘宝的图,在WEB端,你会看到根据规则计算出来的每件购物车商品金额,以及订单总金额。因为涉及到很多交互,为了提供相应速度,很多计算流程可能在前端实现。然后提交,数据到服务器。

但是服务器要使用前端传过来的数据么,例如商品金额,订单总金额等等。
答案是:千万不要!
因为这些数据,都可以用技术手段修改,如果服务器依赖前端的计算结果,会导致数据不可靠。
比较可靠的做法是服务器端,根据用户选择的商品ID,规则从新计算商品金额和订单总金额。 这意味着,同样的逻辑可能要在不同前端和服务器端,都实现一次。当然如果不涉及很强的快速响应的操作,可以只依赖服务器实现。


 

4. 十分复杂的逻辑,可以通过服务器实现。

在真正开始之前,产品经理童鞋把订单的状态梳理了一遍,因为我们的业务有点特别,这个订单状态图,让人看后觉得十分压抑。而需求变更似乎也是未来可能发生的事情。

下面这张图是订单列表的几个项目,订单状态本身的变化可以归结为有限自动状态机,订单中的每项描述了当前状态和可以触发的动作,动作触发后会进入目标状态。

815ACC18-02E0-4464-BD5B-31B92372E543

订单的跳转是很复杂的,如果我们要实现安卓,IOS和WEB,这意味着,这些繁琐的根据订单状态绘制和可触发事件(按钮),根据不同的状态来跳转,等等繁琐的操作,要在每个客户端分别实现。

这意味着,不仅仅代码数量的提高,还有高昂的沟通成本,每端的开发人员都要充分理解产品设计,理顺逻辑工作才能进行,显然这会极大降低效率。

我们最后采用的方案是逻辑由服务器控制。
例如当前订单状态是未付款,那么服务器订单,添加两个可以执行的操作,“支付”和“取消”。并且规定了按钮的样式(因为我希望取消是灰色的不容易被执行,支付是高亮色,容易被执行)

大概的设计如下:

public class Order{

    private String orderId;
    private String orderStatus;
    private List<OrderAction> orderActions;

    private static class OrderAction{
        private String actionName;
        private String actionId;
        private Integer actionStyle; 
    }
}

这样做的一个很大的好处是,服务器端可以控制订单跳转,不用书写繁杂的易错逻辑,而客户端只要把动作和页面连接起来,就可以实现。

 


 

当然,产品还没有上线,还会遇到更多有趣的问题,如果有更多心得我也会及时总结,虽然可能比较皮毛,不过还是希望对相关业务开发人员所有帮助~

传送门:http://sunrising.me/?p=96

 

猜你喜欢

转载自blog.csdn.net/hopeztm/article/details/51704583