程式碼回現 | 如何實現交易反欺詐?

VoltDB_China發表於2021-04-14

一、背景概述

交易反欺詐是VoltDB適用場景之一,是典型的事件驅動的業務,核心是攝取高頻的交易資料,並逐條對交易進行一系列複雜的反欺詐規則校驗,最終生成評判交易可疑度的分值,傳送給下游業務系統,觸發交易攔截動作。
反欺詐規則中涉及大量的透過分析歷史交易生成的指標項,在VoltDB中進行流式計算,可基於本地儲存的豐富的上下文資料對事件進行分析決策,使實時計算靠近上下文資料,獲得效能優勢。

二、例項回現

下面我們透過一個刷卡的應用,展示VoltDB是如何實現一個簡單的反欺詐用例的。為了讓示例程式碼更加簡潔,又能突出VoltDB的功能,這裡使用一個地鐵刷卡的場景替代金融交易(如信用卡刷卡),以避免引入過多專業的金融業務知識。同時一個繁忙地鐵系統產生的交易吞吐量不可小覷,定義的反欺詐規則也更容易理解。
可以透過這個連結來訪問詳細的程式碼
在這個應用中,模擬如下幾個場景:

  1. 多輛列車在地鐵站點之間執行,生成列車進站事件。透過這個場景可以瞭解,如何將資料釋出到VoltDB Topic中,以及如何消費Topic中的資料。
  2. 公交卡充值操作。透過這個場景,可以瞭解,如何使用一個包含自定義業務規則的procedure來處理Topic中的資料,同時使用Stream物件將資料匯出到Topic中,並透過檢視對Stream中的資料流進行統計,生成實時的統計報表。檢視會逐條統計Stream中的流資料,將處理結果儲存到檢視中,是VoltDB實現流式計算的方式之一。
  3. 乘客刷卡乘車,生成高頻交易資料。透過這個場景,可以瞭解,如何使用VoltDB資料庫客戶端api直接運算元據表(區別與將資料傳送到Topic中),儲存交易資料。如何透過VoltDB的java procedure定製反欺詐校驗規則,並呼叫java procedure進行交易校驗和反欺詐行為。
    讓我們來具體瞭解一下,在VoltDB中執行這個用例的過程。

2.1準備工作

1. 啟用VoltDB Topic功能
VoltDB提供一個統一的配置檔案,主要的特性都可以在其中進行定義,如:持久化、高可用、安全性等等,這裡主要介紹與案例相關的VoltDB Topic功能。如下配置開啟了Topic服務,並在伺服器上開啟埠9999,用於接受客戶端發來的訊息。

  <Topics enabled="true">
        <properties>
            <property name="port">9999</property>
            <property name="group.initial.rebalance.delay.ms">0</property>
            <property name="retention.policy.threads">1</property>
        </properties>
        <profiles>
            <profile name="retain_compact">
                <retention policy="compact" limit="2048" />
        </profile>
        </profiles>
    </Topics>

2.根據特定配置檔案啟動VoltDB
3.建立Topic,Topic的用途後面的程式碼分析中提到

CREATE Topic TRAINTOPIC execute procedure train_events.insert;
CREATE TOPIC RECHARGE execute procedure RechargeCard;
CREATE TOPIC using stream CARD_ALERT_EXPORT properties(topic.format=avro);
create topic using stream FRAUD properties(topic.format=avro,consumer.keys=TRANS_ID);

4.建立資料表
在處理實時事件流時,可以充分利用底層的資料庫引擎,充分利用本地關係型資料進行資料分析,得到反欺詐業務指標。在本例中將建立如下資料表和檢視(省略具體DDL)
在這裡插入圖片描述
5.初始化資料
透過VoltDB的資料匯入功能,從csv檔案中初始化站點和列車

csvloader --file $PROJ_HOME/data/redline.csv --reportdir log stations
csvloader --file $PROJ_HOME/data/trains.csv --reportdir log trains

2.2 程式碼分析-列車執行

