一、整体架构
1.1项目架构图
1.2MySQL表格
- 用户信息表
- 用户密码表
- 商品信息表
- 商品库存表
- 订单信息表
- 秒杀信息表(Id;秒杀名称;开始时间;结束时间;秒杀价格;关联商品Id)
二、难点解析
1. 瞬时流量高并发
- 加入限流措施Sentinel,比如对短时间之内来自某一个用户,某一个IP、某个设备的重复请求做丢弃处理;或者某段时间的请求量超过阈值就限制流量的访问
2. 物品超卖
从Kafka消息中间件中拉取消息后,获取到秒杀商品的product_Id,之后执行事务代码,事务代码中对记录加行锁的语句以及该语句对应的底层SQL如下图所示,加完行锁后再去执行更新库存的逻辑代码,这样其它事务只能等待该事务完成之后才能继续进行操作
productBalanceDao.getLock(product_Id);
<select id="getLock" resultMap="BaseResultMap" parameterType="java.lang.Long">
<![CDATA[
select * from product where id=#{product_Id,jdbcType=BIGINT} for update;
]]>
</select>
3. 分布式环境下的ID生成
- UUID
- 雪花算法
- Redis的INCR生成分布式ID
- Zookeeper的有序节点生成分布式ID
4. Redis分布式锁遇到的一些难点
-
SetNx和Expire的非原子性操作
– String result = jedis.set(lockKey, requestId, “NX”, “PX”, expireTime); -
忘记释放锁
– finally -
释放了别人的锁
– 将lockKey的值设置为requestId(唯一),在finally中释放锁的时候进行比较当前锁的值是否是自己当时加锁时候的requestId,如果不是就不会释放锁,直接返回 -
大量失败请求
– 在规定的时间,比如500毫秒内,自旋不断尝试加锁(说白了,就是在死循环中,不断尝试加锁),如果成功则直接返回。如果失败,则休眠50毫秒,再发起新一轮的尝试。如果到了超时时间,还未加锁成功,则直接返回失败。 -
如何实现锁重入
– Redisson框架中实现了可重入锁 -
锁竞争/锁优化问题
– a. 读写锁
– RReadWriteLock readWriteLock = redisson.getReadWriteLock(“readWriteLock”);
– RLock rLock = readWriteLock.readLock();
– b. 锁分段 like Concurrentmap
– 将同一件物品的库存分为多个小库存,可以分段加锁,为了提升系统性能,我们可以将库存分段,比如:分为100段,这样每段就有20个商品可以参与秒杀。在秒杀的过程中,先把用户id获取hash值,然后除以100取模。模为1的用户访问第1段库存,模为2的用户访问第2段库存,模为3的用户访问第3段库存,后面以此类推,到最后模为100的用户访问第100段库存。以前是许多线程竞争一把锁,但现在是多个线程同时竞争100把锁
-
锁超时问题(任务执行时间 > 锁过期时间)
– a. 锁续命:设置一个后台线程定期查看锁有没有没释放,如果没有被释放的话就给锁续命(使用TimerTask)类实现。在实现自动续期功能时,还需要设置一个总的过期时间,如果业务代码到了这个总的过期时间,还没有执行完,就不再自动续期了。 -
如何保证缓存和数据库的一致性问题
– a. 最终一致性:先修改数据库+再删缓存+读写锁优化 ( 修改数据库+删除缓存(写锁) 和 查缓存(空)+读数据库加载到缓存(读锁)------>写和读不能并发执行 ) 、设置缓存的超时时间
– b. 实时一致性:直接操作DB(放弃缓存、适合读多写多的场景)、canal、分布式数据库
-
针对以上具体问题的解决办法点击这里
三、其它重点
- Redis作为缓存进行数据预热,如果Redis还有库存,预减Redis库存,并将下单请求加入消息队列
- Kafka作为消息队列缓存订单请求,它提供了一个异步通信机制,既可以提高并发量,又降低服务之间的耦合度