mysql版本
- mysql版本采用的是 8.0.16,它与5.6版本相比,默认的存储引擎改为InnoDB引擎,而且它的增删查改性能相比原先的版本也有较大的提升。
数据库表
(密码需要跟用户的主表信息分开存取)
- 用户信息表:id、名字、性别、年龄、手机号等用户基本信息。
- 用户密码表:id、密码(以密文的方式存储在数据库里,不能以明文的方式)、use_id 作为外键关联到前面的用户信息表上。
单机版redis
- 因为单机版redis与cluster集群相比,仅仅存在水平扩展的容量问题,而由于这个项目是自己学习所用,数据并不多,所以不存在容量问题,于是采用的单机版redis了。
redis缓存商品详情信息
- 通过id得到商品详情信息。
- 如果redis中不存在这个商品详情信息,就将它存入redis中进行缓存(失效时间设置的是十分钟):RedisTemplate.opsForValue().set(“item_”+id , itemInfo); redisTemplate.expire(“item_”+id , 10 , TimeUnit.MINUTES);
(要注意itemInfo类需要实现 Serializable 接口)
分布式扩展的Jmeter压测情况
- 分布式扩展:采用1000个线程(循环20次)进行压测,扩展前平均耗时500毫秒左右,tps在1500左右,cpu占用率80%左右。扩展后,mysql服务器的cpu占用率降到了10%左右,解决了单机性能瓶颈的问题。(cpu占用率采用Linux的top命令查看)
- redis缓存商品详情信息:缓存优化后,平均耗时缩短到200ms左右,tps达到了2000。减轻了数据库服务器的压力,减少了对数据库磁盘的访问。
Redis缓存库存提高交易性能
- 缓存优化前,采用1000个线程(循环20次)进行压测,平均耗时有2000毫秒,也就是快2秒多了,tps在400左右,cpu占用10%左右,相比查询而言,交易过程做的一些数据库IO操作比较耗时,400的tps却对应2秒的耗时,显然是不妥当的。
- 缓存优化后,平均耗时降到了600毫秒左右,tps达到了1200多,优化方案产生了明显的效果。
- 交易性能瓶颈的原因:程序中当用户进行下单操作时,首先会有一个对于下单商品是否存在的校验,它会对数据库发送sql来获取活动商品的信息,还有一个对于用户是否合法的校验,还有一个对商品活动的校验,也就是校验活动中是否存在该商品以及校验活动是否正在进行中,这些校验完成后还要进行一个落单的减库存操作,这个操作在秒杀场景中将会是一个热点操作,具体的sql语句是 where item_id = #{item_id},这里有一个对数据库的行锁,落单减库存成功后就进行订单入库,生成订单流水号,并将流水号插入到数据库中,还要向数据库中插入商品的销量,总共加起来对数据库产生了六次IO操作。 交易性能瓶颈的总结:首先它对于交易的验证完全依赖于数据库,也就是完全通过sql形式发送给数据库来做读操作进行验证;还有一个库存行锁等待的问题,所有减库存的操作都是串行进行的。
- item_id上有一个唯一索引,加唯一索引的方式:alter table stock_table add unique index item_id_index(item_id)
优化交易性能:
- 将用户信息和活动商品的信息存到redis缓存中,在redis缓存中来对它们进行校验,至于活动的属性,可以将活动的开始时间和结束时间存到redis缓存中,但活动的数据是可以修改的,比如运营发现活动时间配置错误或者这个活动需要提前开始,然后运营修改了数据库中的活动属性,但redis缓存中的活动属性并没有正常过期,这就会导致用户仍然可以以活动商品的秒杀价格来进行交易,因此需要有一个紧急下线的功能,在后台提供一个给运营使用的将活动紧急下线的接口,在这个接口里我们可以手动的通过代码的方式清除掉redis缓存。
- redisTemplate.opsForValue().set(“item_” + id , itemInfo);
redisTemplate.expire(“item_” + id , 10 , TimeUnit.MINUTES);(十分钟)
项目交易的执行流程
- 开始交易,获取活动商品信息和用户信息。
- 校验商品是否存在,校验用户是否合法,校验活动中是否存在该商品,校验活动是否正在进行。
- 扣减库存,库存不足则结束交易。
- 扣减成功则进行订单入库,生成订单流水号。
- 最后数据库中插入商品的销量,交易完成。(itemService.increaseSales(itemId , amount))
库存扣减行锁的优化
- 优化前,是通过mysql的sql语句进行库存的扣减:set stock = stock - #{amount} where item_id = #{item_id} and stock >= #{amount}。这里在表中给 item_id 加了一个唯一索引,数据库会在 item_id = #{item_id} 这个地方加一个数据库的行锁,将 item_id 对应的商品串行化的减库存,这里会成为数据库性能的瓶颈。
- 库存行锁的优化方案是,将扣减库存的操作做到redis中,也就是将库存同步到缓存中,下单交易的时候就减缓存中的库存。redis中key是活动商品id,value是对应的库存,减库存操作的实现方式是:redisTemplate.opsForValue().increment(活动商品id,amount*-1)。得到的库存扣减后的结果如果大于等于0,则更新库存成功,return true;如果小于0,就表示库存扣成负的了,就卖不了,所以 return false,表示库存扣减失败。在活动开始的时候,自动上架活动的商品,活动没有开始时,商品是下架状态。
- 接着采用异步消息队列的方式来扣减数据库中的库存,这样的话既能保证C端用户通过redis完成一次高效的购买体验,又能通过异步扣减的操作来保证redis和数据库中库存数据的最终一致性。(如果失败,就把redis中扣减的库存加回来)
- (其中,在数据库的减库存方法上加了一个Transactional注解,来保证减库存和创建订单的操作要么同时成功,要么同时失败)
事务消息
- 首先生产者在消息队列上开启一个异步发送事务型消息的操作。这样会构建了一个map结构,key是商品id,value是库存扣减的数量,然后通过这个map构建出对应的消息,并投递到消息队列中,此时这个消息是一种半消息的状态,也就是对消费端是不可见的;接着真正要做的是创建订单和redis扣减库存的操作,如果执行成功,刚才的半消息就可以被消费者消费了,执行失败就进行回滚。