在這個場景中,客戶端模擬8輛列車在17個站點之間執行,產生進站事件併傳送到Topic。由於設定的列車進出站時間比較短(微秒為單位),所以會產生高頻事件流。
在服務端,VoltDB完成:
1.訊息接收
2.消費訊息
3.將列車進站事件記錄到資料庫中
在客戶端,透過java類TrainProducer生成多輛列車進站事件,並將事件傳送到VoltDB Topic中。TrainProducer的執行命令如下:

java metro.pub.TrainProducer localhost:9999 TRAINTOPIC 8

TrainProducer類接收四個引數:

  1. .指定接收列車進站和離站事件的VoltDB伺服器埠。這裡假設在同一臺機器上執行client程式碼和VoltDB,而前面在VoltDB配置檔案中我們已經指定Topic的監聽埠是9999。
  2. 指定VoltDB broker
  3. 指定資料傳送的Topic名稱。
  4. 指定要模擬的列車數量。

分析一下TrainProducer的主要方法,main方法生成10個執行緒,每50毫秒執行一次publish()方法,將列車進出站時間傳送到Topic“TRAINTOPIC”中。

public static void main(String[] args) {
        ScheduledExecutorService EXECUTOR = Executors.newScheduledThreadPool(10);
        TrainProducer producer = new TrainProducer(args[0], args[1], Integer.parseInt(args[2]));
        System.out.println("Scheduling trains");
        EXECUTOR.scheduleAtFixedRate (
                () -> {
                    producer.publish(producer.getNewEvents());
                }, 1, 50, MILLISECONDS);
    }

跟蹤程式碼找到producer的定義,它其實就是原生的KafkaProducer,所以可以看到VoltDB Topic完全相容kafka api。而brokers即是main方法中的傳參localhost:9999,因此上面producer.getNewEvents()方法生成的資料將被髮送到VoltDB Topic中。

private Producer<String, TrainEvent> createProducer() {
        Properties props = new Properties();
        props.put("bootstrap.servers", brokers);
        props.put("acks", "all");
        props.put("retries", 0);
        props.put("batch.size", 16384);
        props.put("linger.ms", 1);
        props.put("buffer.memory", 33554432);
        props.put("key.serializer",
           "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer",
           "metro.serde.TrainEventSer");
        Producer<String, TrainEvent> producer = new KafkaProducer
           <String, TrainEvent>(props);
        return producer;
    }

Publish方法所傳送的訊息由producer.getNewEvents()方法生成。有必要提前看一下Stations類,其中定義了17個火車站點,包括每個站點的到下一個站點的執行時間(Station.nextStnDuration)和本站點停車時間(Station.stnWaitDuration),時間以微秒為單位。所有列車將依次在這些站點中執行。

 static HashMap<Integer, Station> idToStationMap = new HashMap<>();
    static {
        idToStationMap.put(1, new Station(1, 1200000, 450000));
        idToStationMap.put(2, new Station(2, 1050000, 250000));
        idToStationMap.put(3, new Station(3, 850000, 300000));
        idToStationMap.put(4, new Station(4, 900000, 350000));
        idToStationMap.put(5, new Station(5, 500000, 260000));
        idToStationMap.put(6, new Station(6, 950000, 190000));
        idToStationMap.put(7, new Station(7, 450000, 130000));
        idToStationMap.put(8, new Station(8, 200000, 280000));
        idToStationMap.put(9, new Station(9, 200000, 110000));
        idToStationMap.put(10, new Station(10, 450000, 300000));
        idToStationMap.put(11, new Station(11, 550000, 200000));
        idToStationMap.put(12, new Station(12, 550000, 200000));
        idToStationMap.put(13, new Station(13, 800000, 150000));
        idToStationMap.put(14, new Station(14, 950000, 100000));
        idToStationMap.put(15, new Station(15, 1000000, 130000));
        idToStationMap.put(16, new Station(16, 1200000, 220000));
        idToStationMap.put(17, new Station(17, 1500000, 500000));
}
   public static class Station {
        public final int stationId;
        public final int nextStnDuration;
        public final int stnWaitDuration;
        public Station(int stationId, int nextStnDuration, int stnWaitDuration) {
            this.stationId = stationId;
            this.nextStnDuration = nextStnDuration;
            this.stnWaitDuration = stnWaitDuration;
        }
    }

