财务系统重复付款case分析及解决方案

背景

2017年6月27日杨晨值日的过程中发现一个case,经查询发现天玑系统bug导致重复支付了三笔。

情景重现

本次问题的key在于数据库层面没有做幂等,导致连续两次一模一样的数据都可以插入成功。
重现问题的demo代码如下(struts)

public class Constant {
    public static List<Integer> list = new ArrayList<>();
    //模拟数据库
    static {
        list.add(1);
        list.add(2);
    }
}
  
public class TestAction extends ActionSupport {
    @Setter
    private int data;
    @Getter
    private List<Integer> list = Constant.list;
 
    public String showCase() throws InterruptedException {
        //模拟向数据库添加数据
        list.add(data);
        return SUCCESS;
    }
}

浏览器请求url为 http://localhost:8080/struts-test/showcase?data=3
由于添加数据时没有做幂等(list.add(data); ) 所以只要浏览器持续请求这个url,系统就会一直执行add方法,导致list中出现重复数据。

解决方案

数据库层做幂等

这种方法可以通过在数据库表中添加一个UK来解决,这样可以防止插入两条一模一样的数据。由于代码中是用list模拟数据库的,所以这种方式不便演示。

在前端解决

此次case中发送生成withdraw请求的来源是点击页面按钮。在点击了一次按钮之后,将按钮设为disabled就可以防止通过点击按钮发送第二次请求。这种方式实现简单,但是并不能从根本上解决问题,因为完全可以通过拼装url达到和点击按钮一样的效果。目前线上系统暂时按照这种方案解决,只能作为缓兵之计。

在应用层解决

简单来讲,就是在插入数据之前先判断数据是否存在。若存在则不允许插入,若不存在则执行插入操作。

方案对比

时间 复杂度 安全性 对现有系统改动
方案1
方案2
方案3

初步方案

经过对比以上三种备选方案,选用第三种方案最优。方案实现的demo代码如下

public class TestAction extends ActionSupport {
    //由于struts的action默认为原型模式,所以LOCK必须设为static
    private static final String LOCK = "LOCK";
     
    private int data;
    private List<Integer> list = Constant.list;
 
    public String showCase() throws InterruptedException {
        //模拟向数据库添加数据
        synchronized (LOCK) {
            if (list.contains(data)) {
                System.out.println("数据已存在,不可重复添加");
            } else {
                System.out.println("数据不存在,可以添加");
                //模拟一个耗时较长的操作
                Thread.sleep(5000);
                list.add(data);
            }
        }
        return SUCCESS;
    }
}

方案优化

由于所选择的方案在应用层加锁会导致多个请求的代码串行执行,可能会造成线程阻塞。所以可以缩小同步代码块的范围,只在关键部位串行执行。优化后的代码如下

public class TestAction extends ActionSupport {
     
    private static final String LOCK = "LOCK";
     
    private int data;
    private List<Integer> list = Constant.list;
 
    public String showCase() throws InterruptedException {
         
        if (list.contains(data)) {
            System.out.println("数据已存在,不可重复添加");
        } else {
            //模拟一个耗时较长的操作
            Thread.sleep(5000);
            synchronized (LOCK) {
                if (!list.contains(data)) {
                    System.out.println("数据不存在,可以添加");
                    list.add(data);
                } else {
                    System.out.println("数据已存在,不可重复添加");
                }
            }
        }
        return SUCCESS;
    }
}

存在的问题

synchronized锁只作用于单个jvm内部的对象,在分布式环境下无效。由于线上机器有两台,一台机器的jvm与另一台机器的jvm是相互独立的,所以这种情况下synchronized锁并不适用。

posted @ 2017-07-13 16:12  商商-77  阅读(807)  评论(0编辑  收藏  举报