分布式电商项目——14.秒杀

秒杀系统设计

概要设计原理
前端:实现动静分离,将静态资源部署到第三方的服务器上实现加速例如 七牛云等CND加速。
后端:先生成要抢购商品的数量的令牌,封装List保存到redis中。之后谁抢到令牌了,就将商品的一些信息保存到MQ中之后异步的根据MQ去修改库存实现秒杀。

Java实现微服务秒杀抢购课程安排

1.秒杀抢购前端优化方案
2.秒杀抢购如何防止超卖问题
3.基于MQ和Redis实现秒杀抢购
4.秒杀抢购如何防止伪造

使用Nginx实现页面缓存(前端优化)

events {
  #的最大连接数(包含所有连接数)1024
  worker_connections  1024;  ## Default: 1024
}

http{

   # 代理缓存配置
   proxy_cache_path "./meite_cachedata"  levels=1:2 keys_zone=meitecache:256m inactive=1d max_size=1000g; 
   server {
     listen 80;
     location /{
        #使用缓存名称
        proxy_cache meitecache;
		#对以下状态码实现缓存
        proxy_cache_valid 200 206 304 301 302 1d;
		#缓存的key
        proxy_cache_key $request_uri;
        add_header X-Cache-Status $upstream_cache_status;
		#反向代理地址
        proxy_pass http://127.0.0.1:8080;
      }
  
   }
}

使用数据库乐观锁实现防止超卖问题

数据库表接口