所以getNewEvents主要的邏輯是首先隨機設定列車從任意站點出發,然後呼叫next()根據系統當前時間和站點的Station.nextStnDuration、Station.stnWaitDuration來判斷每輛列車目前執行到哪個站點,如果next返回的LastKnownLocation物件有變化,則判斷列車已進入下一站,將列車進站事件trainEvent放到records中,用於傳送給Topic。(注:列車排程不是本樣例的重點,因此next方法不會考慮列車的衝突問題,它假設站點之間由足夠多的軌道,可以供多個列車並行)。

public List<TrainEvent> getNewEvents() {
        ArrayList<TrainEvent> records = new ArrayList<>();
        for(TrainEvent trainEvent : idToTrainMap.values()) {
            LastKnownLocation prevLoc = trainEvent.location;
            LastKnownLocation curLoc = next(prevLoc, LocalDateTime.now());
            if(!prevLoc.equals(curLoc)) {
                trainEvent = new TrainEvent(trainEvent.trainId, curLoc);
                idToTrainMap.put(trainEvent.trainId, trainEvent);
                records.add(trainEvent);
            }
        }
        return records;
    }

Topic TRAINTOPIC定義如下,train_events.insert是VoltDB為表建立的預設儲存過程,命名規則為[tablename].insert。Topic與儲存過程連用,表示儲存過程train_events.insert消費該Topic TRAINTOPIC中的trainEvent資料,並寫入train_events表中。

CREATE Topic TRAINTOPIC execute procedure train_events.insert;

2.23 程式碼分析-公交卡充值

在這個場景中,客戶端將完成充值訊息傳送。
在服務端,VoltDB完成:

  1. 訊息接收
  2. 消費訊息
  3. 使用自定義邏輯處理訊息 將充值資料更新到資料庫中
  4. 生成充值訊息,並將資料寫入stream物件中
  5. 基於stream物件建立檢視,來生成實時的充值統計報表
  6. 將stream中的充值訊息釋出到Topic中,供後續(VoltDB之外的)資料處理邏輯進行消費。例如被spark消費,由於進行後續的批處理邏輯。

在客戶端透過執行java類CardsProducer,首先初始化公交卡記錄,並將記錄寫入資料庫表中。然後隨機生成卡片充值事件,傳送事件到Topic RECHARGE中。CardsProducer的執行命令如下:

java metro.pub.CardsProducer --mode=recharge --servers=localhost:9999 --Topic=RECHARGE

CardsProducer類接收三個引數:

  1. 執行模式,用於指定是初始化公交卡記錄還是生成充值事件。
  2. 指定VoltDB broker
  3. 指定資料傳送的Topic名稱
    分析一下CardsProducer的主要方法,main方法生成10個執行緒,每5毫秒執行一次publish()方法,將列車進出站時間傳送到Topic“RECHARGE”中。
    public static void main(String[] args) throws IOException {
        CONFIG.parse("CardsProducer", args);
        if(CONFIG.mode.equals("new")) {
            genCards(CONFIG);
            return;
        }
        ScheduledExecutorService EXECUTOR = Executors.newScheduledThreadPool(10);
        CardsProducer producer = new CardsProducer(CONFIG.servers, CONFIG.Topic);
        System.out.println("Recharging Cards");
        EXECUTOR.scheduleAtFixedRate (
                () -> {
                    producer.publish(producer.getRechargeActivityRecords(1));
                }, 1, 5, MILLISECONDS);
    }

