注意点
- 很短时间内出现大量请求
- 对软件和硬件做出相应优化
- 保证系统的高可用性
整体架构图
实现方案
- redis:实现缓存+分布式锁
- RocketMQ:用于解耦和异步调用
- Mysql:存放真实的商品信息
- Mybatis:orm框架
- springboot:接收请求,进行整合
简易代码实现
数据库
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for goods
-- ----------------------------
DROP TABLE IF EXISTS `goods`;
CREATE TABLE `goods` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`goods_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`price` decimal(10, 2) NULL DEFAULT NULL,
`stocks` int(255) NULL DEFAULT NULL,
`status` int(255) NULL DEFAULT NULL,
`pic` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT NULL,
`update_time` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of goods
-- ----------------------------
INSERT INTO `goods` VALUES (1, '小米12s', 4999.00, 1000, 2, 'xxxxxx', '2023-02-23 11:35:56', '2023-02-23 16:53:34');
INSERT INTO `goods` VALUES (2, '华为mate50', 6999.00, 10, 2, 'xxxx', '2023-02-23 11:35:56', '2023-02-23 11:35:56');
INSERT INTO `goods` VALUES (3, '锤子pro2', 1999.00, 100, 1, NULL, '2023-02-23 11:35:56', '2023-02-23 11:35:56');
-- ----------------------------
-- Table structure for order_records
-- ----------------------------
DROP TABLE IF EXISTS `order_records`;
CREATE TABLE `order_records` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NULL DEFAULT NULL,
`order_sn` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`goods_id` int(11) NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
seckill-web服务
配置yml文件
server:
port: 8082
tomcat:
threads:
max: 400
spring:
redis:
host: 127.0.0.1
port: 6379
database: 10
password: 1234
rocketmq:
name-server: 127.0.0.1:9876
producer:
group: seckill-group
编写控制类
思路:
1.利用setnx对用户进行去重,每种商品只能抢购一次
2.库存预扣减
3.消息放入mq中,异步处理订单
@GetMapping("/seckill")
public String doSecKill(Integer goodsId/*, Integer userId*/) {
int userId = userIdAtomic.incrementAndGet();
// uk
String key = userId + "-" + goodsId;
Boolean flag = redisTemplate.opsForValue().setIfAbsent("uk" + key, "");
if (!flag) {
return "你已经参与过该商品的抢购了";
}
//假设库存已经同步了 key:goods_stock:1 val:10
Long count = redisTemplate.opsForValue().decrement("goodsId", goodsId);
if (count <= 0) {
return "商品已经清空";
}
//mq 异步处理
HashMap<String, Integer> map = new HashMap<>(4);
map.put("goodsId", goodsId);
map.put("userId", userId);
rocketMQTemplate.asyncSend("secKillTopic", JSON.toJSONString(map), new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("发送成功");
}
@Override
public void onException(Throwable throwable) {
System.out.println("发送失败:" + throwable.getMessage());
}
});
return "正在抢购中......";
}
seckill-service服务
配置yml文件
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/seckill?serverTimezone=GMT%2B8&SSL=false
password: root
username: root
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: 127.0.0.1
port: 6379
database: 10
password: 1234
main:
allow-bean-definition-overriding: true
rocketmq:
name-server: 127.0.0.1:9876
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
通过easy-code插件生成对应的service,dao,mapper文件(省略)
配置数据同步(定时任务同步库存到redis中 为了测试方便,项目启动时同步数据)
@Component
public class DataSync {
@Resource
private GoodsDao goodsDao;
@Resource
private RedisTemplate redisTemplate;
/**
* 在项目启动后触发
* 并且属性注入完后执行
* @PostConstruct
* 实例化 (new)
* 属性注入
* 初始化(前PostConstruct、中InitializingBean、后BeanPostProcessor)自定义的initMethod方法
* 使用
*/
@PostConstruct
public void initData() {
List<Goods> goodsList = goodsDao.selectSecKillGoods();
if (CollectionUtils.isEmpty(goodsList)){
return;
}
goodsList.forEach(good->{
redisTemplate.opsForValue().set("goodsId"+good.getId(),good.getStocks().toString());
});
}
}
配置rocketmq的监听器
@Component
@RocketMQMessageListener(topic = "secKillTopic",
consumerGroup = "secKill-consumer-group",
consumeMode = ConsumeMode.CONCURRENTLY,
consumeThreadNumber = 40)
public class SecKillListener implements RocketMQListener<MessageExt> {
@Resource
private GoodsService goodsService;
@Resource
private RedisTemplate redisTemplate;
int ZX_TIME = 10000;
/**
* 扣减库存
* 减订单表
* 方案一:加Synchronized锁
* @param messageExt
*/
// @Override
// public void onMessage(MessageExt messageExt) {
// String msg = new String(messageExt.getBody());
// // userId-goodsId
// int userId = Integer.parseInt(msg.split("-")[0]);
// int goodsId = Integer.parseInt(msg.split("-")[1]);
// //在事务外面加锁 实现安全
// synchronized (this) {
// goodsService.realSecKill(userId, goodsId);
// }
// }
/**
* 方案二:redis setnx
*
* @param messageExt
*/
@Override
public void onMessage(MessageExt messageExt) {
String msg = new String(messageExt.getBody());
// userId-goodsId
int userId = Integer.parseInt(msg.split("-")[0]);
int goodsId = Integer.parseInt(msg.split("-")[1]);
int curThreadTime = 0;
while (curThreadTime < ZX_TIME) {
Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock:" + goodsId, "");
if (flag) {
try {
goodsService.realSecKill(userId, goodsId);
return;
} finally {
redisTemplate.delete("lock:" + goodsId);
}
} else {
curThreadTime += 200;
try {
Thread.sleep(200L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
商品测试类
@Service("goodsService")
public class GoodsServiceImpl implements GoodsService {
@Resource
private GoodsDao goodsDao;
@Resource
private OrderRecordsDao orderRecordsDao;
/**
* 通过ID查询单条数据
*
* @param id 主键
* @return 实例对象
*/
@Override
public Goods queryById(Integer id) {
return this.goodsDao.queryById(id);
}
/**
* 新增数据
*
* @param goods 实例对象
* @return 实例对象
*/
@Override
public Goods insert(Goods goods) {
this.goodsDao.insert(goods);
return goods;
}
/**
* 修改数据
*
* @param goods 实例对象
* @return 实例对象
*/
@Override
public Goods update(Goods goods) {
this.goodsDao.update(goods);
return this.queryById(goods.getId());
}
/**
* 通过主键删除数据
*
* @param id 主键
* @return 是否成功
*/
@Override
public boolean deleteById(Integer id) {
return this.goodsDao.deleteById(id) > 0;
}
/**
* 方案一常规做法
* 扣减库存
* 写订单表
*
* @param userId
* @param goodsId
*/
// @Override
// @Transactional(rollbackFor = Exception.class)
// public void realSecKill(int userId, int goodsId) {
// Goods goods = goodsDao.queryById(goodsId);
// Integer finalStock = goods.getStocks() - 1;
// if (finalStock < 0) {
// throw new RuntimeException("商品" + goodsId + "不足,用户Id:" + userId);
// }
// goods.setStocks(finalStock);
// goods.setUpdateTime(new Date());
// int update = goodsDao.update(goods);
// if (update > 0) {
// OrderRecords order = new OrderRecords();
// order.setUserId(userId);
// order.setGoodsId(goodsId);
// order.setCreateTime(new Date());
// orderRecordsDao.insert(order);
// }
// }
/**
* 方案二行锁方案:但不适用于并发量高的场景,最终压力都是数据库承担
* update goods set stocks = stocks - 1,update_time = now() where id = #{value}
* @param userId
* @param goodsId
*/
// @Override
// @Transactional(rollbackFor = Exception.class)
// public void realSecKill(int userId, int goodsId) {
// int i = goodsDao.updateStock(goodsId);
// if (i > 0) {
// OrderRecords order = new OrderRecords();
// order.setUserId(userId);
// order.setGoodsId(goodsId);
// order.setCreateTime(new Date());
// orderRecordsDao.insert(order);
// }
// }
/**
* 方案三
* @param userId
* @param goodsId
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void realSecKill(int userId, int goodsId) {
int i = goodsDao.updateStock(goodsId);
if (i > 0) {
OrderRecords order = new OrderRecords();
order.setUserId(userId);
order.setGoodsId(goodsId);
order.setCreateTime(new Date());
orderRecordsDao.insert(order);
}
}
}