但行好事
莫论前程❤

金融项目中数据类型的选择

挚爱阅读(11)

​ 最近在面试的时候,因为所写的简历上有一个金融方向的项目,面试官问了一句金融项目关于金额的数据存储使用什么数据类型.当时有些懵,因为我做的项目用的是hbase非关系型数据库.只有一种数据类型就是字符串.

​ 回家后经过一番谷歌现在做一下笔记,以供大家参考.

​ 在讲述前先来做一个小实验,提高一下大家的兴趣.

public class Test {
        public static void main(String[] args) {
            double a=0.03;
            double b=0.02;
            double c=a-b;
            System.out.println(c);
    }
}
# 输出结果为:
0.009999999999999998

这就是为什么面试官会问这个问题的原因所在.如果你回答的是float或者double,那恭喜你准备回家吧.

如果觉得不相信的同学可以动手在编辑器上实现并运行一下你会发现结果居然是0.009999999999999998,因为float与double都是浮点数想让float或者double精确地表示0.1(或者10的任何负数次方值)是不可能的,浮点数参与的运算通常伴随着因为无法精确表示而进行的近似或舍入, 所以导致结果会有丝毫的偏差,而涉及金额的计算是绝对不予许存在偏差的

这种舍入错误产生的原因是浮点数实际上是用二进制系统实现的,而分数1/10在二进制系统中没有精确的表示,其道理就如同在十进制系统中无法精确表示1/3一样;再比如0.5在二进制系统中有精确表示,而0.55在二进制系统中没有精确表示。

在java中使用BigDecimal来解决.

当使用double进行商业运算时,double计算会丢失精度时。可以使用BigDecimal进行计算。

import java.math.BigDecimal;
import org.junit.Test;
public class TestBigDecimal{
  @Test
  public void test(){
      double a=0.1;
      double b=0.2;
      System.out.println(a+b);

      BigDecimal a1=new BigDecimal("0.1");

      BigDecimal b1=new BigDecimal("0.2");

      System.out.println(a1+b1);
      BigDecimal c1=new BigDecimal("0.23574");
      a1=a1.add(c1);
      a1=a1.setScale(1,BigDecimal.ROUND_DOWN);
      System.out.println(a1);
  }
}

//输出结果如下:
0.30000000000000004

0.3
0.4

double的加减无法精确计算出0.3,而使用BigDecimal却可以。

当然,如果直接将double传给BigDecimal,你会发现不但无法解决精度问题,反而对精度进行了补全。所以,为确保精度,我们将String传给它。

  • BigDecimal.setScale() // 方法用于格式化小数点
setScale(1)                        //      表示保留一位小数,默认用四舍五入方式 
setScale(1,BigDecimal.ROUND_DOWN)    //      直接删除多余的小数位,如2.35会变成2.3 
setScale(1,BigDecimal.ROUND_UP)      //      进位处理,2.35变成2.4 
setScale(1,BigDecimal.ROUND_HALF_UP)  //     四舍五入,2.35变成2.4
setScaler(1,BigDecimal.ROUND_HALF_DOWN) //   四舍五入,2.35变成2.3,如果是5则向下舍

.scale()  取精度值,即小数点后位数(注:BigDecimal可以通过setScale来提高精度,只要新设的值比原来的大!

​ BigDecimal也可以通过setScale来降低精度。因为新设的值比原来的小,所以必须保证原来数值的该位小数点后面都是0,只有这样才可以设比原来小的精度。

​ 例:原来的值是:4.1235648,想把scale设为小于7为都会出错的,如果原来的值是:4.1235000,把scale设为小于4位会出错,而设为4、5、6、7都没有问题,设得更大,肯定不会出错)

  • add(BigDecimal)         BigDecimal对象中的值相加,然后返回这个对象。
  • subtract(BigDecimal) BigDecimal对象中的值相减,然后返回这个对象。
  • multiply(BigDecimal)  BigDecimal对象中的值相乘,然后返回这个对象。
  • divide(BigDecimal)     BigDecimal对象中的值相除,然后返回这个对象。

BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)

BigDecimal mData = new BigDecimal("9.655").setScale(2, BigDecimal.ROUND_HALF_UP);

----结果:----- 
mData=9.66
  • toString()                 将BigDecimal对象的数值转换成字符串。
  • doubleValue()          将BigDecimal对象中的值以双精度数返回。
  • floatValue()             将BigDecimal对象中的值以单精度数返回。
  • longValue()             将BigDecimal对象中的值以长整数返回。
  • intValue()               将BigDecimal对象中的值以整数返回。