和前面TrainProducer一樣,CardsProducer中的 producer也是KafkaProducer,不多介紹。getRechargeActivityRecords方法用來生成一條隨機的充值事件,包括卡號、充值金額和充值站點。每5毫秒執行一次。

  public List<CardEvent> getRechargeActivityRecords(int count) {
        final ArrayList<CardEvent> records = new ArrayList<>();
        int amt = (ThreadLocalRandom.current().nextInt(18)+2)*1000;
        int stationId = ThreadLocalRandom.current().nextInt(1, 18);
        ThreadLocalRandom.current().ints(count, 0, CONFIG.cardcount).forEach((cardId)
                -> {
                    records.add(new CardEvent(cardId, amt, stationId));
                    }
        );
        return records;
    }

這個場景中,Client端的程式碼非常簡單,到此為止。更多的邏輯在服務端定義,請看以下。
Topic用於接收充值事件,它的定義如下:

CREATE TOPIC RECHARGE execute procedure RechargeCard;

其中RechargeCard用於消費Topic中的資料,而RechargeCard是一個java procedure,它透過java+sql的方式,自定義了業務邏輯。java procedure是VoltDB在處理流資料時經常用到的物件,它是一個執行在VoltDB服務端的java類,而非client端程式碼。它需要提前編譯成jar包(如下procs.jar),並載入到VoltDB java 執行時環境中。之後使用如下DDL定義。定義了RechargeCard後,在上面的CREATE TOPIC中才能被引用。

sqlcmd --query="load classes $PROJ_HOME/dist/procs.jar"
CREATE PROCEDURE PARTITION ON TABLE cards COLUMN card_id PARAMETER 0 FROM CLASS metro.cards.RechargeCard;

讓我們看一下RechargeCard中的邏輯,重點關注如何將java業務邏輯與SQL進行結合。其中定義run()方法和四個sql語句。RechargeCard從Topic RECHARGE中消費資料,進行反序列化之後,逐條將資料(即充值事件)作為傳參交給run()方法,run()是procedure的入口方法。
voltQueueSQL是VoltDB的server 端api,用來執行sql並返回結果。Sql getCard和getStationName首先根據從Topic中獲取的資料進行充值事件合法性校驗,如果資料庫中沒有對應的充值站點或公交卡記錄,則執行sql exportNotif寫入一條錯誤資訊。否則,update VoltDB資料庫中對應公交卡,增加餘額,並執行sql exportNotif寫入一條成功資訊。

public class RechargeCard extends VoltProcedure {
    public final SQLStmt updateBalance = new SQLStmt("UPDATE cards SET balance = balance + ? WHERE card_id = ? AND card_type = 0");
    public final SQLStmt getCard = new SQLStmt("SELECT * from cards WHERE card_id = ?");
    public final SQLStmt exportNotif = new SQLStmt("INSERT INTO CARD_ALERT_EXPORT values (?, NOW, ?, ?, ?, ?, ?, ?)");
    public final SQLStmt getStationName = new SQLStmt("SELECT name FROM stations WHERE station_id = ?"); 
    public long run(int cardId, int amt, int stationId) {
        voltQueueSQL(getStationName, stationId);
        voltQueueSQL(getCard, cardId);
        String station = "UNKNOWN";
        
        final VoltTable[] results = voltExecuteSQL();
        if(results.length == 0) 
            exportError(cardId, station);
        
        VoltTable stationResult = results[0];
        if(stationResult.advanceRow()) 
            station = stationResult.getString(0);
        
        VoltTable card = results[1];
        if(card.advanceRow()) {
            voltQueueSQL(updateBalance, amt, cardId);
            
            String name = card.getString(5);
            String phone = card.getString(6);
            String email = card.getString(7);
            int notify = (int) card.getLong(8);
            
            voltQueueSQL(updateBalance, amt, cardId);
            voltQueueSQL(exportNotif, cardId, station, name, phone, email, notify, "Card recharged successfully");
            
            voltExecuteSQL(true);
        } else {
            exportError(cardId, station);
        }
        return 0;
}
    private void exportError(int cardId, String station) {
        exportError(cardId, station, "", "", "", 0, "Could not locate details of card for recharge");
    }
    
    private void exportError(int cardId, String station, String name, String phone, String email, int notify, String msg) {
        voltQueueSQL(exportNotif, cardId, station, name, phone, email, notify, msg);
        voltExecuteSQL(true);
    }
}

