商品、单品、SPU、SKU
SPU
SPU是(Standard Product Unit)标准化产品单元,区分品种,该集合描述了一个产品的特性。属性值、特性相同的商品就可以称为一个SPU。
例如,iphone4就是一个SPU,N97也是一个SPU,这个与商家无关,与颜色、款式、套餐也无关。
SKU
SKU是(stock keeping unit)库存量单位,区分单品,库存进出计量的单位, 可以是以件、盒、托盘等为单位。在服装、鞋类商品中使用最多最普遍。
例如纺织品中一个SKU通常表示:规格、颜色、款式。 SKU是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。比如一香烟是50条,一条里有十盒,一盒中有20支,这些单位就要根据不同的需要来设定SKU
商品特指与商家有关的商品,可对应多个SKU
sku通常在电商项目里代表库存。可以放在product表里,也可以拆出来放在库存表里,看你们自己的表结构设计了。
订单
订单状态 | 状态说明 |
---|---|
待付款 | 成功创建订单,但还未支付 |
已付款 | 订单成功支付 |
待发货 | 订单货物已完成配货,正在等待快递发出 |
已发货(待收货) | 订单货物已发出,可以根据物流号在本站或相关物流公司网站查询订单配送状态 |
已收货 | 订单商品已经到了下单时选择的收货地址,手动确认收货或者系统自动收货 |
已完成* | 已核对并签收订单商品30天(可配置) |
已取消 | 表示由您自己或委托客服取消的订单 |
已确认(货到付款) | 货到付款的订单需要本商城人员确认 |
前端(VUE)
实现1:
1 | <div id="recomTime" v-show="orderinfo.orderState=='1'">剩余支付时间 {{rocallTime}}</div> |
实现2:
1 | countDown() { |
订单超时未支付自动关闭实现(后端java)
思路:
1.采用定时轮询(数据库消耗大)
2.前端访问时再更新(用户体验差)
3.采用延时任务
1)java自带的DelayQueue
2)redis实现延时队列: zset的score实现,redis的过期属性实现
3)时间轮(netty,kafka等都有)
4)消息中间件(Rabbit MQ,Rocket MQ )
4.可以在数据里加一个过期字段(或者根据创建时间字段来判断是否过期),等查询订单时判断是否过期来是否进行更新操作,返回结果。
定时任务
实现思路比较简单。启动一个计划任务,每隔一定时间处理一次,这种处理方式只是适用比较小而简单的项目。
假设订单表的结构为:
t_order(oid, finish_time, stars, status, …)
然后,定时任务每隔一个 5 分钟(时间自己设定)等会这么做一次:
select oid from t_order where finish_time > 30分钟 and status=0;
update t_order set status=1 where oid in(超时订单id);
如果数据量很大,需要分页查询,分页 update,这将会是一个 for 循环。
但是,这种设计方案有一种明显的不足。
- 时效性差,会有一定的延迟,这个延迟时间最大就是每隔一定时间的大小,如果你设置每分钟定时轮询一次,那么理论上订单取消时间的最大误差就有一分钟,当然也可能更大,比如一分钟之内有大量数据,但是一分钟没处理完,那么下一分钟的就会顺延。
- 效率低。
- 对数据库的压力比较大。
但是,也有优势。
- 定时任务,实现起来简单。
- 也能很好的做分布式集群。
可以用quartz来实现,也可以用java的schedule实现
应该采用延时任务
定时任务有明确的触发时间,延时任务没有
定时任务有执行周期,而延时任务在某事件触发后一段时间内执行,没有执行周期
定时任务一般执行的是批处理操作是多个任务,而延时任务一般是单个任务
被动取消
这种实现方案和懒加载的思想一致,就是被动的取消订单。只有当用户或商户查询订单信息时,再判断该订单是否超时,如果超时再进行超时逻辑的处理。
但是这种方式依赖于用户的查询操作触发,这也就是说如果用户不进行查询订单的操作,该订单就永远不会被取消。不会取消的订单,也就可能意味着库存可能被占用。
所以,在实际实现上,可能是被动取消 + 定时任务的这种组合实现方式。这种情况下定时任务的时间可以设置的稍微“长“一点。
缺点:
- 会产生额外影响,比如统计,订单数,库存等产生影响。
- 影响用户体验,用户打开订单列表可能要处理大量数据,影响显示的实时性。
优点,同样是实现起来简单。
延时消息(可见redis部分的延时队列实现)
这种方式是目前比较普遍的实现方式。
延时消息的这种实现方式,包含两个重要的数据结构:
- 环形队列,例如可以创建一个包含 2400 个 slot 的环形队列(本质是个数组)。
- 任务集合,环上每一个 slot 是一个 Set。
本质上,就是一个时间轮算法的一个实现。
如果公司允许,可以在此基础上,扩展成一个分布式的,支持集群的延时队列。但是缺点是,难度较高,小公司根本没有这个机会来做。
Redis 缓存
利用 redis 的 zset。zset是一个有序集合,每一个元素(member)都关联了一个 score,通过 score 排序来取集合中的值。
我们将订单超时时间戳与订单号分别设置为 score 和 member。系统扫描第一个元素判断是否超时,具体如下图所示。
但是,这种实现方式,在高并发条件下,多消费者可能会取到同一个订单号。多个线程消费同一个资源的情况。
解决方案:1.加一个分布式锁来处理。但是,性能下降严重。2.采用对ZREM的返回值进行判断,只有大于0的时候,才消费数据
使用redis的Keyspace Notifications,中文翻译就是键空间机制,就是利用该机制可以在key失效之后,提供一个回调,实际上是redis会给客户端发送一个消息。是需要redis版本2.8以上。
注:Redis的发布/订阅目前是即发即弃(fire and forget)模式的,因此无法实现事件的可靠通知。也就是说,如果发布/订阅的客户端断链之后又重连,则在客户端断链期间的所有事件都丢失了。因此,方案二不是太推荐。当然,如果你对可靠性要求不高,可以使用。
优点:(1)由于使用Redis作为消息通道,消息都存储在Redis中。如果发送程序或者任务处理程序挂了,重启之后,还有重新处理数据的可能性。(2)做集群扩展相当方便 (3)时间准确度高
缺点:(1)需要额外进行redis维护
JDK的延迟队列
利用JDK自带的DelayQueue来实现,这是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素,放入DelayQueue中的对象,是必须实现Delayed接口的。
Poll():获取并移除队列的超时元素,没有则返回空
take():获取并移除队列的超时元素,如果没有则wait当前线程,直到有元素满足超时条件,返回结果。
优点:效率高,任务触发时间延迟低。
缺点:
(1)服务器重启后,数据全部消失,怕宕机
(2)集群扩展相当麻烦
(3)因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM(out of memory)内存溢出异常
(4)代码复杂度较高
时间轮算法
时间轮算法可以类比于时钟,如上图箭头(指针)按某一个方向按固定频率轮动,每一次跳动称为一个 tick。这样可以看出定时轮由个3个重要的属性参数,ticksPerWheel(一轮的tick数),tickDuration(一个tick的持续时间)以及 timeUnit(时间单位),例如当ticksPerWheel=60,tickDuration=1,timeUnit=秒,这就和现实中的始终的秒针走动完全类似了。
如果当前指针指在1上面,我有一个任务需要4秒以后执行,那么这个执行的线程回调或者消息将会被放在5上。那如果需要在20秒之后执行怎么办,由于这个环形结构槽数只到8,如果要20秒,指针需要多转2圈。位置是在2圈之后的5上面(20 % 8 + 1)
可以用Netty的HashedWheelTimer来实现
优点:效率高,任务触发时间延迟时间比delayQueue低,代码复杂度比delayQueue低。
缺点:
(1)服务器重启后,数据全部消失,怕宕机
(2)集群扩展相当麻烦
(3)因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常
消息队列
可以采用rabbitMQ的延时队列。
RabbitMQ具有以下两个特性,可以实现延迟队列:
1.RabbitMQ可以针对Queue和Message设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为dead letter
2.lRabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可选)两个参数,用来控制队列内出现了deadletter,则按照这两个参数重新路由。
结合以上两个特性,就可以模拟出延迟消息的功能
优点: 高效,可以利用rabbitmq的分布式特性轻易的进行横向扩展,消息支持持久化增加了可靠性。
缺点:本身的易用度要依赖于rabbitMq的运维.因为要引用rabbitMq,所以复杂度和成本变高
实例:
最后选择采用redis+delayqueue实现
springboot项目中引入redis可以参考这篇文章:https://zhuanlan.zhihu.com/p/164608337
具体流程:
(1)用户下单完成以后,把订单ID插入到DelayQueue中,同时插入到Redis中。
(2)30分钟之内,用户付款完成,则从DelayQueue中删除,从Redis中删除。
(3)超过30分钟,DelayQueue中的订单ID出队,查询数据库,改状态为取消,删除redis。
(4)如果30分钟之类,服务器重启,则服务器重新启动以后,从redis中读取待订单,重新插入到DelayQueue。
DelayQueue是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。这种队列是有序的,即队头对象的延迟到期时间最长。注意:不能将null元素放置到这种队列中。
Delayed,用来标记那些应该在给定延迟时间之后执行的对象。此接口的实现必须定义一个 compareTo 方法,该方法提供与此接口的 getDelay 方法一致的排序。
我们首先实现一个BaseDelayed,所有的DelayQueue中的元素必须要实现Delayed接口:
1 | public abstract class BaseDelayed<T> implements Delayed { |
然后定义一个超时出队的监听:
1 | public interface OnDelayedListener<T extends BaseDelayed> { |
然后定义一个DelayedService:
1 | //在DelayedService加载以后,会回调InitializingBean接口 |
DelayedService在系统启动完成以后,会回调InitializingBean接口的afterPropertiesSet(),这里我们启动从DelayQueue中读数据的线程,如果读到数据,需要把数据返回出去,所以就需要注册一些等待数据的监听,该什么时候注册呢?
我们本例中是需要一个OrderService的,可以在OrderService加载完成以后来注册监听:
1 |
|
同时,如果服务器重启过,OrderService加载完成以后,还要去redis中把那些订单查出来,插入到DelayQueue中。
DelayedOrder定义如下:
1 | public class DelayedOrder extends BaseDelayed<String>{ |
如果系统中还有别的这种任务,还可以继续继承BaseDelayed来进行添加。
controller就没什么好说的了:
1 |
|
测试一下:
浏览器执行:
http://localhost:8080/order/create?orderId=order1
http://localhost:8080/order/create?orderId=order2
http://localhost:8080/order/create?orderId=order3
http://localhost:8080/order/pay?orderId=order1
然后关闭服务器。
此时查看redis中的数据有order2和order3两条。
重启开启服务器:
redis中的order2和order3被重新插入到DelayQueue,超时以后,出队,订单置为自动取消,同时从redis中删除掉。
实际项目里引入:
1 | RedisConfig类重新设置redisTemplate |
InitializingBean接口是实现注入bean的初始化(在任意地方注入bean(比如需要调用service时,会自动装填bean),都会直接执行重写的afterPropertiesSet方法,不用主动调用)
1、Spring为bean提供了两种初始化bean的方式,实现InitializingBean接口,实现afterPropertiesSet方法,或者在配置文件中通过init-method指定,两种方式可以同时使用。
2、实现InitializingBean接口是直接调用afterPropertiesSet方法,比通过反射调用init-method指定的方法效率要高一点,但是init-method方式消除了对spring的依赖。
3、如果调用afterPropertiesSet方法时出错,则不调用init-method指定的方法。