金融系统MySQL数据库规范

  • 项目中所涉及到的表结构都需要增加创建时间和修改时间
  • 项目中所涉及到的用户表中的id,都使用user_id
  • 项目中所涉及到的记录的状态信息,必须使用枚举类型(对应java枚举类)
  • 项目中所涉及到货币单位表示都使用decimal类型,并且保留8位小数
  • 项目中所涉及到的账户流水表,请使用_record作为后缀,如果是普通操作日志或者系统记录日志请使用_log后缀

关键字段定义如下:

字段名类型备注
idbigint(20)自增长
create_timedatetime创建时间
update_timedatetime修改时间
user_idbigint(20)用户id
amountdecimal(20,8)货币类型金额
methodvarchar(64)支付方式
statusvarchar(64)状态信息 说明:PUBLIC 正常 DELETED 删除 LOCKED 锁定

时间处理相关
create_time datetime NOT NULL COMMENT ‘创建时间’,
update_time datetime NOT NULL COMMENT ‘本记录最后修改时间’,

在mybatis的*Mapper.xml文件中统一使用now()函数,如下:
<sql id="Base_Column_List">
    id, openid, mobile, user_id, create_time, update_time
</sql>

<insert id="insert" parameterType="UserWechatPo">
    INSERT INTO user_wechat
    (openid, mobile,user_id, create_time, update_time)
    VALUES
    (#{openid}, #{mobile}, #{userId}, now(), now())
</insert>

<update id="update" parameterType="WechatMessage">
    UPDATE
    wechat_message
    <set>
        <if test="status != null">
            status = #{status},
        </if>
        update_time = now()
    </set>
    WHERE id = #{id}
</update>

Decimal详解

Decimal为专门为财务相关问题设计的数据类型。DECIMAL从MySQL 5.1引入,列的声明语法是

DECIMAL(M,D)

在MySQL 5.1中,参量的取值范围如下:

  • M是数字的最大数(精度)。其范围为1~65(在较旧的MySQL版本中,允许的范围是1~254),M 的默认 值是10。
  • D是小数点右侧数字的数目(标度)。其范围是0~30,但不得超过M。

说明:float占4个字节,double占8个字节,decimail(M,D)占M+2个字节。

如DECIMAL(5,2) 的最大值为9999.99,因为有7 个字节可用。

能够解决数据的范围和精度的问题。

定时任务常见实现方式

挚爱阅读(219)

本文将介绍三种常用的实现定时任务的方法,希望能给大家在日常项目中带来一些启示和帮助。

一、Timer类

在java中一个完整的定时任务需要由Timer和TimerTask两个类配合完成。

  • 其中Timer是一种工具,线程用其安排在后台线程中执行的任务,可安排任务执行一次或者定期重复执行;

  • 而TimerTask是由Timer安排执行一次或者重复执行的任务。

Timer中提供了四个构造方法:

  • schedule(TimerTask task, Date time) ——安排在指定的时间执行指定的任务;
  • schedule(TimerTask task, Date firstTime, long period) ——安排指定的任务在指定的时间开始进行重复的固定延迟执行;
  • schedule(TimerTask task, long delay) ——安排在指定延迟后执行指定的任务;
  • schedule(TimerTask task, long delay, long period) ——安排指定的任务在指定的延迟后开始进行重复的固定速率执行。
1、在指定延迟时间执行定时任务
public class TestUserTimer {
    public static void main(String[] args) {
        System.out.println("任务开始" +  new Date());
        timer();
    }

//    方法一;设定指定任务task在指定时间time执行schedule(TimeerTask,Date time)
    private static void timer(){
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("2秒后执行一次该任务" + new Date());
            }
        },2000);   //单位毫秒ms,延迟2秒后执行
    }
}

执行结果:

任务开始Thu May 31 14:37:33 CST 2018
2秒后执行一次该任务Thu May 31 14:37:35 CST 2018

2. 在指定时间执行定时任务
public class TestUserTimer {
    public static void main(String[] args) {
        System.out.println("指定的时间为 14:47");
        timer();
    }

//    方法二;在指定时间执行任务
    private static void timer(){
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.HOUR_OF_DAY,14);
        calendar.set(Calendar.MINUTE,47);
        calendar.set(Calendar.SECOND,0);
        final Date time  = calendar.getTime();
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                Date date = new Date(this.scheduledExecutionTime());
                System.out.println("14:47时执行一次该任务: " + date);
            }
        },time);
    }
}

