初步介绍
目前很多服务都是分布式开发的,就算自己做项目,练手,也都是采用的分模块,切分端口号的操作,那么这样就必然涉及一个远程调用的问题。那么一但涉及远程调用,就需要考虑一个性能以及安全的问题,本文就是初步讲解安全的问题
本文的一个整体流程
- 常规业务流程分析
- 具体每一步的业务
- 针对每一步遇到问题的解决
常规业务流程分析
- 我们做电商平台都知道一个流程,订单与发货模块,都是分开处理的,端口号是不一样的,这样导致的也就是我们只能跨域请求
- 常规的,也就是客户下单,然后后台发送一个数据到发货模块,然后发货系统发出商品
- 中间涉及访问数据库之类的操作
- 具体流程图我就不作出演示,业务很简单
数据库表
这里的数据库表只是简单模拟,没有正式开发的数据要求,id(订单ID),name(订单描述之类),status(订单状态),version(订单版本)
实体类
public class Order {
private int id;
private String name;
private int status;
private int version;
//省略getset方法
}
Mapper
public interface OrderMapper {
@Select("select * from table_order where id=#{id}")
Order querryById(int id);
@Insert("insert into table_order values(#{id},#{name},#{status},#{version})")
void insertOrder(Order order);
@Update("update table_order set status=#{status} where id=#{id}")
int updateOrder(Order order);
@Update("update table_order set version=#{version} where id=#{id}")
int updateOrderByVersion(Order order);
}
由于模仿项目,直接通过接口注解操作,方便
Service层
@Service
public class OrderServiceImpl implements OrderService {
private Logger logger= LoggerFactory.getLogger(getClass());
@Autowired
private OrderMapper orderMapper;
@Autowired
private InvokeService invokeService;
private final static String url="http://localhost:8080/deliver?id=";
@Override
public Order querryById(int id) {
Order order=orderMapper.querryById(id);
if (order!=null)
return order;
return null;
}
@Override
public void insertOrder(Order order) {
orderMapper.insertOrder(order);
}
@Override
@Transactional
public String sendOrder(Order order) {
int id=order.getId();
String status=invokeService.invoke(url,id);
order=new Order();
order.setStatus(1);
order.setId(id);
orderMapper.updateOrder(order);
return status;
}
}
由于是跨域请求,所以我们分离一个service出来,专门做跨域问题(invokeService)
invokeService
@Service
public class InvokeServiceImpl implements InvokeService {
@Autowired
private RestTemplate restTemplate;
@Override
public String invoke(String url, int id) {
String body = restTemplate.getForEntity(url + id, String.class).getBody();
return body;
}
}
跨域处理,端口和之前不一样哦
@RestController
public class DeliveController {
private Logger logger= LoggerFactory.getLogger(getClass());
@RequestMapping("/deliver")
public String deliver(int id){
logger.info("===========接受订单指令,准备发货");
try {
TimeUnit.SECONDS.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
return "-1";
}
return "1";
}
}
当前服务器Controller的代码就不贴出来了,就是一个调用service的代码,这里可以很明显的看出一个业务逻辑了
- 首先获取到订单信息
- 远程调用接口,以做其余数据的处理,以及发货处理
- 接受返回值,判断出货是否成功,然后修改数据库信息
- 因为涉及修改,所以增加了事务管理
第一步分析
- 以上代码,看上去没有任何的问题,也符合业务逻辑以及对数据库的修改,也增加了事务管理,好像没啥问题,那么以下的问题可以思考一下
- 因为增加了事务管理,所以需要保持事务的特性,我们在做跨域请求的时候,当前请求持有这个链接,导致数据库无法操作,而导致一直等待10s,性能以及用户体验很受伤
- 这个对于多线程,高并发而言,是否会出现多发货的问题呢?是否会出现一个严重BUG呢?
解析
- 针对第二个问题,我们可以考虑使用编程式事务处理,可以将我们的数据库操作和跨域请求分开,这样就不会导致我们一个独占链接的问题了
- 针对第三个问题,后面继续分析
初步优化
@Autowired
private TransactionTemplate transactionTemplate;
public String sendOrderByTemplate(Order order) {
int id=order.getId();
String status=invokeService.invoke(url,id);
transactionTemplate.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus transactionStatus) {
Order order1=new Order();
order1.setId(id);
order1.setStatus(Integer.parseInt(status));
orderMapper.updateOrder(order1);
return null;
}
});
return status;
}
通过以上的这个代码,就可以明显的优化第二个问题给我们带来的影响了,因为我们的跨域请求和数据库访问是分开的,是两个独立的代码,不会导致数据库链接资源被浪费掉。
但是,我们这个还是不能解决我们多线程的问题,因为只要是高并发,就会同一时间怼到我们方法上面来,就会同一时间去请求跨域信息,已导致“发货系统”出现问题,那么如何解决呢?
最终优化
public String sendOrderBySingle(Order order) {
int id=order.getId();
boolean flag=(boolean)transactionTemplate.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus transactionStatus) {
Order order1=new Order();
order1.setId(id);
order1.setStatus(3);
order1.setVersion(1);
return 1==orderMapper.updateOrderByVersion(order1);
}
});
String status="";
if (flag){
status=invokeService.invoke(url,id);
final String finalStatus = status;
transactionTemplate.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus transactionStatus) {
Order order1=new Order();
order1.setId(id);
order1.setStatus(Integer.parseInt(finalStatus));
orderMapper.updateOrder(order1);
return null;
}
});
}else {
logger.info("======================远程调用接口失败");
}
return status;
}
我们通过以上的方法,就可以很好的避免这个问题了,因为你有一个标志位以及版本信息存在,所有的请求,都要先走上面的方法,然后标志位就更新了,这样就不会导致所有请求都访问跨域了。
是不是感觉这样的逻辑理解起来很容易呢?下面总体总结一下
总结
- 整体业务逻辑都很清晰,只是考虑一个数据库链接资源以及高并发的问题
- 然后优化的方法,就采用事务分离,以及标志位和版本控制
- 其实,这里对第三种优化做一个解释,为什么会这样去想
分析
我们都知道,数据库有乐观锁与悲观锁吧。悲观锁很好理解就是锁住每一次的请求,而乐观锁就不会。
然后乐观锁的实现方式就是CAS算法,这个算法就是比较并且替换。我们首先比较版本号,如果版本号一样就操作,否则不会做出操作,因为一旦版本号不一样,就说明线程不同步了,就说明数据的有效信息不对了。
这就是第三种优化的核心思想,就是一个版本的控制
疑问
- 那么以上的方法就真的 安全了吗?考虑以下的问题
- 如果真的是高并发,同时怼到服务器上,虽然不会怼到跨域方法上面,但是会直接怼到本地数据库呀,有可能瞬间你的数据库就瘫痪了
- 如果你考虑加缓存,将数据放在缓存上,每次读取都走内容读,就很快,那么你考虑过缓存雪崩吗?或者刚好遇到缓存时间失效,过期了呢?
- 那么,针对以上问题又如何解释,后面文章会继续分析