• 财务系统重复付款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锁并不适用。

  • 相关阅读:
    MFC之绘制线条
    CDC类详解
    MFC之消息映射机制实现方法
    VS2008如何自动添加消息映射
    MFC框架程序剖析
    Visual Assist 相同内容高亮显示
    Win32 Console Application、Win32 Application、MFC三者之间的联系和区别
    win32应用程序创建流程
    BigDecimal最基础用法【转】
    html 高亮显示表格当前行【转】
  • 原文地址:https://www.cnblogs.com/umgsai/p/7161112.html
Copyright © 2020-2023  润新知