企业级购物车实现思路

1 前言

        之前有幸在公司的项目中完成了购物车模块的从0到1以及之后的优化工作。而在这之前我在网上查询相关资料的时候,发现要不就是一些最简单的入门实现,要不就是实现得不够完善、没有考虑实际。所以在这里我想把我具体的实现分享出来,供大家参考(注:本文不会涉及到具体的代码细节,只是一个大概的实现思路)。


2 数据结构

        因为用户退出app后再打开,还是能看到自己对购物车最新的修改内容,所以实际上每一次用户对购物车的操作都是需要请求到后端进行落库的(在我这里的实现中,商品的选中状态是由前端自己保存的)。很自然而然地就会想到可以用数据库来实现购物车。但是因为购物车的请求量相对来说还是比较大的(相比于其他模块),数据库很可能会成为购物车的性能瓶颈,所以我们可以用Redis来进行存储。

        购物车是很典型的用Redis的hash结构来进行存储的应用场景:hset/hmset用来添加商品、hincrby用来增减商品数量、hlen用来查看商品数量、hdel用来删除商品、hgetall用来获取所有商品信息等等。而我在购物车中具体存储一个商品时会使用到两个hash:

key hashKey hashValue
用户id SKU id:number 购买数量
用户id SKU id:info 商品展示信息

        第一个hash用来存放用户id->SKU id->购买数量的对应关系,而第二个hash用来存放用户id->SKU id->商品信息的对应关系。其实只用到第一个hash就可以实现一个简易的购物车,第二个hash是用来做读取优化的,之后会进行解释。这里之所以没有将两个hash合并成一个是因为对于增减商品数量请求的话,可以直接对第一个hash进行hincrby就行了,如果合并成一个hash的话就不能直接调用hincrby操作了。

        因为Redis中的数据是存放在内存中的,Redis主要是用来做缓存用的,而不是数据库。所以如果购物车只使用Redis来进行存储,同时购物车数据量又很多,可能会造成内存紧张的情况出现。解决办法是可以对Redis中的购物车数据设置一个过期时间,等过期时间到达的时候,会自动删除该条数据。但是这样做的话又会造成数据丢失,所以我们还需要使用MySQL数据库来进行兜底。如果一次请求中发现Redis中的数据找不到、过期了的话,会重新尝试请求MySQL中的数据。如果MySQL中有数据的话会将该条数据重新放到Redis中,并最终返回。

        MySQL中购物车表的设计就很简单了,直接是id(表主键)、user_id(用户id)、hash_key(HashKey)、hash_value(HashValue)就可以了。因为我们对MySQL购物车表的用途就只是拿出来数据重新放到Redis中、和存放数据做备份用这两个功能,同时MySQL本身就支持各个字段的单独查询,所以直接设计成和Redis中hash结构一样的字段就可以了。


3 数据一致性问题

3.1 Redis和MySQL数据的一致性

        既然购物车的数据会同时存放在Redis和MySQL中,那么不可避免地就会出现数据一致性的问题。如何保证Redis中的数据和MySQL中是一样的呢?一种实现方案是商品数据在存放Redis中的同时,会异步刷新到MySQL中。但是对于请求量大的情况下这样做仍然会造成MySQL的压力。所以可以在MySQL之前再架一层MQ,用MQ来实现缓冲。这种方案是可以的,但是我当时并没有采用这种方案,而是另一种方案实现:对于前端过来的修改请求,我只会修改Redis中的数据。而通过定时任务的方式,每天会将Redis中的数据定时刷新到MySQL中。这样做的好处是可以把MySQL的请求集中放在一天中最空闲的时段(比如凌晨3点)执行。而且Redis中本身也有自己的持久化机制,可以近似地认为Redis中的数据不会丢失(实际上还是会有丢失的可能,但是购物车数据相比于其他的比如订单数据来说并不是那么重要,所以丢失了也没有太大关系。而且Redis也不是任何时候都会丢失数据。同时基于此(购物车数据不是特别重要)也可以不用考虑分布式事务(Redis和MySQL)的问题。在我看来,对于购物车模块来说性能会比数据正确性更重要。如果真的出现了问题的话,手动补偿就行了)。当然如果觉得这种方案不靠谱、会丢失数据的话,也可以转而使用第一种方案实现。

        如果使用第二种方案的话,那么这里需要对删除操作做些特殊说明。删除操作不同于增加和修改操作,如果前端发起了一次删除操作,是必须同时将Redis和MySQL中的相关数据进行删除的,而且必须是同步删除。可以想想如果这里只删除Redis中的数据而没有删除MySQL数据的话会造成什么:在之后的定时同步操作中,发现这条数据在Redis中没有,而在MySQL中有。那么我会认为这条数据仅仅是因为Redis中数据过期导致的,就不会进行删除。那么在之后请求这条数据时,又会重新将这条数据从MySQL中放到Redis中。我明明是删除了这条数据,结果反而是重新出现了。没有删除成功,从而导致bug的出现。至于说这里的删除操作如果不是同步的,而是异步的话,可能会造成上一次异步删除MySQL操作还没执行,下一次又对这个数据进行操作的情况出现。但是如果使用同步操作的话就需要关注这里是否是性能瓶颈点。

        对于Redis中购物车数据的过期时间设置也是有讲究的。如果定时任务是每天同步一次的话,那么过期时间必须要大于一天。也就是说过期时间的设置必须要大于定时刷新的频率。因为如果小于的话,那么可能还没等到Redis数据刷新到MySQL前,Redis中的数据就已经提前过期被删除了,这样就会造成数据丢失的情况出现。

        另外,过期时间尽量设置成一个随机值,避免Redis数据在同一时间过期导致将大量请求打到后面的MySQL中,给MySQL造成压力的情况出现。

