简易描述秒杀系统的几个主要特点,分布式情况下使用锁,订单超时未支付使用mq的延时队列取消
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<version>2.6.3version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.27version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.2version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.2.8version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
<version>2.5.6version>
dependency>
<dependency>
<groupId>org.redissongroupId>
<artifactId>redisson-spring-boot-starterartifactId>
<version>3.17.0version>
dependency>
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.34.7:3306/order?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
druid:
# 下面为连接池的补充设置,应用到上面所有数据源中
# 初始化大小,最小,最大
initial-size: 15
min-idle: 15
max-active: 50
# 配置获取连接等待超时的时间
max-wait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
time-between-eviction-runs-millis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
min-evictable-idle-time-millis: 300000
redis:
port: 6379
database: 0
host: 192.168.34.7
rabbitmq:
host: 192.168.34.7
port: 5672
virtualHost: /
username: admin
password: admin
listener:
direct:
#手动ask回复
acknowledge-mode: manual
#mybatis plus 设置
mybatis-plus:
mapper-locations: classpath*:com/**/mapper/xml/*.xml
global-config:
# 关闭MP3.0自带的banner
banner: false
db-config:
#主键类型 0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)";
id-type: uuid
# 默认数据库表下划线命名
table-underline: true
configuration:
#这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
@Configuration
public class RabbitMQConfig {
@Bean//支付路由交换机
public DirectExchange payOrderDirectExchange(){
return new DirectExchange("payOrderDirectExchange",true,false);
}
@Bean//支付队列
public Queue payOrderQueue(){
return new Queue("payOrderQueue",true);
}
@Bean//支付绑定
public Binding payOrderBinding(){
return BindingBuilder.bind(payOrderQueue()).to(payOrderDirectExchange()).with("payOrder");
}
@Bean//取消订单路由交换机
public DirectExchange cancelOrderDirectExchange(){
return new DirectExchange("cancelOrderDirectExchange",true,false);
}
@Bean//取消订单队列,TTL DLX DLK
public Queue cancelOrderQueue(){
Map<String,Object> map = new HashMap<>();
map.put("x-message-ttl",30*60*1000);//过期时间30分钟
map.put("x-dead-letter-exchange","cancelOrderDeadDirectExchange");
map.put("x-dead-letter-routing-key","cancelOrderDead");
return new Queue("cancelOrderQueue",true,false,false,map);
}
@Bean//取消订单绑定
public Binding cancelOrderBinding(){
return BindingBuilder.bind(cancelOrderQueue()).to(cancelOrderDirectExchange()).with("cancelOrder");
}
@Bean//配合取消订单ttl结合的死信路由交换机
public DirectExchange cancelOrderDeadDirectExchange(){
return new DirectExchange("cancelOrderDeadDirectExchange",true,false);
}
@Bean//配合取消订单ttl结合的死信队列
public Queue cancelOrderDeadQueue(){
return new Queue("cancelOrderDeadQueue",true);
}
@Bean//配合取消订单ttl结合的死信绑定
public Binding cancelOrderDeadBinding(){
return BindingBuilder.bind(cancelOrderDeadQueue()).to(cancelOrderDeadDirectExchange()).with("cancelOrderDead");
}
}
@Configuration
public class RedisConfig {
@Bean
public LettuceConnectionFactory lettuceConnectionFactory(){
return new LettuceConnectionFactory();
}
@Bean
public Redisson redisson(){
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.34.7:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
// 配置redisTemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory());
// 设置 RedisConnection 工厂。 它就是实现多种 Java Redis 客户端接入的秘密工厂
// 使用 String 序列化方式,序列化 KEY 。
redisTemplate.setKeySerializer(RedisSerializer.string());
// 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
redisTemplate.setValueSerializer(RedisSerializer.string());
return redisTemplate;
}
}
@Autowired
private OrderService orderService;
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private Redisson redisson;
@PostMapping("/saveOrder")
@Transactional
public String saveOrder(@RequestBody Order order){
String shopId = order.getShopId();
//使用redisson加锁
RLock lock = redisson.getLock(shopId);
lock.lock();
String id;
try {
//拿到了锁,库存减1,如果库存是0,就直接return,防止超卖
//代码省略。。。。。。。。。
id = UUID.randomUUID().toString();
order.setOrderId(id);
order.setCreateTime(new Date());
orderService.save(order);
//订单超时未支付自动取消,通过rabbitmq的TTL加死信队列实现
rabbitTemplate.convertAndSend("cancelOrderDirectExchange","cancelOrder",id);
} finally {
lock.unlock();
}
return id;
}
/**
* 取消订单的监听消费
* @param message
* @param channel
* @throws Exception
*/
@RabbitListener(queues = "cancelOrderDeadQueue")
@Transactional
public void cancelOrderListener(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
String body = new String(message.getBody(), "UTF-8");
try {
//订单超时未支付取消
//.....代码省略
log.info("订单编号 {} 已取消 ",body);
channel.basicAck(deliveryTag,false);
}catch (Exception e){
//手动ask回复,可以配合缓存增加重试次数
channel.basicReject(deliveryTag,false);
throw new RuntimeException("cancelOrderListener Exception",e);
}
}
@GetMapping("/pay")
public String pay(String id){
log.info("{} 订单支付了",id);
//mq修改订单状态,并发送物流等信息
rabbitTemplate.convertAndSend("payOrderDirectExchange","payOrder",id);
return "success";
}
/**
* 保存订单,通知物流
* @param message
* @param channel
* @throws Exception
*/
@RabbitListener(queues = "payOrderQueue")
@Transactional
public void payOrderListener(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
String body = new String(message.getBody(), "UTF-8");
try {
//1.订单状态修改
//2.发送物流信息
//..............代码忽略
channel.basicAck(deliveryTag,false);
}catch (Exception e){
//手动ask回复,可以配合缓存增加重试次数
channel.basicReject(deliveryTag,false);
throw new RuntimeException("payOrderListener Exception",e);
}
}