运行结果为:

指定的时间为 14:47
14:47时执行一次该任务: Thu May 31 14:47:00 CST 2018

  • Date 日期类 在JDK1.0中,Date类是唯一的一个代表时间的类,但是由于Date类不便于实现国际化,所以从JDK1.1版本开始,推荐使用Calendar类进行时间和日期处理。这里简单介绍一下Date类的使用。
    • 输出格式: Thu May 31 14:47:00 CST 2018
    • 使用Date类代表指定的时间
    • Date date = new Date(this.scheduledExecutionTime());
  • Calendar 日历类
    • 从JDK1.1版本开始,在处理日期和时间时,系统推荐使用Calendar类进行实现。
    • Calender.HOUR 12小时制
    • Calender.HOUR_OF_DAY 24小时制
  • 注意: 如果延迟执行时间在当前时间之前,则立即执行
3. 在延迟指定时间后以指定的间隔时间循环执行定时任务
public class TestUserTimer {
    public static void main(String[] args) {
        System.out.println("任务开始");
        timer();
    }

//    在延迟指定时间后以指定的间隔时间循环执行定时任务
    private static void timer(){
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("执行一次该任务: " + new Date());
            }
        },2000,1000);
    }
}

输出结果:

任务开始
执行一次该任务: Thu May 31 15:35:49 CST 2018
执行一次该任务: Thu May 31 15:35:50 CST 2018
执行一次该任务: Thu May 31 15:35:51 CST 2018
执行一次该任务: Thu May 31 15:35:52 CST 2018
执行一次该任务: Thu May 31 15:35:53 CST 2018

4、Timer类小结

Timer类是一种简单实用的实现定时任务的方法,然而它存在着自身的缺陷:

(1)Timer对调度的支持是基于绝对时间而不是相对时间,因此它对于系统时间的改变非常敏感

(2)Timer线程是不会捕获异常的,如果TimerTask抛出未检查的异常则会导致Timer线程终止,同时Timer也不会重新恢复线程的执行,它会错误的认为整个Timer线程都会取消,已经被安排但尚未执行的TimerTask也不会再执行了,新的任务也不能被调度。因此,如果TimerTask抛出未检查的异常,Timer将会产生无法预料的行为。

二、ScheduledExecutorService

​ ScheduledExecutorService是基于相对时间Timer内部是单一线程

ScheduledThreadPoolExecutor内部是个线程池,可以支持多个任务并发执行

ScheduledExecutor的设计思想是每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发的,相互之间不会受到干扰;只有当任务的时间到来时,ScheduledExecutor才会真正启动一个线程。

1、Timer的第一个缺陷

public class TestUserTimer {
    private Timer timer;
    private long  start;

    public TestUserTimer() {
        this.timer = new Timer();
        start = System.currentTimeMillis();
    }

    /**
     * 延迟一秒执行调度任务,输出后睡眠4 秒
     */
    public void timerOne(){
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("任务1开始执行: " + (System.currentTimeMillis()- start) +"毫秒");
                try {
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },1000);
    }

    public void timerTwo(){
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("任务2开始执行: " + (System.currentTimeMillis()-start) + "毫秒");
            }
        },3000);
    }
    public static void main(String[] args) {
        TestUserTimer testUserTimer = new TestUserTimer();
        System.out.println("任务开始: " + testUserTimer.start);
        testUserTimer.timerOne();
        testUserTimer.timerTwo();
    }
}

运行结果:

任务开始: 1527753568049
任务1开始执行: 1007毫秒
任务2开始执行: 5007毫秒

按照设想,任务1与开始时间间隔为1秒,而任务2与开始时间的时间间隔为3秒。然而,由于Timer在执行定时任务时只会创建一个工作线程,当工作线程因为某种原因而导致线程任务执行时间过长,超过了两个任务的间隔时间,则会出现以上情况。

  • 上述代码说明Timer类是单线程的.

使用ScheduledExecutorService优化:

public class TestScheduledExecutorTimer {

    private ScheduledExecutorService  schedule;
    private long  start;

    public TestScheduledExecutorTimer() {
        this.schedule =  Executors.newScheduledThreadPool(2); //执行器创建预期的线程池
        start = System.currentTimeMillis();
    }


