电商秒杀方法
一、秒杀业务分析
1.正常电子商务流程 (1)查询商品;(2)创建订单;(3)扣减库存;(4)更新订单;(5)付款;(6)卖家发货
2.秒杀业务特性流程 ( 1)低廉价格;(2)大幅推广;(3)瞬时售空;(4)一般是定时上架;(5)时间短、瞬时并发量高;
3.秒杀实现技术挑战
(1)秒杀技术挑战 假设某网站秒杀活动只推出一件商品,预计会吸引1万人参加活动,也就说最大并发请求数是10000,秒杀系统需要面对的技术挑战有:
对现有网站业务造成冲击 秒杀活动只是网站营销的一个附加活动,这个活动具有时间短,并发访问量大的特点,如果和网站原有应用部署在一起,必然会对现有业务造成冲击,
稍有不慎可能导致整个网站瘫痪。 解决方案:将秒杀系统独立部署,甚至使用独立域名,使其与网站完全隔离。
二、秒杀抢购修改库存如何减少数据库IO操作
在高并发情况下,如果突然有10万个不同用户的请求进行秒杀,但是商品的库存数量只有100个,那么这时候可能会出现10个请求执行修改秒杀库存sql语句,这时候可能会出现数据库访问压力承受不了?
-秒杀抢购修改库存如何减少数据库IO操作 数据库分表分库、读写分离、使用redis缓存减去数据库访问压力
非常靠谱的秒杀方案 基于MQ+库存令牌桶实现 同时有10万个请求实现秒杀、商品库存只有100个 实现只需要修改库存100次就可以了
方案实现流程:提前对应的商品库存生成好对应令牌(100个令牌),在10万个请求中,只要谁能够获取到令牌谁就能够秒杀成功, 获取到秒杀令牌后,在使用mq异步实现修改减去库存。
三、使用数据库乐观锁实现防止超卖问题
1、数据库表结构
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='秒杀库存表';
2、实体类
/**
* 秒杀实体
*/
@Data
public class SeckillEntity {
private long seckillId;
//商品名称
private String name;
//库存数量
private Integer inventory;
//秒杀开启时间
private Date startTime;
//秒杀结束时间
private Date endTime;
//创建时间
private Date createTime;
//版本号
private Long version;
}
/**
* 订单实体
*/
@Data
public class OrderEntity {
//秒杀商品ID
private Long seckillId;
//用户手机号
private String userPhone;
//状态
private Integer state;
//创建时间
private Date createTime;
}
3、工具类
@Component
public class RedisUtil {
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 如果key存在的话返回fasle 不存在的话返回true
public Boolean setNx(String key, String value, Long timeout) {
Boolean setIfAbsent = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
if (timeout != null) {
stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
return setIfAbsent;
}
public StringRedisTemplate getStringRedisTemplate() {
return stringRedisTemplate;
}
public void setList(String key, List<String> listToken) {
stringRedisTemplate.opsForList().leftPushAll(key, listToken);
}
/**
* 存放string类型
*
* @param key
* key
* @param data
* 数据
* @param timeout
* 超时间
*/
public void setString(String key, String data, Long timeout) {
try {
stringRedisTemplate.opsForValue().set(key, data);
if (timeout != null) {
stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
} catch (Exception e) {
}
}
/**
* 开启Redis 事务
*
* @param isTransaction
*/
public void begin() {
// 开启Redis 事务权限
stringRedisTemplate.setEnableTransactionSupport(true);
// 开启事务
stringRedisTemplate.multi();
}
/**
* 提交事务
*
* @param isTransaction
*/
public void exec() {
// 成功提交事务
stringRedisTemplate.exec();
}
/**
* 回滚Redis 事务
*/
public void discard() {
stringRedisTemplate.discard();
}
/**
* 存放string类型
*
* @param key
* key
* @param data
* 数据
*/
public void setString(String key, String data) {
setString(key, data, null);
}
/**
* 根据key查询string类型
*
* @param key
* @return
*/
public String getString(String key) {
String value = stringRedisTemplate.opsForValue().get(key);
return value;
}
/**
* 根据对应的key删除key
*
* @param key
*/
public Boolean delKey(String key) {
return stringRedisTemplate.delete(key);
}
}
@Component
public class GenerateToken {
@Autowired
private RedisUtil redisUtil;
/**
* 生成令牌
*
* @param prefix
* 令牌key前缀
* @param redisValue
* redis存放的值
* @return 返回token
*/
public String createToken(String keyPrefix, String redisValue) {
return createToken(keyPrefix, redisValue, null);
}
/**
* 生成令牌
*
* @param prefix
* 令牌key前缀
* @param redisValue
* redis存放的值
* @param time
* 有效期
* @return 返回token
*/
public String createToken(String keyPrefix, String redisValue, Long time) {
if (StringUtils.isEmpty(redisValue)) {
new Exception("redisValue Not nul");
}
String token = keyPrefix + UUID.randomUUID().toString().replace("-", "");
redisUtil.setString(token, redisValue, time);
return token;
}
public void createListToken(String keyPrefix, String redisKey, Long tokenQuantity) {
List<String> listToken = getListToken(keyPrefix, tokenQuantity);
redisUtil.setList(redisKey, listToken);
}
public List<String> getListToken(String keyPrefix, Long tokenQuantity) {
List<String> listToken = new ArrayList<>();
for (int i = 0; i < tokenQuantity; i++) {
String token = keyPrefix + UUID.randomUUID().toString().replace("-", "");
listToken.add(token);
}
return listToken;
}
public String getListKeyToken(String key) {
String value = redisUtil.getStringRedisTemplate().opsForList().leftPop(key);
return value;
}
/**
* 根据token获取redis中的value值
*
* @param token
* @return
*/
public String getToken(String token) {
if (StringUtils.isEmpty(token)) {
return null;
}
String value = redisUtil.getString(token);
return value;
}
/**
* 移除token
*
* @param token
* @return
*/
public Boolean removeToken(String token) {
if (StringUtils.isEmpty(token)) {
return null;
}
return redisUtil.delKey(token);
}
}
4、mapper类
@Mapper
public interface SeckillMapper {
/**
* 基于版本号形式实现乐观锁
*
* @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);
/**
* 查询秒杀订单
* @param seckillId
* @return
*/
@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);
/**
* 插入秒杀订单
* @param orderEntity
* @return
*/
@Insert("INSERT INTO `meite_order` VALUES (#{seckillId},#{userPhone}, '1', now());")
int insertOrder(OrderEntity orderEntity);
}
5、service类
/**
* 库存超卖
*/
@Service
public class SpikeCommodityService {
@Autowired
private SeckillMapper seckillMapper;
@Autowired
private RedisUtil redisUtil;
@Transactional
public JSONObject spike(String phone, Long seckillId) {
JSONObject jsonObject = new JSONObject();
// 1.验证参数
if (StringUtils.isEmpty(phone)) {
jsonObject.put("error","手机号码不能为空!");
return jsonObject;
}
if (seckillId == null) {
jsonObject.put("error","库存id不能为空!");
return jsonObject;
}
// >>>限制用户访问频率 比如10秒中只能访问一次
Boolean resultNx = redisUtil.setNx(phone, seckillId + "", 10l);
if (!resultNx) {
jsonObject.put("error","该用户操作过于频繁,请稍后重试!");
return jsonObject;
}
// 2.根据库存id查询商品是否存在
SeckillEntity seckillEntity = seckillMapper.findBySeckillId(seckillId);
if (seckillEntity == null) {
jsonObject.put("error","该商品信息不存在!");
return jsonObject;
}
// 3.对库存的数量实现减去1
Long version = seckillEntity.getVersion();
int inventoryDeduction = seckillMapper.optimisticVersionSeckill(seckillId, version);
if (inventoryDeduction<=0) {
jsonObject.put("error","秒杀失败");
return jsonObject;
}
// 4.添加秒杀成功订单
OrderEntity orderEntity = new OrderEntity();
orderEntity.setSeckillId(seckillId);
orderEntity.setUserPhone(phone);
int insertOrder = seckillMapper.insertOrder(orderEntity);
if (insertOrder<=0) {
jsonObject.put("success","恭喜你,秒杀成功!");
return jsonObject;
}
jsonObject.put("error","秒杀失败");
return jsonObject;
}
}
6、controller类
@RestController
public class SpikeCommodityController {
@Autowired
private SpikeCommodityService spikeCommodityService;
@RequestMapping("/spike")
public JSONObject spike(String phone, Long seckillId){
JSONObject jsonObject = spikeCommodityService.spike(phone,seckillId);
return jsonObject;
}
}
7、启动类
@SpringBootApplication
public class SpikeBootStrap {
public static void main(String[] args) {
SpringApplication.run(SpikeBootStrap.class);
}
}
8、pom文件
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
</parent>
<properties>
<mybatis-spring-boot.version>1.3.1</mybatis-spring-boot.version>
<mybatis.version>3.4.5</mybatis.version>
</properties>
<dependencies>
<!-- 集成commons工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- 集成lombok 框架 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.30</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis起步依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--Mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.0.7</version>
</dependency>
<!-- 添加springboot对amqp的支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- redis缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
9、yml文件
server:
port: 9800
spring:
application:
name: app-mayikt-spike
redis:
host: 127.0.0.1
# password: 123456
port: 6379
pool:
max-idle: 100
min-idle: 1
max-active: 1000
max-wait: -1
###数据库相关连接
datasource:
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/meite_spike
四、秒杀服务基于库存令牌桶实现修改商品库存
1、生产者
/**
* 生产者发送消息
*/
@Component
public class SpikeCommodityProducer implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@Transactional
public void send(JSONObject jsonObject){
String jsonString = jsonObject.toJSONString();
String messAgeId = UUID.randomUUID().toString().replace("-", "");
MessageBuilder.withBody(jsonString.getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_JSON)
.setContentEncoding("utf-8")
.setMessageId(messAgeId);
//构造参数
this.rabbitTemplate.setMandatory(true);
this.rabbitTemplate.setConfirmCallback(this);
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
//获取id
String messageId = correlationData.getId();
JSONObject jsonObject = JSONObject.parseObject(messageId);
if (ack){
System.out.println("消费成功");
}else{
//重试机制调用
send(jsonObject);
}
}
}
2、service类
/**
* 基于mq实现库存
*/
@Component
public class SpikeCommodity {
@Autowired
private SeckillMapper seckillMapper;
@Autowired
private RedisUtil redisUtil;
@Autowired
private GenerateToken generateToken;
@Autowired
private SpikeCommodityProducer spikeCommodityProducer;
@Transactional
public JSONObject getOrder(String phone, Long seckillId) {
JSONObject jsonObject = new JSONObject();
// 1.验证参数
if (StringUtils.isEmpty(phone)) {
jsonObject.put("error","手机号码不能为空!");
return jsonObject;
}
if (seckillId == null) {
jsonObject.put("error","库存id不能为空!");
return jsonObject;
}
// 2.从redis从获取对应的秒杀token
String seckillToken = generateToken.getListKeyToken(seckillId + "");
if (StringUtils.isEmpty(seckillToken)) {
return null;
}
// 3.获取到秒杀token之后,异步放入mq中实现修改商品的库存
sendSeckillMsg(seckillId, phone);
return jsonObject;
}
@Async
public void sendSeckillMsg(Long seckillId, String phone) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("seckillId",seckillId);
jsonObject.put("phone",phone);
spikeCommodityProducer.send(jsonObject);
}
}
3、创建token
// 采用redis数据库类型为 list类型 key为 商品库存id list 多个秒杀token
public String addSpikeToken(Long seckillId, Long tokenQuantity) {
// 1.验证参数
if (seckillId == null) {
return "商品库存id不能为空!";
}
if (tokenQuantity == null) {
return "token数量不能为空!";
}
SeckillEntity seckillEntity = seckillMapper.findBySeckillId(seckillId);
if (seckillEntity == null) {
return "商品信息不存在!";
}
// 2.使用多线程异步生产令牌
createSeckillToken(seckillId, tokenQuantity);
return "令牌正在生成中.....";
}
@Async
public void createSeckillToken(Long seckillId, Long tokenQuantity) {
generateToken.createListToken("seckill_", seckillId + "", tokenQuantity);
}
4、消费者
/**
* 消费者
*/
@Component
public class StockConsumer {
@Autowired
private SeckillMapper seckillMapper;
@RabbitListener(queues = {"modify_inventory_queue"})
public void process(Message message, Channel channel) throws UnsupportedEncodingException {
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(), "UTF-8");
JSONObject jsonObject = JSONObject.parseObject(msg);
// 1.获取秒杀id
Long seckillId = jsonObject.getLong("seckillId");
SeckillEntity seckillEntity = seckillMapper.findBySeckillId(seckillId);
if (seckillEntity == null) {
return;
}
Long version = seckillEntity.getVersion();
int inventoryDeduction = seckillMapper.optimisticVersionSeckill(seckillId, version);
if (!toDaoResult(inventoryDeduction)) {
return;
}
// 2.添加秒杀订单
OrderEntity orderEntity = new OrderEntity();
String phone = jsonObject.getString("phone");
orderEntity.setUserPhone(phone);
orderEntity.setSeckillId(seckillId);
orderEntity.setState((int) 1l);
int insertOrder = seckillMapper.insertOrder(orderEntity);
if (!toDaoResult(insertOrder)) {
return;
}
}
// 调用数据库层判断
public Boolean toDaoResult(int result) {
return result > 0 ? true : false;
}
}
5、MQ配置类
/**
* rabbitMq配置类
*/
@Configuration
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");
}
}

浙公网安备 33010602011771号