1.【秒杀】
1.1. 技术选择型
Ø Springboot
Ø Redis
Ø Rocketmq
Ø Mysql
Ø MybatisPlus
1.2. 架构图
1.3. 准备工作-数据库
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for goods
-- ----------------------------
DROP TABLE IF EXISTS `goods`;
CREATE TABLE `goods` (
`goods_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`goods_name` varchar(300) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '商品名称',
`price` decimal(15, 2) DEFAULT NULL COMMENT '现价',
`content` text CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT '详细描述',
`status` int(1) DEFAULT 0 COMMENT '默认是1,表示正常状态, -1表示删除, 0下架',
`total_stocks` int(11) DEFAULT 0 COMMENT '总库存',
`create_time` datetime(0) DEFAULT NULL COMMENT '录入时间',
`update_time` datetime(0) DEFAULT NULL COMMENT '修改时间',
`spike` int(11) DEFAULT 0 COMMENT '是否参与秒杀1是0否',
PRIMARY KEY (`goods_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 95 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '商品' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of goods
-- ----------------------------
INSERT INTO `goods` VALUES (18, 'Apple iPhone XS Max 移动联通电信4G手机 ', 1.01, '<div style=\"margin: 0px; padding: 0px; color: #666666; font-family: tahoma, arial, \'Microsoft YaHei\', \'Hiragino Sans GB\', u5b8bu4f53, sans-serif; font-size: 12px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-style: initial; text-decoration-color: initial;\" align=\"center\">\n<table id=\"__01\" style=\"text-align: center;\" border=\"0\" width=\"750\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td><img class=\"\" style=\"margin: 0px; padding: 0px; border: 0px; vertical-align: middle;\" src=\"https://img30.360buyimg.com/cms/jfs/t1/4626/32/3475/220504/5b997365E80a1373f/279c244f12161cb3.jpg\" alt=\"\" width=\"750\" height=\"1991\" /></td>\n</tr>\n<tr>\n<td><img class=\"\" style=\"margin: 0px; padding: 0px; border: 0px; vertical-align: middle;\" src=\"https://img12.360buyimg.com/cms/jfs/t1/3397/21/3533/236322/5b99759aE73795787/f782e04a140c8f16.jpg\" alt=\"\" width=\"750\" height=\"2052\" /></td>\n</tr>\n<tr>\n<td><img class=\"\" style=\"margin: 0px; padding: 0px; border: 0px; vertical-align: middle;\" src=\"https://img11.360buyimg.com/cms/jfs/t1/5274/3/3465/245167/5b997365E16b81bc9/93e07e40f3af5e62.jpg\" alt=\"\" width=\"750\" height=\"2250\" /></td>\n</tr>\n<tr>\n<td><img class=\"\" style=\"margin: 0px; padding: 0px; border: 0px; vertical-align: middle;\" src=\"https://img30.360buyimg.com/cms/jfs/t1/2322/11/3524/269574/5b997365E26f81a7a/e01fc9486da9eda1.jpg\" alt=\"\" width=\"750\" height=\"2327\" /></td>\n</tr>\n<tr>\n<td><img class=\"\" style=\"margin: 0px; padding: 0px; border: 0px; vertical-align: middle;\" src=\"https://img13.360buyimg.com/cms/jfs/t1/5074/21/3432/296470/5b997364Ee966f7a0/7f424d41479db45d.jpg\" alt=\"\" width=\"750\" height=\"2561\" /></td>\n</tr>\n<tr>\n<td><img class=\"\" style=\"margin: 0px; padding: 0px; border: 0px; vertical-align: middle;\" src=\"https://img13.360buyimg.com/cms/jfs/t1/5770/18/3580/288371/5b997365Ea2c58cb4/176b9a40ccd4e56b.jpg\" alt=\"\" width=\"750\" height=\"2668\" /></td>\n</tr>\n<tr>\n<td><img class=\"\" style=\"margin: 0px; padding: 0px; border: 0px; vertical-align: middle;\" src=\"https://img11.360buyimg.com/cms/jfs/t1/227/21/3811/268132/5b997364E3d6c51b2/92d2a3a559e3baa8.jpg\" alt=\"\" width=\"750\" height=\"2850\" /></td>\n</tr>\n<tr>\n<td><img class=\"\" style=\"margin: 0px; padding: 0px; border: 0px; vertical-align: middle;\" src=\"https://img20.360buyimg.com/cms/jfs/t1/3787/5/3493/125020/5b997363E3c9f5910/ddbd08a556744630.jpg\" alt=\"\" width=\"750\" height=\"1486\" /></td>\n</tr>\n<tr>\n<td><img class=\"\" style=\"margin: 0px; padding: 0px; border: 0px; vertical-align: middle;\" src=\"https://img30.360buyimg.com/cms/jfs/t1/1687/5/3327/266718/5b997366E9cc80e69/9e40ceae1fef4466.jpg\" alt=\"\" width=\"750\" height=\"3376\" /></td>\n</tr>\n<tr>\n<td><img class=\"\" style=\"margin: 0px; padding: 0px; border: 0px; vertical-align: middle;\" src=\"https://img30.360buyimg.com/cms/jfs/t1/457/6/3849/283318/5b997363E0c5ab7a9/6f636f0a286bc87c.jpg\" alt=\"\" width=\"750\" height=\"2455\" /></td>\n</tr>\n<tr>\n<td><img class=\"\" style=\"margin: 0px; padding: 0px; border: 0px; vertical-align: middle;\" src=\"https://img10.360buyimg.com/cms/jfs/t1/397/25/3796/217624/5b9975a8E5ee578af/4d8f05a606fa5c4a.jpg\" alt=\"\" width=\"750\" height=\"2703\" /></td>\n</tr>\n</tbody>\n</table>\n</div>', 1, 10, '2019-03-29 14:40:00', '2019-06-22 18:28:32', 1);
INSERT INTO `goods` VALUES (59, '兰蔻粉水清滢柔肤水400ml 爽肤水女保湿舒缓滋润嫩肤', 420.00, '<p><img src=\"http://img-test.gz-yami.com/2019/04/71f54ee20ef34872b1e0aa53cb75b7b6.jpg\" alt=\"\" width=\"790\" height=\"1110\" /></p>', 1, 10, '2019-04-21 19:15:34', '2019-04-29 14:30:44', 1);
INSERT INTO `goods` VALUES (68, '【Dole都乐】菲律宾都乐非转基因木瓜1只 单只约410g', 26.00, '<p style=\"text-align: justify;\"><img src=\"http://img-test.gz-yami.com/2019/04/e7536a53a83d450e8635ce1e9819faf6.jpg\" alt=\"\" width=\"790\" height=\"350\" /></p>', 1, 10, '2019-04-21 21:56:38', '2019-05-22 10:30:37', 0);
INSERT INTO `goods` VALUES (69, '阿迪达斯官方 adidas 三叶草 NITE JOGGER 男子经典鞋BD7956', 1199.00, '<p><img src=\"http://img-test.gz-yami.com/2019/04/6d0bea4a0be54423999136bcd1158897.jpg\" alt=\"\" width=\"790\" height=\"2232\" /></p>', 1, 10, '2019-04-21 22:10:04', '2019-05-23 20:17:03', 0);
INSERT INTO `goods` VALUES (70, '【Dole都乐】比利时Truval啤梨12只 进口水果新鲜梨 单果120g左右', 38.00, '<p><img src=\"http://img-test.gz-yami.com/2019/04/67ce2251e9b14ea08b87752ef7b30207.jpg\" alt=\"\" width=\"760\" height=\"488\" /></p>', 1, 10, '2019-04-22 16:43:33', '2019-06-22 09:40:24', 0);
INSERT INTO `goods` VALUES (71, '旗舰店官网 自拍神器 梵高定制', 6998.00, '<p><img src=\"http://img-test.gz-yami.com/2019/04/fa35b300102e45f3a57d7c5c775ebf6d.jpg\" alt=\"\" width=\"790\" height=\"853\" /></p>\n<p><img src=\"http://img-test.gz-yami.com/2019/04/f8fd168ddb8a437dbb5b742691bd1d02.jpg\" alt=\"\" width=\"800\" height=\"800\" /><img src=\"http://img-test.gz-yami.com/2019/04/db46108466264b48841b18437940e0b3.jpg\" alt=\"\" width=\"800\" height=\"800\" /></p>', 1, 10, '2019-04-23 15:43:26', '2019-05-21 11:01:59', 0);
-- ----------------------------
-- Table structure for order
-- ----------------------------
DROP TABLE IF EXISTS `order`;
CREATE TABLE `order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`userid` int(11) DEFAULT NULL,
`goodsid` int(11) DEFAULT NULL,
`createtime` datetime(0) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 23 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
1.4. 创建项目选择依赖spike-web(接受用户秒杀请求)
1.4.1. Pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.powernode</groupId>
<artifactId>seckill-web</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>04-seckill-web</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- rocketmq的依赖 -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.9</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.4.2. 修改配置文件
server:
port: 7001
spring:
application:
name: spike-web
redis:
host: 192.168.47.128
port: 6380
database: 0
rocketmq:
name-server: 127.0.0.1:9876 # rocketMq的nameServer地址
producer:
group: powernode-group # 生产者组别
send-message-timeout: 3000 # 消息发送的超时时间
retry-times-when-send-async-failed: 2 # 异步消息发送失败重试次数
max-message-size: 4194304 # 消息的最大长度
1.4.3. 添加布隆过滤器的配置
@Bean
public BitMapBloomFilter bitMapBloomFilter() {
return new BitMapBloomFilter(100);
}
1.4.4. 创建SpikeController
@RestController
public class SeckillController {
@Autowired
private BitMapBloomFilter bitMapBloomFilter;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 因为我们没有做登录 安全框架
* 1.先判断当前用户对这个商品有没有买过
* 2.看这个商品的库存够不够
* 3.组装数据放mq
*
* @return
*/
@GetMapping("doSeckill")
public String doSeckill(Integer goodsId, Integer userId) {
// 使用布隆过滤器来判断用户是否买过这个商品 (只要是去重操作 都可以使用)
// 一个用户针对一个商品只能买一次 双主键
String secKillId = userId + "-" + goodsId;
if (bitMapBloomFilter.contains(secKillId)) {
return "您已经参与过该商品的抢购,请参与其他商品(*^▽^*)";
}
bitMapBloomFilter.add(secKillId);
// 判断该商品的库存是否足够
// 需要将mysql的数据 要同步到redis去
// 直接通过redis来做减法 因为redis是单线程的 所以是安全的
Long count = redisTemplate.opsForValue().decrement("secKill:" + goodsId);
// 我不是只放10个人进来操作 而是多放10%-30%的请求进来
// 为了防止有的请求报错 失败了
if (count < -3) {
return "该商品已经卖完了,下次早点来";
}
// 走到这里的人 就有机会买到
// 组装数据 放mq 操作数据库的代码 异步执行 让这里的线程return回收 提高服务器并发量
HashMap<String, Integer> map = new HashMap<>(4);
// 如果你知道你的map要放多少数据进去 直接在定义的时候就写好
map.put("userId", userId);
map.put("goodsId", goodsId);
// 通过mq来做数据传输 最好放什么类型的数据 Integer Map Obj String是万能的 json
rocketMQTemplate.syncSend("secKillTopic", JSONUtil.toJsonStr(map));
return "正在拼命抢购中,请稍后查看";
}
}
1.5. 创建项目选择依赖spike-service(处理秒杀)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.powernode</groupId>
<artifactId>seckill-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>05-seckill-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- rocketmq的依赖 -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.9</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.5.1. 修改yml文件
server:
port: 7002
spring:
application:
name: spike-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/spike?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: root
redis:
host: 192.168.47.128
port: 6380
database: 0
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:mapper/*.xml
rocketmq:
name-server: 127.0.0.1:9876
1.5.2. 逆向生成实体类等
1.5.3. 修改启动类
@SpringBootApplication
@MapperScan(basePackages = {"com.bjpowernode.mapper"})
public class SpikeServiceApplication {
public static void main(String[] args) {
SpringApplication.run(SpikeServiceApplication.class, args);
}
}
1.5.4. 同步mysql数据到redis
/**
* 实现CommandLineRunner 项目启动后会执行里面的run方法
*/
@Component
public class MysqlToRedis implements CommandLineRunner {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private GoodsMapper goodsMapper;
/**
* 查询数据库中参与秒杀的商品
* 将商品id和数量存入redis
*/
public void goods2Redis() {
List<Goods> goodsList = goodsMapper.selectList(new LambdaQueryWrapper<Goods>()
.eq(Goods::getSpike, 1)
.eq(Goods::getStatus, 1)
);
if (CollectionUtils.isEmpty(goodsList)) {
return;
}
goodsList.forEach(goods -> {
redisTemplate.opsForValue().set("goods_stock:" + goods.getGoodsId(), goods.getTotalStocks().toString());
});
}
@Override
public void run(String... args) throws Exception {
goods2Redis();
}
}
1.5.5. 创建秒杀监听
@Component
@RocketMQMessageListener(topic = "secKillTopic",
consumerGroup = "secKill-group",
messageModel = MessageModel.CLUSTERING,
consumeThreadMax = 64
)
@Slf4j
public class SpikeListener implements RocketMQListener<MessageExt> {
@Autowired
private OrderService orderService;
/**
* 处理秒杀
*
* @param message
*/
@Override
public void onMessage(MessageExt message) {
String spikeStr = new String(message.getBody());
JSONObject jsonObject = JSONUtil.parseObj(spikeStr);
Integer userId = jsonObject.getInt("userId");
Integer goodsId = jsonObject.getInt("goodsId");
try {
orderService.realDoSpike(userId, goodsId);
} catch (Exception e) {
log.error("抢购失败,用户id为{},商品id为{}", userId, goodsId);
}
}
}
1.5.6. 修改OrderService
public interface OrderService extends IService<Order> {
/**
* 做秒杀
*
* @param userId
* @param goodsId
*/
void realDoSpike(Integer userId, Integer goodsId);
}
1.5.7. 修改OrderServiceImpl
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper;
@Autowired
private GoodsMapper goodsMapper;
@Autowired
private StringRedisTemplate redisTemplate;
// 定义一个时间
private Long allTime = 1000L;
@Autowired
private RedissonClient redissonClient;
@Override
public int deleteByPrimaryKey(Integer id) {
return orderMapper.deleteByPrimaryKey(id);
}
@Override
public int insert(Order record) {
return orderMapper.insert(record);
}
@Override
public int insertSelective(Order record) {
return orderMapper.insertSelective(record);
}
@Override
public Order selectByPrimaryKey(Integer id) {
return orderMapper.selectByPrimaryKey(id);
}
@Override
public int updateByPrimaryKeySelective(Order record) {
return orderMapper.updateByPrimaryKeySelective(record);
}
@Override
public int updateByPrimaryKey(Order record) {
return orderMapper.updateByPrimaryKey(record);
}
/**
* 减库存和写订单表
* 有线程安全问题
* A线程执行到 goodsMapper.selectByPrimaryKey(goodsId); B 也执行这行代码 都查到有1件商品
* 加锁
* <p>
* 分布式锁
* mysql的版本号做 乐观锁
* update goods set stock = stock ,version = version + 1 where goodsId = goodsId and version = version
* 如果有两个人同时查询到 库存足够 并且version 相同
* 此时去操作数据库 只能有一个人成功 体验不好
* <p>
* redis来做分布式锁
* setnx set if not exist 如果这个key不存在 就能设置成功 返回true 通过业务来判断 加锁
*
* @param goodsId
* @param userId
*/
@Override
@Transactional(rollbackFor = RuntimeException.class)
public void realSpike(Integer goodsId, Integer userId) {
// RLock lock = redissonClient.getLock("goods_lock:" + goodsId);
// lock.lock();
// 写一个自旋 来重试
long time = 0;
while (time < allTime) {
// setnx = setIfAbsent(缺席) 给一个过期时间 方式卡死 别的线程永远进不来了 时间不要写死 定义变量 更新
// 锁续命 long id = Thread.currentThread().getId(); 拿到当前获取锁的一个线程id
// 搞一个任务调度 每隔2/3的时间 去判断一下 获取锁的线程是否是当前线程 如果是 就将时间 加 1/3 的时间
Boolean flag = redisTemplate.opsForValue().setIfAbsent("goods_lock:" + goodsId, "", Duration.ofSeconds(30));
// redisTemplate.expire("goods_lock:" + goodsId, Duration.ofSeconds(10));
if (flag) {
try {
Goods goods = goodsMapper.selectByPrimaryKey(goodsId);
// 只能锁住本地jvm 在分布式环境中 不能起作用
int finalStock = goods.getTotalStocks() - 1;
if (finalStock < 0) {
throw new RuntimeException("商品的库存不足,id为" + goods.getGoodsId());
}
goods.setTotalStocks(finalStock);
goods.setUpdateTime(new Date());
int i = goodsMapper.updateByPrimaryKey(goods);
if (i > 0) {
writeOrder(goodsId, userId);
return;
}
} finally {
// 删掉这个锁 不然别人就获取不到锁了
redisTemplate.delete("goods_lock:" + goodsId);
// lock.unlock();
}
} else {
time += 300;
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 写订单表
*
* @param goodsId
* @param userId
*/
private void writeOrder(Integer goodsId, Integer userId) {
Order order = new Order();
order.setCreatetime(new Date());
order.setUserid(userId);
order.setGoodsid(goodsId);
orderMapper.insert(order);
}
}
1.5.8. pom.xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.6</version>
</dependency>
1.5.9. 启动类
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("localhost:6379");
return Redisson.create(config);
}