项目说明:
1.订单超时处理,用redis的zset有序集合来做
2.基于rabbitMQ实现在高并发下性能倍增
项目结构
第一步:新建maven项目
第二步:配置pom文件
第三步:项目版本说明
## springboot 2.0 集成 mybatis
### 环境:
* 开发工具:spring tools
* springboot: **2.0.1.RELEASE**
* jdk:1.8.0_40
* maven:3.3.9
* alibaba Druid 数据库连接池:1.1.9
### 额外功能:
* PageHelper 分页插件
v0.0.1 版本支持
spring boot 2+,集成mybatis
添加
http://localhost:8080/user/add
post
参数 userName,password,phone
分页查询
http://localhost:8080/user/all?pageNum=1&pageSize=2
v0.0.2
增加单元测试,集成redis
实现业务;订单超时处理,用redis的zset有序集合还做
关键技术:
key为订单号,value为时间戳
判断条件为时间戳和当前时间戳比较,如果在指定范围内,则删除
zset通过score来排序,每次取第一条数据,循环查询
zset的key是可以被覆盖的
v0.0.3
增加模拟3000并发访问数据库
v0.0.4
增加 RabbitMQ 支持
6种工作模式
第一种:
1.hello world 点对点
2.工作模式(竞争) 一对多
通过这个例子我们可以看做高并发情况下的消息产生和消费,这会产生一个消息丢失的问题。万一客户端在处理消息的时候挂了,那这条消息就相当于被浪费了,针对这种情况,rabbitmq推出了消息ack机制,熟悉tcp三次握手的一定不会陌生。
我们看看springboot是实现ack的
3.发布订阅模式
生产者将消息不是直接发送到队列,而是发送到X交换机,然后由交换机发送给两个队列,两个消费者各自监听一个队列,来消费消息。
这种方式实现同一个消息被多个消费者消费。工作模式是同一个消息只能有一个消费者。
我们新建三个队列
4:路由模式
需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配,这是一个完整的匹配。
5.主题模式
第四步:核心类RabbitConfig
package com.wtt.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//作用:如果rabbit中不存在名为XXX的列队,则会创建一个
@Configuration
public class RabbitConfig {
@Bean
public Queue recvqueueQueue() {
return new Queue("recvqueue");
}
// 点对点
@Bean
public Queue helloQueue() {
return new Queue("hello");
}
// 一对多
@Bean
public Queue neoQueue() {
return new Queue("neo");
}
@Bean
public Queue objectQueue() {
return new Queue("object");
}
// 发布与订阅
// 生产者将消息不是直接发送到队列,而是发送到X交换机,然后由交换机发送给两个队列,两个消费者各自监听一个队列,来消费消息。
// 这种方式实现同一个消息被多个消费者消费。工作模式是同一个消息只能有一个消费者。
@Bean
public Queue AMessage() {
return new Queue("fanout.A");
}
@Bean
public Queue BMessage() {
return new Queue("fanout.B");
}
@Bean
public Queue CMessage() {
return new Queue("fanout.C");
}
// 定义一个交换机
@Bean
FanoutExchange fanoutExchange() {
return new FanoutExchange("fanoutExchange");
}
// 再把这些队列绑定到交换机上去
@Bean
Binding bindingExchangeA(Queue AMessage, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(AMessage).to(fanoutExchange);
}
@Bean
Binding bindingExchangeB(Queue BMessage, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(BMessage).to(fanoutExchange);
}
@Bean
Binding bindingExchangeC(Queue CMessage, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(CMessage).to(fanoutExchange);
}
// 第6种主题模式
final static String messageA = "topic.A";
final static String messageB = "topic.B";
@Bean
public Queue queueMessageA() {
return new Queue(RabbitConfig.messageA);
}
@Bean
public Queue queueMessageB() {
return new Queue(RabbitConfig.messageB);
}
@Bean
TopicExchange exchange() {
return new TopicExchange("topicExchange");
}
// 绑定队列到交换机上,路由模式,需要完整匹配topic.message,才能接受
@Bean
Binding bindingExchangeMessage(Queue queueMessageA, TopicExchange exchange) {
return BindingBuilder.bind(queueMessageA).to(exchange).with("topic.message");
}
// topic模式,前缀匹配到topic.即可接受
@Bean
Binding bindingExchangeMessages(Queue queueMessageB, TopicExchange exchange) {
return BindingBuilder.bind(queueMessageB).to(exchange).with("topic.#");
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setMessageConverter(new Jackson2JsonMessageConverter());
return template;
}
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
return factory;
}
}
CalendarUtils 获取当前时间second秒后的时间戳
package com.wtt.utils;
import java.util.Calendar;
public class CalendarUtils {
/***
* 获取当前时间second秒后的时间戳
* @param second
* @return
*/
public static long getCurrentTimeInMillis(int second) {
Calendar cal = Calendar.getInstance();
if(second>0) {
cal.add(Calendar.SECOND, second);
}
return cal.getTimeInMillis();//5秒以后的时间戳
}
}
RedisUtils //核心处理类
package com.wtt.utils;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Tuple;
@Component
public class RedisUtils {
@Autowired
private JedisPool jedisPool;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:MM:ss");
// 新增元素
public void addItem(String key, long second, String member) {
Jedis redis = jedisPool.getResource();
// 有序集合
redis.zadd(key, second, member);
System.out.println(sdf.format(new Date()) + "-reids 生产了一个任务 score:"+second);
}
// 判断超时元素
public void doFind(String key) {
Jedis redis = jedisPool.getResource();
while (true) {
Set
if(scores == null || scores.isEmpty()) {
continue;
}else {
//拿到元素,通过score(超时时间戳)与当前时间戳对比,判断是否超时
double score = ((Tuple)scores.toArray()[0]).getScore();
long currentTimeInMillis = CalendarUtils.getCurrentTimeInMillis(0);
if(currentTimeInMillis >=score) {
//订单超时
String member =((Tuple)scores.toArray()[0]).getElement();
redis.zrem(key, member);
System.out.println(sdf.format(new Date()) + "reids 删除一个任务 key:"+key+",member:"+member);
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
表SQL demo
CREATE DATABASE mytest;
CREATE TABLE t_user(
userId INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
userName VARCHAR(255) NOT NULL ,
password VARCHAR(255) NOT NULL ,
phone VARCHAR(255) NOT NULL
) ENGINE=INNODB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8;
UserDomain
package com.wtt.model;
import java.io.Serializable;
public class UserDomain implements Serializable{
private Integer userId;
private String userName;
private String password;
private String phone;
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName == null ? null : userName.trim();
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password == null ? null : password.trim();
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone == null ? null : phone.trim();
}
}
UserMapper.xml
t_user
userId,userName,password,phone
INSERT INTO
userName,password,
phone,
#{userName, jdbcType=VARCHAR},#{password, jdbcType=VARCHAR},
#{phone, jdbcType=VARCHAR},
UserDao
package com.wtt.dao;
import com.wtt.model.UserDomain;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
public interface UserDao {
int insert(UserDomain record);
UserDomain selectByPrimaryKey(String id);
List
}
UserService
package com.wtt.service.user;
import com.github.pagehelper.PageInfo;
import com.wtt.model.UserDomain;
import java.util.List;
/**
* Created by Administrator on 2018/4/19.
*/
public interface UserService {
int addUser(UserDomain user);
PageInfo
public UserDomain getUser(String id);
}
package com.wtt.service.user.impl;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.wtt.dao.UserDao;
import com.wtt.model.UserDomain;
import com.wtt.service.user.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* Created by Administrator on 2017/8/16.
*/
@Service(value = "userService")
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;//这里会报错,但是并不会影响
@Override
public int addUser(UserDomain user) {
return userDao.insert(user);
}
public UserDomain getUser(String id) {
return userDao.selectByPrimaryKey(id);
}
/*
* 这个方法中用到了我们开头配置依赖的分页插件pagehelper
* 很简单,只需要在service层传入参数,然后将参数传递给一个插件的一个静态方法即可;
* pageNum 开始页数
* pageSize 每页显示的数据条数
* */
@Override
public PageInfo
//将参数传给这个方法就可以实现物理分页了,非常简单。
PageHelper.startPage(pageNum, pageSize);
List
PageInfo result = new PageInfo(userDomains);
return result;
}
}
RedisDelayMemages
package com.wtt.service.user;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.wtt.utils.CalendarUtils;
import com.wtt.utils.RedisUtils;
@Service
public class RedisDelayMemages {
@Autowired
private RedisUtils redis;
String key = "orderSet";
//定义一个生产者
public void productionDelayMemssage() {
for(int i=0;i<=50;i++) {
//订单10秒超时
redis.addItem(key, CalendarUtils.getCurrentTimeInMillis(10), "10010"+String.format("%02d", i));
}
}
//定义一个消费者
public void consumeDelayMessage() {
redis.doFind(key);
}
}
RedisConfig
package com.wtt.redisconfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Protocol;
@Configuration
public class RedisConfig {
private Logger logger = LoggerFactory.getLogger(RedisConfig.class);
@Bean(name= "jedis.pool")
@Autowired
public JedisPool jedisPool(@Qualifier("jedis.pool.config") JedisPoolConfig config,
@Value("${jedis.pool.host}")String host,
@Value("${jedis.pool.port}")int port,
@Value("${jedis.pool.password}")String password) {
logger.info("缓存服务器的地址:"+host+":"+port);
return new JedisPool(config, host, port,Protocol.DEFAULT_TIMEOUT,password);
}
@Bean(name= "jedis.pool.config")
public JedisPoolConfig jedisPoolConfig (@Value("${jedis.pool.config.maxTotal}")int maxTotal,
@Value("${jedis.pool.config.maxIdle}")int maxIdle,
@Value("${jedis.pool.config.maxWaitMillis}")int maxWaitMillis) {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(maxTotal);
config.setMaxIdle(maxIdle);
config.setMaxWaitMillis(maxWaitMillis);
return config;
}
}
application.yml
server:
port: 8080
servlet:
path: /
spring:
datasource:
name: mysql_test
type: com.alibaba.druid.pool.DruidDataSource
#druid相关配置
druid:
#监控统计拦截的filters
filters: stat
driver-class-name: com.mysql.jdbc.Driver
#基本属性
url: jdbc:mysql://192.125.155.223:3306/mytest?useUnicode=true&characterEncoding=UTF-8&useSSL=true&allowMultiQueries=true
username: root
password: root
#配置初始化大小/最小/最大
initial-size: 20
min-idle: 20
max-active: 20
#获取连接等待超时时间
max-wait: 60000
#间隔多久进行一次检测,检测需要关闭的空闲连接
time-between-eviction-runs-millis: 60000
#一个连接在池中最小生存的时间
min-evictable-idle-time-millis: 300000
validation-query: SELECT 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
#打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
pool-prepared-statements: false
max-pool-prepared-statement-per-connection-size: 20
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.wtt.model
#pagehelper
pagehelper:
helperDialect: mysql
reasonable: true
supportMethodsArguments: true
params: count=countSql
returnPageInfo: check
#redis服务器地址
jedis:
pool:
host: 127.0.0.1
port: 6379
password: 123456
config:
maxTotal: 100
maxIdle: 10
maxWaitMillis: 100000
UserController
package com.wtt.controller;
import com.github.pagehelper.PageHelper;
import com.wtt.model.UserDomain;
import com.wtt.service.user.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
/**
* Created by Administrator on 2017/8/16.
*/
@Controller
@RequestMapping(value = "/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping(value="/get/{id}",method=RequestMethod.GET)
public @ResponseBody UserDomain getUser(@PathVariable("id")String id){
UserDomain user = userService.getUser(id);
return user;
}
@ResponseBody
@GetMapping("/hello")
public String hello(){
return "hello";
}
@ResponseBody
@PostMapping("/add")
public int addUser(UserDomain user){
return userService.addUser(user);
}
@ResponseBody
@GetMapping("/all")
public Object findAllUser(
@RequestParam(name = "pageNum", required = false, defaultValue = "1")
int pageNum,
@RequestParam(name = "pageSize", required = false, defaultValue = "10")
int pageSize){
return userService.findAllUser(pageNum,pageSize);
}
}
Demo //模拟3000并发
package com.wtt;
import java.util.concurrent.CountDownLatch;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
public class Demo implements Runnable{
public static int failseNum;
private final CountDownLatch countDownLatch ;
RestTemplate restTemplate = new RestTemplate();
public Demo(CountDownLatch countDownLatch) {
super();
this.countDownLatch = countDownLatch;
}
public void run() {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//do
try {
ResponseEntity
System.out.println("接收到是数据:"+result);
}catch(Exception e) {
failseNum++;
System.out.println("失败次数:"+failseNum);
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("开始模拟调用......");
CountDownLatch countDownLatch = new CountDownLatch(1);
//运行模拟并发请求之后,发现以我服务提供的连接池配置来看基本支撑不住300个并发量:
for(int i=300;i>0;i--){
new Thread(new Demo(countDownLatch)).start();
}
countDownLatch.countDown();
System.out.println("结束模拟调用......");
}
}
Application
package com.wtt;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.wtt.dao")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
application.properties
############################### rabbitmq ########################
spring.application.name=spirng-boot-rabbitmq-example
spring.rabbitmq.virtual-host=/admin
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.listener.concurrency=5
spring.rabbitmq.listener.max-concurrency=10
spring.rabbitmq.listener.prefetch=1
spring.rabbitmq.listener.transaction-size=1
Send
package com.mq;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeoutException;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class Send implements Runnable {
private final CountDownLatch countDownLatch;
// 队列名称
private final static String QUEUE_NAME = "recvqueue";//recvqueue
public Send(CountDownLatch countDownLatch) {
super();
this.countDownLatch = countDownLatch;
}
public static void main(String[] argv) throws java.io.IOException, TimeoutException {
CountDownLatch countDownLatch = new CountDownLatch(1);
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(100);
for (int i = 1000; i > 0; i--) {
/* try {
Thread.sleep(Math.round(10));
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}*/
fixedThreadPool.execute(new Send(countDownLatch));
}
countDownLatch.countDown();
fixedThreadPool.shutdown();
}
public void run() {
try {
countDownLatch.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//发送消息
/**
* 创建连接连接到MabbitMQ
*/
ConnectionFactory factory = new ConnectionFactory();
// 设置MabbitMQ所在主机ip或者主机名
factory.setHost("127.0.0.1");
//设置RabbitMQ相关信息
factory.setUsername("guest");
factory.setPassword("guest");
factory.setPort(5672);
factory.setVirtualHost("/admin");
try {
// 创建一个连接
Connection connection = factory.newConnection();
// 创建一个频道
Channel channel = connection.createChannel();
// 指定一个队列
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 发送的消息
String message = "1001";
// 往队列中发出一条消息
channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
// 关闭频道和连接
channel.close();
connection.close();
} catch (Exception e) {
// TODO: handle exception
}
}
}
Customer
package com.mq;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import com.rabbitmq.client.AMQP;
//纯java消费者代码
public class Customer {
private final static String QUEUE_NAME = "recvqueue";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置RabbitMQ相关信息
factory.setHost("127.0.0.1");
factory.setUsername("guest");
factory.setPassword("guest");
factory.setPort(5672);
factory.setVirtualHost("/admin");
//创建一个新的连接
Connection connection = factory.newConnection();
//创建一个通道
Channel channel = connection.createChannel();
//声明要关注的队列 参数要和发送队列保持一致
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
System.out.println("Customer Waiting Received messages");
//DefaultConsumer类实现了Consumer接口,通过传入一个频道,
// 告诉服务器我们需要那个频道的消息,如果频道中有消息,就会执行回调函数handleDelivery
Consumer consumer = new DefaultConsumer(channel) {
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println("Customer Received '" + message + "'");
}
};
//自动回复队列应答 -- RabbitMQ中的消息确认机制
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}