exportNotif的定義如下,其中CARD_ALERT_EXPORT是VoltDB的stream資料庫物件,一種資料管道,insert進去的資料逐一流過。

public final SQLStmt exportNotif = new SQLStmt("INSERT INTO CARD_ALERT_EXPORT values (?, NOW, ?, ?, ?, ?, ?, ?)");

可以在CARD_ALERT_EXPORT上新增資料處理邏輯,實現流計算效果。這個場景中,簡單的在Stream上建立了一個檢視,用於生成實時統計報表。檢視的定義如下:

CREATE VIEW card_export_stats(card_id, station_name, rechargeCount) AS 
	SELECT card_id, station_name, count(*) from CARD_ALERT_EXPORT 
	GROUP BY card_id, station_name;

最後,我們定義Stream中的資料最終流向另外的Topic,該Topic可以讓VoltDB之外的大資料產品進行消費,完成下游資料處理邏輯。

CREATE TOPIC using stream CARD_ALERT_EXPORT properties(Topic.format=avro);

2.4 程式碼分析-乘客刷卡乘車

這個場景中,客戶端隨機生成大量乘客刷卡進站記錄,併傳送給資料庫處理。
服務端完成如下操作:
1.首先進行一系列校驗,如驗證卡資訊,卡餘額,是否盜刷等反欺詐操作。
2.將所有刷卡行為都記錄到資料表中。並將餘額不足和複合欺詐邏輯的刷卡事件分別釋出到不同的Topic中,供其他下游系統訂閱。
在客戶端透過執行java類RidersProducer,與前面兩個場景不同,RidersProducer類直接連線VoltDB資料庫將資料寫入資料表中,而不是將資料傳送到VoltDB Topic中。用來展示VoltDB的多種使用方式。
connectToOneServerWithRetry使用VoltDB client api連線指定ip的VoltDB資料庫。

  void connectToOneServerWithRetry(String server, Client client) {
        int sleep = 1000;
        while (true) {
            try {
                client.createConnection(server);
                break;
            }
            catch (Exception e) {
                System.err.printf("Connection failed - retrying in %d second(s).\n", sleep / 1000);
                try { Thread.sleep(sleep); } catch (Exception interruted) {}
                if (sleep < 8000) sleep += sleep;
            }
        }
        System.out.printf("Connected to VoltDB node at: %s.\n", server);
    }

RidersProducer類建立100個執行緒,runBenchmark方法中每200毫秒這些執行緒執行一次getEntryActivityRecords。getEntryActivityRecords隨機生成一條乘客進站乘車記錄,記錄內容包括卡號、當前時間、進站站點id等

private static final ScheduledExecutorService EXECUTOR =     Executors.newScheduledThreadPool(100);
public void runBenchmark() throws Exception {
        int microsPerTrans = 1000000/RidersProducer.config.rate;
        EXECUTOR.scheduleAtFixedRate (
                () -> {
                    List<Object[]> entryRecords = getEntryActivityRecords(config.cardcount);//生成隨機的進站記錄
                    call(config.cardEntry, entryRecords);//將資料傳送到VoltDB資料庫
                }, 10000, microsPerTrans, MICROSECONDS);
    }
    public static List<Object[]> getEntryActivityRecords(int count) {
        final ArrayList<Object[]> records = new ArrayList<>();
        long curTime = System.currentTimeMillis();
        ThreadLocalRandom.current().ints(1, 0, count).forEach((cardId)
                -> {
                    records.add(new Object[] {cardId, curTime, Stations.getRandomStation().stationId, ENTER.value, 0});
                    }
        );
        return records;
    }

