springcloud是目前比较热门的微服务管理框架, 最近基于springcloud完成了订单服务框架的demo,在此记录一下框架的整体设计点和一些关键的技术点。
整体的架构如下所示:
限于时间关系,只编写了后端rest的代码框架,整体的模块分为:
1) 注册中心eureka:提供模块的注册、发现服务
2) seata server:提供分布式TCC服务,当用户服务发起购买商品流程时,由于需要保证扣减库存、生成订单在一个分布式事物中,通过该服务来保证
3) 网关服务gateway service:基于zuul完成用户操作的拦截、权限校验、登录等服务
4) 库存服务warehouse service:提供商品库存查询、库存扣减服务
5) 订单服务order service:提供生成订单、查询订单等服务
6) 支付服务pay service:提供订单支付服务
7) 用户服务user service:提供购买商品、支付商品等业务化操作
8) 物流服务:负责调用第三方物流接口生成物流订单、查询物流信息,该模块与支付模块之间通过MQ进行解耦
mysql相关的表:
#用户表
create table if not exists `user_table`(
id bigint(20) primary key not null auto_increment,
name varchar(100) not null,
password varchar(100) not null,
money bigint(20) not null,
version int(11) not null
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
#订单信息表
create table if not exists `order_info`(
id varchar(50) primary key not null,
userid bigint(20) not null,
commodity_id varchar(50) not null,
`count` int(11) not null,
total_price bigint(20) not null,
status varchar(20) not null,
create_time varchar(20),
delivery_time varchar(20),
complete_time varchar(20)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
#mq事务表
create table if not exists `mq_transaction`(
id varchar(50) primary key not null,
business varchar(50) primary key not null,
orderId varchar(50) primary key not null
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
为了保证商品信息查询、库存扣减的实时性,采用redis缓存商品库存信息,用户下单时直接更新redis中的库存,以保证高并发性能。
下面梳理一下关键的技术点:
基于zuul实现网关服务
网关服务的主要目的是实现用户权限校验与登录服务
添加@EnableZuulProxy注解开启网关服务:
@EnableFeignClients
@EnableZuulProxy
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
zuul配置文件:
zuul:
host:
connect-timeout-millis: 15000
socket-timeout-millis: 60000
sensitiveHeaders:
ignoredPatterns: /**/sso/** #以sso开头的路径忽略
routes:
user-service:
path: /user/**
serviceId: user-service #校验通过的流量重定向到user-service中
strip-prefix: false
自定义一个zuul过滤器,对请求进行拦截,校验用户权限,校验通过则放行,否则重定向到登录页面:
@Component
public class AccessFilter extends ZuulFilter {
@Value("${token.expire-time}")
private int tokenExpireTime;
@Autowired
private UserService userService;
//前置过滤器
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
HttpServletResponse response = ctx.getResponse();
String url = request.getRequestURL().toString();
String accessToken = null;
String authorization = request.getHeader("authorization");
if (StringUtils.isNotEmpty(authorization)) {
accessToken = StringUtils.substringAfter(authorization, "Bearer ");
}
//从cookie中获取token
Cookie[] cookies = request.getCookies();
Cookie tokenCookie = null;
if (null != cookies) {
for (Cookie cookie : cookies) {
if ("accessToken".equals(cookie.getName())) {
accessToken = cookie.getValue();
tokenCookie = cookie;
}
}
}
//是登录请求或者权限校验通过,则放行
if (url.contains("sso/loginPage") || url.contains("sso/login") || checkToken(accessToken)) {
ctx.setSendZuulResponse(true);
if (null != accessToken) {
ctx.addZuulRequestHeader("authorization", "Bearer " + accessToken);
}
//刷新token
if (null != tokenCookie) {
Long remainTime = JwtUtil.getRemainTimeInToken(accessToken);
//token过期前30秒内刷新token
if (remainTime > 0 && remainTime <= 30000) {
tokenCookie.setValue(JwtUtil.refreshToken(accessToken, tokenExpireTime));
//设置token过期时间
tokenCookie.setMaxAge(tokenExpireTime);
tokenCookie.setPath("/");
response.addCookie(tokenCookie);
}
}
ctx.setResponseStatusCode(200);
return null;
} else {
//没有权限,重定向到登录页面
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
try {
response.sendRedirect("/sso/loginPage?url=" + resolveGetUrl(request));
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
}
通过get请求访问时,请求参数包含在HttpServletRequest中,重定向url时,需要将请求参数解析出来重新拼接:
private String resolveGetUrl(HttpServletRequest request) {
String method = request.getMethod();
String url = request.getRequestURL().toString();
if ("GET".equals(method)) {
Map<String, String[]> parameterMap = request.getParameterMap();
List<String> list = new ArrayList<>();
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
String key = entry.getKey();
String[] values = entry.getValue();
for (String value : values) {
list.add(key + "=" + value);
}
}
if (!list.isEmpty()) {
url = url + "?" + StringUtils.join(list, "&");
}
}
return url;
}
实现的效果:
在浏览器中访问商品信息的接口
带着原始url重定向到登录页面:
登录成功之后,返回商品信息:
cookie中带有token信息,下次访问直接放行
设定cookie 30分钟过期,过期之后需要重新登录