CREATE TABLE `meite_order` (
  `seckill_id` bigint(20) NOT NULL COMMENT '秒杀商品id',
  `user_phone` bigint(20) NOT NULL COMMENT '用户手机号',
  `state` tinyint(4) NOT NULL DEFAULT '-1' COMMENT '状态标示:-1:无效 0:成功 1:已付款 2:已发货',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表';


CREATE TABLE `meite_seckill` (
  `seckill_id` bigint(20) NOT NULL COMMENT '商品库存id',
  `name` varchar(120) CHARACTER SET utf8 NOT NULL COMMENT '商品名称',
  `inventory` int(11) NOT NULL COMMENT '库存数量',
  `start_time` datetime NOT NULL COMMENT '秒杀开启时间',
  `end_time` datetime NOT NULL COMMENT '秒杀结束时间',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `version` bigint(20) NOT NULL DEFAULT '0',
  PRIMARY KEY (`seckill_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀库存表';

定义秒杀服务接口

/**
	 * 用户秒杀接口
	 * 
	 * @phone 手机号码<br>
	 * @seckillId 库存id
	 * @return
	 */
	@RequestMapping("/spike")
	public BaseResponse<JSONObject> spike(String phone, Long seckillId);

@RestController
@Slf4j
public class SpikeCommodityServiceImpl extends BaseApiService<JSONObject> implements SpikeCommodityService {
	@Autowired
	private SeckillMapper seckillMapper;

	@Autowired
	private OrderMapper orderMapper;

	@Autowired
	private RedisUtil redisUtil;

	@Override
	@Transactional
	public BaseResponse<JSONObject> spike(String phone, Long seckillId) {

		// 1.验证参数
		if (StringUtils.isEmpty(phone)) {
			return setResultError("手机号码不能为空!");
		}
		if (seckillId == null) {
			return setResultError("库存id不能为空!");
		}
		// >>>限制用户访问频率 比如10秒中只能访问一次
		Boolean resultNx = redisUtil.setNx(phone, seckillId + "", 10l);
		if (!resultNx) {
			return setResultError("该用户操作过于频繁,请稍后重试!");
		}
		// 2.根据库存id查询商品是否存在
		SeckillEntity seckillEntity = seckillMapper.findBySeckillId(seckillId);
		if (seckillEntity == null) {
			return setResultError("该商品信息不存在!");
		}
		// 3.对库存的数量实现减去1
		Long version = seckillEntity.getVersion();
		int inventoryDeduction = seckillMapper.optimisticVersionSeckill(seckillId, version);
		if (!toDaoResult(inventoryDeduction)) {
			log.info(">>>>phone:{},seckillId:{},秒杀失败!", phone, seckillId);
			return setResultError("系统错误!");
		}
		log.info(">>>>phone:{},seckillId:{},扣库存成功!", phone, seckillId);
		// 4.添加秒杀成功订单
		OrderEntity orderEntity = new OrderEntity();
		orderEntity.setSeckillId(seckillId);
		orderEntity.setUserPhone(phone);
		int insertOrder = orderMapper.insertOrder(orderEntity);
		if (!toDaoResult(insertOrder)) {
			return setResultError("系统错误!");
		}
		log.info(">>>>phone:{},seckillId:{},秒杀成功", phone, seckillId);

		return setResultSuccess("恭喜你,秒杀成功!");
	}

}

数据库访问层

修改库存
public interface SeckillMapper {

	/**
	 * 使用乐观锁修改库存信息 and inventory>0方式
	 * 
	 * @param seckillId
	 * @return
	 */
	@Update("update meite_seckill set inventory=inventory-1 where  seckill_id='10001' and inventory>0")
	int optimisticLockSeckill(Long seckillId);

	/**
	 * 基于版本号形式实现乐观锁
	 * 
	 * @param seckillId
	 * @return
	 */
	@Update("update meite_seckill set inventory=inventory-1 ,version=version+1 where  seckill_id=#{seckillId} and version=#{version} and inventory>0;")
	int optimisticVersionSeckill(@Param("seckillId") Long seckillId, @Param("version") Long version);

	@Update("update meite_seckill set inventory=inventory-1 where  seckill_id='10001';")
	int inventoryDeduction(Long seckillId);

	@Select("SELECT seckill_id AS seckillId,name as name,inventory as inventory,start_time as startTime,end_time as endTime,create_time as createTime,version as version from meite_seckill where seckill_id=#{seckillId}")
	SeckillEntity findBySeckillId(Long seckillId);

}

秒杀成功记录

public interface OrderMapper {

	@Insert("INSERT INTO `meite_order` VALUES (#{seckillId},#{userPhone}, '1', now());")
	int insertOrder(OrderEntity orderEntity);
}

秒杀服务基于库存令牌桶实现修改商品库存

生产者
MQ相关配置

@Component
public class RabbitmqConfig {

	// 添加修改库存队列
	public static final String MODIFY_INVENTORY_QUEUE = "modify_inventory_queue";
	// 交换机名称
	private static final String MODIFY_EXCHANGE_NAME = "modify_exchange_name";

	// 1.添加交换机队列
	@Bean
	public Queue directModifyInventoryQueue() {
		return new Queue(MODIFY_INVENTORY_QUEUE);
	}

	// 2.定义交换机
	@Bean
	DirectExchange directModifyExchange() {
		return new DirectExchange(MODIFY_EXCHANGE_NAME);
	}

	// 3.修改库存队列绑定交换机
	@Bean
	Binding bindingExchangeintegralDicQueue() {
		return BindingBuilder.bind(directModifyInventoryQueue()).to(directModifyExchange()).with("modifyRoutingKey");
	}

}

生产者发送消息

@Component
@Slf4j
public class SpikeCommodityProducer implements RabbitTemplate.ConfirmCallback {

	@Autowired
	private RabbitTemplate rabbitTemplate;

	@Transactional
	public void send(JSONObject jsonObject) {

		String jsonString = jsonObject.toJSONString();
		System.out.println("jsonString:" + jsonString);
		String messAgeId = UUID.randomUUID().toString().replace("-", "");
		// 封装消息
		Message message = MessageBuilder.withBody(jsonString.getBytes())
				.setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8").setMessageId(messAgeId)
				.build();
		// 构建回调返回的数据(消息id)
		this.rabbitTemplate.setMandatory(true);
		this.rabbitTemplate.setConfirmCallback(this);
		CorrelationData correlationData = new CorrelationData(jsonString);
		rabbitTemplate.convertAndSend("modify_exchange_name", "modifyRoutingKey", message, correlationData);

	}

	// 生产消息确认机制 生产者往服务器端发送消息的时候,采用应答机制
	@Override
	public void confirm(CorrelationData correlationData, boolean ack, String cause) {
		String jsonString = correlationData.getId();
		System.out.println("消息id:" + correlationData.getId());
		if (ack) {
			log.info(">>>使用MQ消息确认机制确保消息一定要投递到MQ中成功");
			return;
		}
		JSONObject jsonObject = JSONObject.parseObject(jsonString);
		// 生产者消息投递失败的话,采用递归重试机制
		send(jsonObject);
		log.info(">>>使用MQ消息确认机制投递到MQ中失败");
	}
}

消费者

@Component
@Slf4j
public class StockConsumer {
	@Autowired
	private SeckillMapper seckillMapper;
	@Autowired
	private OrderMapper orderMapper;

	@RabbitListener(queues = "modify_inventory_queue")
	@Transactional
	public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws IOException {
		String messageId = message.getMessageProperties().getMessageId();
		String msg = new String(message.getBody(), "UTF-8");
		log.info(">>>messageId:{},msg:{}", messageId, msg);
		JSONObject jsonObject = JSONObject.parseObject(msg);
		// 1.获取秒杀id
		Long seckillId = jsonObject.getLong("seckillId");
		SeckillEntity seckillEntity = seckillMapper.findBySeckillId(seckillId);
		if (seckillEntity == null) {
			log.warn("seckillId:{},商品信息不存在!", seckillId);
			return;
		}
		Long version = seckillEntity.getVersion();
		int inventoryDeduction = seckillMapper.inventoryDeduction(seckillId, version);
		if (!toDaoResult(inventoryDeduction)) {
			log.info(">>>seckillId:{}修改库存失败>>>>inventoryDeduction返回为{} 秒杀失败!", seckillId, inventoryDeduction);
			return;
		}
		// 2.添加秒杀订单
		OrderEntity orderEntity = new OrderEntity();
		String phone = jsonObject.getString("phone");
		orderEntity.setUserPhone(phone);
		orderEntity.setSeckillId(seckillId);
		orderEntity.setState(1l);
		int insertOrder = orderMapper.insertOrder(orderEntity);
		if (!toDaoResult(insertOrder)) {
			return;
		}
		log.info(">>>修改库存成功seckillId:{}>>>>inventoryDeduction返回为{} 秒杀成功", seckillId, inventoryDeduction);
	}

	// 调用数据库层判断
	public Boolean toDaoResult(int result) {
		return result > 0 ? true : false;
	}

}

新增对应商品令牌桶

/**
	 * 新增对应商品库存令牌桶
	 * 
	 * @seckillId 商品库存id
	 */
	@RequestMapping("/addSpikeToken")
	public BaseResponse<JSONObject> addSpikeToken(Long seckillId, Long tokenQuantity);


@Async
	private void createSeckillToken(Long seckillId, Long tokenQuantity) {
		generateToken.createListToken("seckill_", seckillId + "", tokenQuantity);
	}

根据手机号码和商品库存id查询秒杀记录

public interface OrderSeckillService {

	@RequestMapping("/getOrder")
	public BaseResponse<JSONObject> getOrder(String phone, Long seckillId);

}


@RestController
public class OrderSeckillServiceImpl extends BaseApiService<JSONObject> implements OrderSeckillService {
	@Autowired
	private OrderMapper orderMapper;

	@Override
	public BaseResponse<JSONObject> getOrder(String phone, Long seckillId) {
		if (StringUtils.isEmpty(phone)) {
			return setResultError("手机号码不能为空!");
		}
		if (seckillId == null) {
			return setResultError("商品库存id不能为空!");
		}
		OrderEntity orderEntity = orderMapper.findByOrder(phone, seckillId);
		if (orderEntity == null) {
			return setResultError("正在排队中.....");
		}
		return setResultSuccess("恭喜你秒杀成功!");
	}

}

yml配置

logging:
  level:
    org.springframework.web: INFO
    com.github.binarywang.demo.wx.mp: DEBUG
    me.chanjar.weixin: DEBUG
###服务注册到eureka地址
eureka:
  client:
    service-url:
           defaultZone: http://192.168.107.219:8080/eureka
server:
  port: 9800


  
spring:
  application:
    name: app-mayikt-spike
  redis:
    host: 47.106.241.205
    password: 123456
    port: 6379
    jedis:
      pool:
        max-idle: 100
        min-idle: 1
        max-active: 1000
        max-wait: -1
###数据库相关连接      
  datasource:
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://47.106.241.205:3306/meite_spike

  rabbitmq:
    ####连接地址
    host: 192.168.107.219
    ####端口号
    port: 5672
    ####账号
    username: guest
    ####密码
    password: guest
    ### 地址
    #virtual-host: /admin_host
    virtual-host: /

实现秒杀抢购中会遇到那些问题

前端:
1.突然增加网络访问的带宽
2.用户可能存在重复进行提交
后端:
1.秒杀抢购商品库存超卖的问题
2.

1.为什么秒杀服务需要单独独立以微服务形式部署?
目的:互不影响其他服务、docker部署快速实现扩容
2.当修改商品库存的请求增多,数据库访问压力增大,如何解决
实现分表分库、使用MQ异步实现修改库存
3.如何防止库存超卖的问题
使用数据库乐观锁(CAS无锁)
使用redis实现分布式锁
使用MQ异步形式实现修改库存(用户等待过程)

前端优化方案:

服务器带宽:1兆 2兆
1兆带宽等于128kb/s 加载一个网页640kb 640kb/128klb=5s
带宽入口问题

在一个网页中静态占了大部分整个带宽,动态资源占用带宽非常小。

http://res15.iblimg.com/respc-1/resources/v4.2/widget/footer1200/footer1200.css

http://product.bl.com/3515547.html?bl_ad=644_-366041-1&bl_mmc=YXTF-baiduPC-6xk1078b-_0

上课前体的疑问:

如果秒杀的请求的过多,对数据库频繁的io操作,可能会产生数据库崩溃问题。
分表分库、读写分离 没有用

提前生成好对应库存的令牌 存放在令牌桶中 异步发送到mq中 实现修改库存。

如果采用mq实现秒杀抢购,那么秒杀接口会立马拿到秒杀结果吗?
Mq 异步形式、高并发

1.前端调用秒杀接口如果秒杀成功的话,返回正在排队中……
2.前端写一个定时器使用秒杀token查询是否秒杀成功 MQ消费的速度非常快的情况下

12306抢票的时候 正在出票中…. 等待10秒时间告诉你 出单成功 或者是出单失败 正在排队中….

步骤实现方案:

1.后台系统在发布秒杀商品的时候,给对应的商品添加库存token

秒杀抢购

前端:
1.使用动静分离、将静态资源存放到第三方文件服务器中实现cdn加速,目的减轻秒杀抢购带宽
2.当用户点击秒杀按钮的时候,应该将按钮disabled 防止重复提交
3.使用复杂的图形验证码防止机器模拟
4.秒杀详情页面,使用定时器根据用户信息查询秒杀结果
5.商品的详情页面使用nginx+lua+openresty 实现静态化页面
网关:
1.ratelimter、nginx、hystrix、redis实现限流 令牌痛+漏铜算法 对用户秒杀请求实现限流和服务保护。
2.用户黑名单和白名单拦截
秒杀接口:
1.服务降级级、隔离、熔断
2.从redis中获取秒杀的令牌(能够获取到令牌就能够秒杀成功,否则就秒杀失败!)
3.异步使用MQ执行修改库存操作
4.提供一个根据用户信息查询秒杀结果接口

项目部署点:
Nginx+lvs 实现服务高可用和集群
其他点:
分时段抢购

具体例子:现在有100个商品同时秒杀抢购,每个商品库存为100个?
基于mq+库存令牌桶 100*100=10000 数据库执行修改库存操作压力还是非常大?
最靠谱的访问:12306 分时段秒杀 中午 下午

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值