    public void timerOne(){
        schedule.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务开始执行,与开始时间的时间间隔为: " +(System.currentTimeMillis() - start) + "毫秒");
                try {
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },1000, TimeUnit.MILLISECONDS);
    }

    public void timerTwo(){
        schedule.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2开始执行: " + (System.currentTimeMillis()-start) + "毫秒");
            }
        },2000,TimeUnit.MILLISECONDS);
    }

    public static void main(String[] args) {
        TestScheduledExecutorTimer testScheduledExecutorTimer = new TestScheduledExecutorTimer();
        System.out.println("任务开始: " + testScheduledExecutorTimer.start);
        testScheduledExecutorTimer.timerOne();
        testScheduledExecutorTimer.timerTwo();
    }
}

输出结果:

任务开始: 1527760168681
任务开始执行,与开始时间的时间间隔为: 1004毫秒
任务2开始执行: 2005毫秒

  • 上述代码说明ScheduledExecutorService是多线程并行的

2.Timer的第二个缺陷

public class TestTimerDemo {

    private Timer timer;
    private long  start;

    public TestTimerDemo() {
        this.timer = new Timer();
        start = System.currentTimeMillis();
    }


    public void timerOne(){
         timer.schedule(new TimerTask() {
        @Override
            public void run() {
               throw  new RuntimeException();
            }
        },1000);
    }

    public void timerTwo(){
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("任务2开始执行: " + new Date());
            }
        },3000);
    }

    public static void main(String[] args) {
        TestTimerDemo timerDemo = new TestTimerDemo();
        System.out.println("任务开始: " + timerDemo.start);
        timerDemo.timerOne();
        timerDemo.timerTwo();
    }
}

输出结果:

任务开始: 1527761009205
Exception in thread “Timer-0” java.lang.RuntimeException
at com.sojson.test.TestTimerDemo$1.run(TestTimerDemo.java:45)
at java.util.TimerThread.mainLoop(Timer.java:555)
at java.util.TimerThread.run(Timer.java:505)

  • 上述代码表名
    • timerOne抛出异常,而timerTwo并没有执行。

使用ScheduledExecutorService优化:

public class TestScheduledExecutorTimer {

    private ScheduledExecutorService  schedule;
    private long  start;

    public TestScheduledExecutorTimer() {
        this.schedule =  Executors.newScheduledThreadPool(2); //执行器创建预期的线程池
        start = System.currentTimeMillis();
    }

    /**
     * 延迟一秒执行调度任务,输出后睡眠4 秒
     */
    public void timerOne(){
        schedule.schedule(new Runnable() {
            @Override
            public void run() {
                throw new RuntimeException();
            }
        },1000, TimeUnit.MILLISECONDS);
    }

    public void timerTwo(){
        schedule.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2开始执行: " + (System.currentTimeMillis()-start) + "毫秒");
            }
        },2000,TimeUnit.MILLISECONDS);
    }

    public static void main(String[] args) {
        TestScheduledExecutorTimer testScheduledExecutorTimer = new TestScheduledExecutorTimer();
        System.out.println("任务开始: " + testScheduledExecutorTimer.start);
        testScheduledExecutorTimer.timerOne();
        testScheduledExecutorTimer.timerTwo();
    }
}

输出结果:

任务开始: 1527761726364
任务2开始执行: 2025毫秒

  • 上述代码说明:ScheduledExcetorService是多线程的,线程之间互不影响.

3.ScheduledExecutorService小结

可以看到针对Timer类存在的两个缺陷,ScheduledExecutorService可以很好地解决,其思路主要在于每一个被调度的任务都是由线程池中的一个线程去执行,任务之间是并发的,不会互相干扰。

三、Quartz

​ Quartz是一个完全由Java编写的开源作业调度框架,为在Java应用程序中进行作业调度提供了简单强大的机制。与前两种方法相比,Quartz对于定时的配置更为丰富,实际应用的场景多。

Quartz最重要的3个基本要素:

  • Scheduler——调度器,所有的调度都由它控制;
  • Trigger——定义触发的条件,包括SimpleTrigger和CronTrigger等;
  • JobDetail & Job——JobDetail定义的是任务数据,而真正的执行逻辑在Job中。Scheduler的每次执行都会根据JobDetail创建一个新的Job实例。
详情请看Quartz使用教程.