3.2 购物车商品和商品主数据的一致性

        像订单中存放的商品信息,因为其保存的是下单这一刻的商品数据,所以只需要存商品下单这一刻的快照数据就可以了,之后商品数据怎么改都不会影响到订单商品。而购物车中的商品信息则不一样。比如说某一个商品加入到购物车时的价格是10元,后来该商品降价了,变成了8元。那么在购物车中是要能显示出最新的价格8元的,并且有“比加入购物车时便宜2元”类似的营销信息。再比如说某一个商品下架了,又或者是没有库存了,那么购物车中该商品的信息是要置灰的,并且不能下单。所以基于此,购物车中的商品数据是要能实时显示出最新的商品信息的。

        要做到这点,最简单的方式就是在每一次查询购物车的时候都去请求商品的相关接口,这样做在请求量大、购物车信息多的情况下会有性能问题,并且如果是跨服务调用的话,性能会更差。第二种方式是定时去刷新。这样就需要在Redis中自己保存商品的相关信息,也就是在上面第2小节中存放第二个hash的作用。这样做的话在每次查询购物车的时候就只需要查询Redis(如果Redis过期的话,查询MySQL)中的数据就行了,不需要再调用商品接口。但是这样做的问题是如果商品很久才会修改一次,那么定时任务仍然会定时去调用,浪费CPU资源。而且定时任务的频率到底应该设置成多少呢?设置小了会造成资源浪费,设置大了又会造成数据刷新不及时。所以在这里我最终使用了第三种方式,也就是通过消息监听修改数据的方式。在商品数据发生变动的时候,我们可以通过类似于canal这样的中间件,监听MySQL binlog日志变化,找到发生变动的那条数据。并进行全局的MQ消息广播。购物车这里接收到相关消息,找到属于购物车中的商品id,并同时修改Redis和MySQL中的数据就行了(我之前写过《关于关联商品数据与商品主数据之间的一致性问题的一些思考》,感兴趣的话可以查看)。

        还有一点需要说明的是:在监听到商品改动消息的时候,我拿到的是改动商品的id,那么此时我需要通过这些商品id找到相应的购物车信息,从而进行修改。如果是MySQL数据的话,直接通过商品id字段(hash_key)就能直接找到了,但是对于Redis中的数据却不能直接做到。只能通过用户id(也就是key)找到相应的购物车数据而不能通过商品id(也就是hashKey)找到。所以我们需要额外地维护一个商品id到用户id的关系,这样就可以通过变动商品的id找到相应的用户id,进而找到相应的购物车信息,从而修改Redis数据。但是这样做的话就需要在每次增删改购物车信息的时候同时维护这个关联关系。


4 总结

        最后总的实现流程如下所示:

企业级购物车实现思路_第1张图片


原创不易,未得准许,请勿转载,翻版必究

你可能感兴趣的:(工作点滴,购物车,Redis,数据一致性)