1、集合类不安全

1.1、故障现象

java.util.ConcurrentModificationException
集合类不安全导致的并发修改异常

1.2、导致原因

并发争抢修改导致,

1.3、解决办法

  • new Vector();
    • 线程安全的,但为保留类,基本废弃
  • Collections.synchronizedList(new ArrayList<>());
  • new CopyOnWriteArrayList();
    • 写时复制,读写分离,

CopyOnWrite容器即写时复制容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将容器Object[] 进行Copy,复制出一个新的容器Object[] newElements,然后往新的容器Object[] newElements里添加元素,添加完元素之后,再讲原容器的引用指向新的容器setArray(newElements); 。这样做的好处就是可以对CpoyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,所以CpoyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

2、分布式OpenFeign远程调用的坑

2.1、Feign远程调用丢失请求头

在微服务项目中,我们做了单点登录,在项目使用feign 调用另一个模块的远程服务时,发现提示无权限调用。
image.png
解决方案:
加上自定义拦截器,在拦截器中,获取老请求的请求头,设置到新请求中。

  1. //解决方法
  2. @Configuration
  3. public class GuliFeignConfig {
  4. @Bean("requestInterceptor")
  5. public RequestInterceptor requestInterceptor(){
  6. //给容器中放一个RequestInterceptor 重写apply 在里面给请求设置上自己需要的请求头等信息
  7. return new RequestInterceptor() {
  8. @Override
  9. public void apply(RequestTemplate requestTemplate) {
  10. //1.RequestContextHolder 拿到刚进来的这个请求
  11. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  12. HttpServletRequest request = attributes.getRequest();//老请求
  13. if(request!=null){
  14. //同步请求头数据,Cookie
  15. String cookie = request.getHeader("Cookie");
  16. //给新请求同步老请求的cookie
  17. requestTemplate.header("Cookie",cookie);
  18. }
  19. System.out.println("feign 在远程调用之前先进行 RequestInterceptor.apply");
  20. }
  21. };
  22. }
  23. }

2.2、Feign异步远程调用,丢失上下文

image.pngimage.png

3、SpringBoot本地事务的坑

  1. /**
  2. * 同一个对象内的事务方法互调,被调用的方法事务设置默认失效,原因是绕过了代理对象(事务是使用代理对象来控制的)b和c做任何设置都是无效的,都是和a公用一个事务
  3. * @Transactional(timeout = 30) // a 事务的所有设置会传播到和它公用一个事务的方法
  4. * public void a(){
  5. * // b和c做任何设置都是无效的,都是和a公用一个事务,相当于把b(), c()方法的代码拷贝过来
  6. * b();
  7. * c();
  8. * int a = 10/0; // a和b回滚,c不回滚
  9. * }
  10. * 解决:使用代理对象来调用事务方法
  11. * 1) 引入spring-boot-starter-aop, 引入aspectj
  12. * 2) @EnableAspectJAutoProxy(exposeProxy = true): 开启Aspectj动态代理功能,之后的所有动态代理都是使用Aspectj创建的(即使没有接口也可以创建动态代理)
  13. * 对外暴露代理对象
  14. * 3) 本类互调用代理对象 AopContext.currentProxy()
  15. */
  16. @Transactional(timeout = 30) // a 事务的所有设置会传播到和它公用一个事务的方法
  17. public void a(){
  18. OrderServiceImpl orderService = (OrderServiceImpl) AopContext.currentProxy();
  19. orderService.b(); // a事务, 会覆盖b中定义的事务属性如超时时间等
  20. orderService.c(); // 新事务,a的设置和c的设置彼此独立
  21. int a = 100/0; // a和b回滚,c不回滚
  22. }
  23. @Transactional(propagation = Propagation.REQUIRED, timeout = 2)
  24. public void b(){}
  25. @Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 20)
  26. public void c(){}

4、接口幂等性

接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的

4.1、哪些情况需要防止

对于业务中需要考虑幂等性的地方一般都是接口的重复请求,重复请求是指同一个请求因为某些原因被多次提交。导致这个情况会有几种场景:

  1. 前端重复提交:提交订单,用户快速重复点击多次,造成后端生成多个内容重复的订单。
  2. 接口超时重试:对于给第三方调用的接口,为了防止网络抖动或其他原因造成请求丢失,这样的接口一般都会设计成超时重试多次。
  3. 消息重复消费:MQ消息中间件,消息重复消费。
  4. 用户页面回退再次提交
  5. 微服务互相调用:由于网络问题,导致请求失败。feign触发重试机制
  6. 其他业务情况

对于一些业务场景影响比较大的,接口的幂等性是个必须要考虑的问题,例如金钱的交易方面的接口。否则一个错误的、考虑不周的接口可能会给公司带来巨额的金钱损失,那么背锅的肯定是程序员自己了。

4.2、什么情况下需要幂等

以SQL为例,有些操作时天然幂等的基本SELECT、UPDATE、DELETE、INSERT。CRUD操作具备幂等性。
但是UPDATE、INSERT某些情况下不是幂等的

  1. UPDATE tab1 SET col1 = col1+1 WHERE col2 = 2,每次执行的结果都会发生变化,不是幂等的
  2. INSERT into user(userid,name) values(1,‘a’) 如果userid不是主键,可以重复,那么多次进行业务操作,数据会新增多条,不具备幂等性。

    4.3、解决方案

  3. token机制

    1. 服务端提供了发送Token的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取Token,服务器会把Token保存到redis中。
    2. 然后调用业务接口请求时,把token携带过去,一般放在请求头部。
    3. 服务器判断token是否存在redis中,存在表示第一次请求,然后删除token,继续执行业务。但是这种方式存在危险性。
    4. 先删除token还是后删除token
      1. 先删可能导致业务没有执行,重试还带上之前的token,请求不能执行
      2. 后删可能导致业务处理成功,但是服务闪断,出现超时,没有删除token,重试后执行两边
    5. token的获取、比较和删除三个操作必须时原子性的,要实现原子性可以在redis情况下使用lua脚本
  4. 各种锁机制
    1. 数据库悲观锁,悲观锁一般伴随事务一起使用,id字段一定是主键或者唯一索引,不然可能会造成锁表结果。
    2. 数据库乐观锁,乐观锁主要处理读多写少的问题这种情况适合在更新场景中,根据数据的version版本,去对比操作时数据带过来的version,如果version一样可以进行更新操作,否则更新失败。
    3. 业务层分布式锁,如果多个及其可能在同一时间同时处理相同的数据,比如多台机器定时任务都拿到了相同数据处理,我们就可以加分布式锁,处理完成后释放锁。获取到锁的必须先判断这个数据是否被处理过。
  5. 各种唯一约束
    1. 数据库唯一约束
      1. 插入数据应该按照唯一索引进行插入
      2. 这种机制是立通了数据库的主键唯一约束的特性,解决了insert场景时幂等问题
      3. 不过分布式情况下,数据必须落在同一个数据库和同同一个表中
    2. redis set防重
      1. 数据处理过了就不处理,比如百度网盘的秒传功能,把数据计算为一个MD5值,若该值已存在,就不操作
    3. 防重表
      1. 一个数据处理过了就在防重表中放一条数据表明这个数据已经操作过了,这个数据再过来就去防重表查询,有数据就不操作
    4. 全局的唯一请求id,可以在Feign中用
      1. 调用接口时,生成一个唯一id,redis讲数据保存到集合中(去重),存在即处理过。
      2. 可以使用nginx设置每一个请求的唯一id

[

](https://blog.csdn.net/FengYi1108/article/details/116615577)
[

](https://blog.csdn.net/FengYi1108/article/details/116615577)