接著呼叫call方法,將資料records傳送到資料庫進行處理。Call方法定義如下,callProcedure是VoltDB的client端api,用於將資料傳送給指定名稱的procedure進行處理,可以透過同步和非同步IO兩種方式進行呼叫,非同步呼叫時需要指定回撥函式對資料庫呼叫的返回結果進行處理,即本例中的自定義了BenchmarkCallback。

   protected static void call(String proc, Object[] args) {
        try {
            client.callProcedure(new BenchmarkCallback(proc, args), procName, args);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

Call方法將資料傳送給procedure,procedure名稱由如下程式碼指定。一起看看procedure中的具體邏輯。

 @Option(desc = "Proc for card entry swipes")
        String cardEntry = "ValidateEntry";

Procedure ValidateEntry的部分定義,首先定義了6個SQL。

 //查詢公交卡是否存在
    public final SQLStmt checkCard = new SQLStmt(
        "SELECT enabled, card_type, balance, expires, name, phone, email, notify FROM cards WHERE card_id = ?;");
    //卡充值
    public final SQLStmt chargeCard = new SQLStmt(
        "UPDATE cards SET balance = ? WHERE card_id = ?;");
    //查詢指定站點的入站費用
    public final SQLStmt checkStationFare = new SQLStmt(
        "SELECT fare, name FROM stations WHERE station_id = ?;");
    //記錄進站事件
    public final SQLStmt insertActivity = new SQLStmt(
        "INSERT INTO card_events (card_id, date_time, station_id, activity_code, amount, accept) VALUES (?,?,?,?,?,?);");
    //再次用到card_alert_export 這個stream物件,用於傳送公交卡欠費訊息
    public final SQLStmt exportActivity = new SQLStmt(
        "INSERT INTO card_alert_export (card_id, export_time, station_name, name, phone, email, notify, alert_message) VALUES (?,?,?,?,?,?,?,?);");
    //將刷卡欺詐行為寫入stream物件fraud中
    public final SQLStmt publishFraud = new SQLStmt(
            "INSERT INTO fraud (trans_id, card_id, date_time, station, activity_type, amt) values (?, ?, ?, ?, ?, ?)"
            );

值得說明的,上面最後一個sql中用到的fraud是另外一個stream物件,用於插入刷卡欺詐事件,透過DDL定義其中的刷卡欺詐行為最終會發布到VoltDB Topic中,用於下游處理產品消費。

CREATE STREAM FRAUD partition on column CARD_ID (
  TRANS_ID varchar not null,
  CARD_ID integer not null,
  DATE_TIME timestamp not null,
  STATION integer not null,
  ACTIVITY_TYPE TINYINT not null,
  AMT integer not null
);
create Topic using stream FRAUD properties(Topic.format=avro,consumer.keys=TRANS_ID);

前面已經提到run方法是procedure的入口方法,VoltDB執行procedure時,自動呼叫該方法。前面客戶端傳進的records記錄,被逐一傳遞到run方法到引數中進行處理。run方法定義如下

public VoltTable run(int cardId, long tsl, int stationId, byte activity_code, int amt) throws VoltAbortException {
        //查詢公交卡是否存在
        voltQueueSQL(checkCard, EXPECT_ZERO_OR_ONE_ROW, cardId);
        //查詢指定站點的交通費用
        voltQueueSQL(checkStationFare, EXPECT_ONE_ROW, stationId);
        VoltTable[] checks = voltExecuteSQL();
        VoltTable cardInfo = checks[0];
        VoltTable stationInfo = checks[1];
        byte accepted = 0;
        //如果公交卡記錄等於0,說明卡不存在
        if (cardInfo.getRowCount() == 0) {
            //記錄刷卡行為到資料庫表中,將accept欄位置為拒絕“REJECTED”
            voltQueueSQL(insertActivity, cardId, tsl, stationId, ACTIVITY_ENTER, amt, ACTIVITY_REJECTED);
voltExecuteSQL(true);
//返回“被拒絕”訊息給客戶端。
            return buildResult(accepted,"Card Invalid");
        }
        // 如果卡存在,則取出卡資訊。
        cardInfo.advanceRow();
        //卡狀態,0不可用,1可用
        int enabled = (int)cardInfo.getLong(0);
        int cardType = (int)cardInfo.getLong(1);
        //卡餘額
        int balance = (int)cardInfo.getLong(2);
        TimestampType expires = cardInfo.getTimestampAsTimestamp(3);
        String owner = cardInfo.getString(4);
        String phone = cardInfo.getString(5);
        String email = cardInfo.getString(6);
        int notify = (int)cardInfo.getLong(7);
        // 查詢指定站點的進站費用
        stationInfo.advanceRow();
        //指定站點的進站費用
        int fare = (int)stationInfo.getLong(0);
        String stationName = stationInfo.getString(1);
        // 刷卡時間
        TimestampType ts = new TimestampType(tsl);
        // 如果卡狀態為不可用
        if (enabled == 0) {
                //向客戶端返回“此卡不可用”
                return buildResult(accepted,"Card Disabled");
        }
        // 如果卡型別為“非月卡”
        if (cardType == 0) { // 如果卡內餘額充足
                if (balance > fare) {
                    //isFrand為反欺詐策略,後面介紹
                    if (isFraud(cardId, ts, stationId)) {
                        // 如果認定為欺詐,記錄刷卡記錄,記錄型別為“欺詐刷卡”
                        voltQueueSQL(insertActivity, cardId, ts, stationId, ACTIVITY_ENTER, fare, ACTIVITY_FRAUD);
                        //並且把欺詐事件寫入stream,並最終被髮布到VoltDB Topic中。見前面STREAM FRAUD到ddl定義
                        voltQueueSQL(publishFraud, generateId(cardId, tsl), cardId, ts, stationId, ACTIVITY_ENTER, amt);
                        voltExecuteSQL(true);
                        //向客戶端返回“欺詐交易”訊息
                        return buildResult(0, "Fraudulent transaction");
                    } else {
                        // 如果不是欺詐行為,則減少卡內餘額,完成正常消費
                        voltQueueSQL(chargeCard, balance - fare, cardId);
                        //記錄正常的刷卡事件
                        voltQueueSQL(insertActivity, cardId, ts, stationId, ACTIVITY_ENTER, fare, ACTIVITY_ACCEPTED);
                        voltExecuteSQL(true);
                        //向客戶端返回卡內餘額
                        return buildResult(1, "Remaining Balance: " + intToCurrency(balance - fare));
                    }
                } else {
                        // 如果卡內餘額不足,記錄刷卡失敗事件。
                        voltQueueSQL(insertActivity, cardId, ts, stationId, ACTIVITY_ENTER, 0, ACTIVITY_REJECTED);
                        if (notify != 0) {  
                            //再次用到card_alert_export 這個stream物件,用於傳送公交卡欠費訊息
                            voltQueueSQL(exportActivity, cardId, getTransactionTime().getTime(), stationName, owner, phone, email, notify, "Insufficient Balance");
                        }
                        voltExecuteSQL(true);
                        //向客戶端返回“餘額不足“訊息
                        return buildResult(0,"Card has insufficient balance: "+intToCurrency(balance));
                }
        }
    }

以上程式碼中有一個isFraud方法,用於判定是否為欺詐性刷卡。這裡定義了一些簡單反欺詐規則

  1. 如果一秒鐘內相同的卡片有1次以上的刷卡記錄,認定為欺詐。因為不可能存在時間間隔如此短的刷卡行為,可能是由於有多張偽造卡片在同時刷卡。
  2. 同一張卡在過去一小時內,在5個或5個以上站點刷卡進站。假設這同樣被認為是由於有多張偽造卡片在同時刷卡。
  3. 同一張卡在過去一小時內,有過10次以上刷卡進站記錄。進出站次數太多,暫停使用一段時間。
isFraud方法根據當前刷卡記錄中的資料,結合資料庫中的歷史記錄實現以上反欺詐規則。歷史刷卡記錄被儲存在card_events表中,另外基於這張表建立了檢視,統計每張卡在一秒鐘內是否有過刷卡記錄。
CREATE VIEW CARD_HISTORY_SECOND as select card_id, TRUNCATE(SECOND, date_time) scnd from card_events group by card_id, scnd;
isFraud方法的定義
    public final SQLStmt cardHistoryAtStations = new SQLStmt(
        "SELECT activity_code, COUNT(DISTINCT station_id) AS stations " +
        "FROM card_events " +
        "WHERE card_id = ? AND date_time >= DATEADD(HOUR, -1, ?) " +
        "GROUP BY activity_code;"
    );
    public final SQLStmt cardEntries = new SQLStmt(
    "SELECT activity_code " +
    "FROM card_events " +
    "WHERE card_id = ? AND station_id = ? AND date_time >= DATEADD(HOUR, -1, ?) " +
    "ORDER BY date_time;"
    );
    public final SQLStmt instantaneousCardActivity = new SQLStmt(
            "SELECT count(*) as activity_count "
            + "FROM CARD_HISTORY_SECOND "
            + "WHERE card_id = ? "
            + "AND scnd = TRUNCATE(SECOND, ?) "
            + "GROUP BY scnd;"
            );
  
 public boolean isFraud(int cardId, TimestampType ts, int stationId) {
        voltQueueSQL(instantaneousCardActivity, cardId, ts);
        voltQueueSQL(cardHistoryAtStations, cardId, ts);
        voltQueueSQL(cardEntries, cardId, stationId, ts);
        final VoltTable[] results = voltExecuteSQL();
        final VoltTable cardInstantaneousActivity = results[0];
        final VoltTable cardHistoryAtStationisTable = results[1];
        final VoltTable cardEntriesTable = results[2];
        //一秒鐘之內已經有一次刷卡記錄的話,返回true
        while (cardInstantaneousActivity.advanceRow()) {
            if(cardInstantaneousActivity.getLong("activity_count") > 0) {
                return true;
            }
        }
        
        while (cardHistoryAtStationisTable.advanceRow()) {
            final byte activity_code = (byte) cardHistoryAtStationisTable.getLong("activity_code");
            final long stations = cardHistoryAtStationisTable.getLong("stations");
            if (activity_code == ACTIVITY_ENTER) {
                // 過去1小時之內在五個站點刷卡進站,返回true
                if (stations >= 5) {
                    return true;
                }
            }
        }
        byte prevActivity = ACTIVITY_INVALID;
        int entranceCount = 0;
        while (cardEntriesTable.advanceRow()) {
            final byte activity_code = (byte) cardHistoryAtStationisTable.getLong("activity_code");
            if (prevActivity == ACTIVITY_INVALID || prevActivity == activity_code) {
                if (activity_code == ACTIVITY_ENTER) {
                    prevActivity = activity_code;
                    entranceCount++;
                } else {
                    prevActivity = ACTIVITY_INVALID;
                }
            }
        }
        // 如果在過去1小時內有10次連續的刷卡記錄,返回true。
        if (entranceCount >= 10) {
            return true;
        }
        return false;
    }

您看好VoltDB嗎? 馬上行動吧!
歡迎私信,與更多小夥伴一起探討。

關於VoltDB
VoltDB支援強ACID和實時智慧決策的應用程式,以實現互聯世界。沒有其它資料庫產品可以像VoltDB這樣,可以同時需要低延時、大規模、高併發數和準確性相結合的應用程式加油。
VoltDB由2014年圖靈獎獲得者Mike Stonebraker博士建立,他對關聯式資料庫進行了重新設計,以應對當今不斷增長的實時操作和機器學習挑戰。Stonebraker博士對資料庫技術研究已有40多年,在快速資料,流資料和記憶體資料庫方面帶來了眾多創新理念。
在VoltDB的研發過程中,他意識到了利用記憶體事務資料庫技術挖掘流資料的全部潛力,不但可以滿足處理資料的延遲和併發需求,還能提供實時分析和決策。VoltDB是業界可信賴的名稱,在諾基亞、金融時報、三菱電機、HPE、巴克萊、華為等領先組織合作有實際場景落地案例。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69903219/viewspace-2768059/,如需轉載,請註明出處,否則將追究法律責任。

相關文章