You are not logged in.

  • Login
Bitte besucht unser neues Forum unter https://forum.nitrado.net | Please visit our new Forum at https://forum.nitrado.net

Dear visitor, welcome to Nitrado.net Prepaid Gameserver Community-Support - Archiv. If this is your first visit here, please read the Help. It explains in detail how this page works. To use all features of this page, you should consider registering. Please use the registration form, to register here or read more information about the registration process. If you are already registered, please login here.

BlvckBytes

User / Kunde

  • "BlvckBytes" is male
  • "BlvckBytes" started this thread

Posts: 797

Location: (*blvckbytes).home

Occupation: Schüler -> HTL für Informationstechnologie und Netzwerktechnik

  • Send private message

1

Saturday, September 2nd 2017, 8:13pm

Caching von normalen sowie objektorientierten Daten

Hey!

Ich habe mich in den letzten paar Tagen ausführlich mit dem Thema Caching im bereich von MySQL-Daten beschäftigt.

Was versteht man unter Caching?
Caching bedeutet, dass man anstatt die Daten für den Spieler jedes mal neu aus der SQL Datenbank zu holen, lokal zwischenspeichert ( im RAM ) und somit schneller und
effizienter darauf zugreifen kann. Beim onEnable werden die Daten geladen, beim onDisable geschrieben. Wenn man auf "Nummer Sicher" gehen will, kann man diese auch
zwischendurch in einem bestimmten Rhythmus speichern, so wie ich es auch tue.

Was muss ich beim Caching beachten?

Es ist für die Performance wichtig, dass die Schreib- und Lesevorgänge Asynchron geschehen, sowie der Schreibvorgang in "Batches" ausgeführt wird, mehr davon später. Wenn du sehr
effizient arbeiten möchtest, bietet es sich an, Objekte zu serialisieren, bevor du sie in die Datenbank schreibst. Beim Laden kannst du dann nämlich genau dieses Objekt wieder erhalten.
Wie das funktioniert, zeige ich dir gleich.

Gut, dann fangen wir mal an!
Ich habe das ganze statisch umgesetzt, da ich finde, dass wenn man so oft darauf zugreifen muss, sich ein Instance-Getter nicht lohnt. Mein Caching spielt sich in der Klasse Namens "DataManager" ab.

Meine DataManager-Klasse

Spoiler Spoiler

Source code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
package me.powerfulenergy.sbs.datamanager;

import com.sun.org.apache.xpath.internal.SourceTree;
import me.powerfulenergy.sbs.Main;
import me.powerfulenergy.sbs.Module;
import me.powerfulenergy.sbs.database.SQLHandler;
import me.powerfulenergy.sbs.utils.Utils;
import org.bukkit.Bukkit;
import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream;
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.*;

public class DataManager {

    private static HashMap< String, Object > cache;
    private static SQLHandler sql;

    static {
        cache = new HashMap<>();
        sql = Main.getSQL();
    }

    /**
     * Reads all Objects with Name from Database and saves
     * into Cache. Then starts the scheduled Dump.
     */
    public static void init() {
        sql.normalResult( "SELECT * FROM `data_manager`", resultSet -> {
            try {
                while( resultSet.next() ) {
                    String name = resultSet.getString( "name" );
                    String value = resultSet.getString( "data" );

                    cache.put( name, fromString( value ) );
                }
            } catch( Exception ex ) {
                ex.printStackTrace();
            }
        } );
        scheduledDump();
    }

    /**
     * Performes a Dump every five Minutes.
     */
    private static void scheduledDump() {
        long delay = 20 * 60 * 5;
        Bukkit.getScheduler().scheduleSyncRepeatingTask( Main.get(), () -> {
            dump();
        }, delay, delay );
    }

    /**
     * Writes the Data into the Cache. It manages Inserting and Updating.
     * If something was marked as deleted in the dump, it gets removed from the DB aswell.
     */
    public static void dump() {
        String query = "INSERT INTO `data_manager` ( `name`, `data` ) VALUES ( ?, ? ) ON DUPLICATE KEY UPDATE `data` = ?";
        sql.prepareStmt( query, preparedStatement -> {
            try {
                ArrayList< String > delete = new ArrayList<>();

                for( Map.Entry< String, Object > ent : cache.entrySet() ) {
                    String name = ent.getKey();
                    Object obj = ent.getValue();

                    if( obj.equals( "delete_from_database" ) ) {
                        delete.add( name );
                        continue;
                    }

                    String data = toString( obj );
                    preparedStatement.setString( 1, name );
                    preparedStatement.setString( 2, data.toString() );
                    preparedStatement.setString( 3, data.toString() );
                    preparedStatement.addBatch();
                }
                sql.preparedBatch( preparedStatement );

                sql.prepareStmt( "DELETE FROM `data_manager` WHERE `name` = ?", psDelete -> {
                    try {

                        for( String name : delete ) {
                            psDelete.setString( 1, name );
                            psDelete.addBatch();
                        }

                    } catch( Exception ex ) {
                        ex.printStackTrace();
                    }
                    sql.preparedBatch( psDelete );
                } );
            } catch( Exception ex ) {
                ex.printStackTrace();
            }
        } );
    }

    /**
     * Set a Name with an Object into the Cache
     * @param name Name of the Object
     * @param obj Object to save
     */
    public static void set( String name, Object obj ) {
        cache.put( name, obj );
    }

    /**
     * Gets a Name from the Cache
     * @param name Name to get
     * @return Object, null if not exists
     */
    public static Object get( String name ) {
        Object obj = cache.containsKey( name ) ? cache.get( name ) : null;

        if( obj == null || obj.equals( "delete_from_database" ) )
            return null;

        return obj;
    }

    /**
     * Deletes a Name from the Cache
     * @param name Name to Delete
     */
    public static void delete( String name ) {
        String deleteKey = "delete_from_database";
        set( name, deleteKey );
    }

    /**
     * Checks if a Name exists in the Cache
     * @param name Name to check for
     * @return Exist-Boolean
     */
    public static boolean exists( String name ) {
        boolean exist = cache.containsKey( name );
        return exist;
    }

    /**
     * Returns a Map with UUIDs and Objects from a Name-Type
     * @param name Name to select Entries
     * @return Map with Data
     */
    public static Map< UUID, Object > typeList( String name ) {
        HashMap< UUID, Object > buf = new HashMap<>();

        for( String type : cache.keySet() ) {
            String[] data = type.split( "\\.", 2 );

            String splitted = data[ 0 ];
            if( splitted.equals( name ) ) {
                UUID uuid = UUID.fromString( data[ 1 ] );
                buf.put( uuid, get( type ) );
            }
        }
        return buf;
    }

    /**
     * Returns all Objects with the same Name-Type
     * @param name Name to select Types
     * @return Object-List
     */
    public static List< Object > singleList( String name ) {
        List< Object > obj = new ArrayList<>();

        for( String type : cache.keySet() ) {
            String[] data = type.split( "\\.", 2 );

            String splitted = data[ 0 ];
            if( splitted.equals( name ) && !cache.get( type ).equals( "delete_from_database" ) )
                obj.add( cache.get( type ) );
        }
        return obj;
    }

    /**
     * De-Serializes the given String to an Object
     * @param string Serialize-String
     * @return Object from String
     */
    private static Object fromString( String string ) {
        Object obj = null;
        try {
            ByteArrayInputStream inputStream = new ByteArrayInputStream( Base64Coder.decodeLines( string ) );
            BukkitObjectInputStream dataInput = new BukkitObjectInputStream( inputStream );

            obj = dataInput.readObject();
        } catch ( Exception ex ) {
            obj = "Error!";
        }

        if( Utils.isLong( string ) )
            return Long.parseLong( string );

        if( Utils.isDouble( string ) )
            return Double.parseDouble( string );

        return obj.equals( "Error!" ) ? string : obj;
    }

    /**
     * Serializes the given Object to a String
     * @param object Object to Serialize
     * @return Serialize-String
     */
    private static String toString( Object object ) {
        try {
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream( outputStream );

            dataOutput.writeObject( object );
            dataOutput.close();

            return Base64Coder.encodeLines( outputStream.toByteArray() );
        } catch ( Exception ex ) {
            ex.printStackTrace();
            return null;
        }
    }
}


So, also nun nochmal zum mitschreiben, was in dieser Klasse passiert:
- "init" wird beim onEnable aufgerufen
- "scheduledDump" kümmert sich um das Schreiben im 5 Minuten abstand
- "singleList" gibt eine Liste aller Objekte mit dem selben Typen zurück
- "typeList" gibt eine HashMap aller Objekte mit Besitzer zurück
- "dump" wird beim onDisable aufgerufen
- "toString" verwandelt ein Objekt in einen String
- "fromString" verwandelt einen String in ein Objekt

Wie speichere ich die Daten in meiner SQL Datenbank ab?
Ich habe eine Tabelle mit folgendem Layout: name | data
Die Spalte "name" ist auf "Unique" gesetzt, damit kann ich per SQL Statement überprüfen, ob dieser Wert schon vorhanden ist.
Falls er vorhanden ist, kann ich ihn Updaten, wenn nicht kann ich die Daten Inserten.

Bei meiner Klasse wird zwischen zwei Arten von Daten unterschieden: aktive Daten und zu löschende Daten.
Zu löschende Daten werden mit dem String "delete_from_database" gekennzeichnet. Diese werden dann dementsprechend beim
Schreibvorgang anders behandelt.

Wie läuft mein Schreibvorgang ab?
Ich habe eine Schleife, welche alle Daten durchgeht. Wenn diese Daten nicht gelöscht werden sollen, werden sie auf den Schreibe-Batch
geaddet. Wenn sie gelöscht werden sollen, auf den Lösch-Batch. Am Ende werden beide Batches ausgeführt, und der Schreibvorgang ist zu Ende.
Ein Batch funktioniert folgendermaßen: Man verwendet eine Query mehrmals, indem man immer wieder die Parameter setzt, und sie auf einen Stapel hinzufügt.
Dann kann man am Ende einen Stapel ausführen, und muss nicht öfters die Datenbank "belästigen".

Was bedeutet serialisieren und wie hilft es mir?
Mit der Serialisierung können ganze Objekte in einen String verwandelt werden, welcher dann ganz einfach in die Datenbank geschrieben werden kann. Beim Laden kann
man aus diesem String wieder ein Objekt erzeugen, und dieses bequem weiterverwenden. Es ist wichtig, dass die zu serialisierende Klasse das Interface Serializable implementiert.

Ein Beispiel der Benutzung:

Mein Bann-Objekt

Spoiler Spoiler

Source code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package me.powerfulenergy.sbs.ban;

import lombok.Getter;
import lombok.Setter;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;

import java.io.Serializable;

public class BanInfo implements Serializable {

    @Getter
    private OfflinePlayer player;

    @Getter @Setter
    private boolean banned;

    @Getter @Setter
    private String reason;

    @Getter @Setter
    private OfflinePlayer executor;

    public BanInfo( Player p ) {
        this.player = p;
        this.banned = false;
        this.reason = "Nicht gebannt.";
    }
}


Meine BannManager-Klasse

Spoiler Spoiler

Source code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package me.powerfulenergy.sbs.ban;

import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import me.powerfulenergy.sbs.Main;
import me.powerfulenergy.sbs.Module;
import me.powerfulenergy.sbs.datamanager.DataManager;
import me.powerfulenergy.sbs.modules.TempbanCommand;
import me.powerfulenergy.sbs.pStr;
import me.powerfulenergy.sbs.utils.PlayerCommand;
import me.powerfulenergy.sbs.utils.Utils;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerLoginEvent;
import ru.tehkode.permissions.bukkit.PermissionsEx;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.HashMap;
import java.util.UUID;

public class BanCommand extends PlayerCommand implements Listener, Module {

    public BanCommand() {
        Main.get().regMan.register( this, "ban" );
        Main.get().regMan.register( this, "unban" );
        Main.get().regMan.register( this );
    }

    @Override
    public boolean cmd( Player p, Command cmd, String[] args ) {
        if( !p.hasPermission( "software.ban" ) ) {
            p.sendMessage( pStr.noPerm );
            return true;
        }

        if( args.length < 1 ) {
            String usage = cmd.getName().equalsIgnoreCase( "ban" ) ? "<Name> [Grund]" : "<Name>";
            p.sendMessage( pStr.prefix + "§7Nutze: /" + cmd.getName() + " " + usage );
            return true;
        }

        OfflinePlayer target = Bukkit.getOfflinePlayer( args[ 0 ] );
        BanInfo info = ( BanInfo ) DataManager.get( "bans." + target.getUniqueId() );

        if( info == null ) {
            p.sendMessage( pStr.prefix + "§cDer Spieler existiert nicht!" );
            return true;
        }

        if( cmd.getName().equalsIgnoreCase( "ban" ) ) {

            if( PermissionsEx.getUser( target.getName() ).has( "software.ban.bypass" ) ) {
                p.sendMessage( pStr.prefix + "§cDiesen Spieler kannst du nicht bannen!" );
                return true;
            }

            String reason = "Kein Grund angegeben.";
            if( args.length >= 2 ) {
                reason = "";
                for( int i = 1; i < args.length; i++ )
                    reason += args[ i ] + " ";
                reason = reason.substring( 0, reason.length() - 1 );
            }

            info.setBanned( true );
            info.setReason( reason );
            info.setExecutor( p );

            if( target.isOnline() ) {
                Bukkit.getPlayer( target.getName() ).kickPlayer(
                    "§7Du wurdest §4permanent §7vom Server §4gebannt§7!\n" +
                    "§7Von: §4" + p.getName() + "\n" +
                    "§7Grund: §4" + reason
                );
            }

            Bukkit.broadcastMessage( " " );
            Bukkit.broadcastMessage( "§4BAN §8» §e" + target.getName() + " §7wurde §4permanent §7gebannt!" );
            Bukkit.broadcastMessage( "§4BAN §8» §7Von: §e" + p.getName() );
            Bukkit.broadcastMessage( "§4BAN §8» §7Grund: §e" + reason );
            Bukkit.broadcastMessage( " " );
            return true;
        }

        if( cmd.getName().equalsIgnoreCase( "unban" ) ) {

            if( !info.isBanned() ) {
                p.sendMessage( pStr.prefix + "§cDer Spieler ist nicht gebannt!" );
                return true;
            }
            
            info.setBanned( false );
            info.setExecutor( p );
            info.setReason( "Nicht gebannt." );

            Bukkit.broadcastMessage( " " );
            Bukkit.broadcastMessage( "§4BAN §8» §e" + target.getName() + " §7wurde §4entbannt§7!" );
            Bukkit.broadcastMessage( "§4BAN §8» §7Von: §e" + p.getName() );
            Bukkit.broadcastMessage( " " );
            return true;
        }
        return true;
    }

    @EventHandler( priority = EventPriority.HIGHEST )
    public void onLogin( PlayerLoginEvent event ) {
        Player p = event.getPlayer();
        BanInfo info = ( BanInfo ) DataManager.get( "bans." + p.getUniqueId() );

        if( info == null )
            DataManager.set( "bans." + p.getUniqueId(), new BanInfo( p ) );

        if( info.isBanned() ) {
            event.setKickMessage(
                "§7Du bist §4permanent §7vom Server §4gebannt§7!\n" +
                "§7Von: §4" + info.getExecutor().getName() + "\n" +
                "§7Grund: §4" + info.getReason()
            );
            event.setResult( PlayerLoginEvent.Result.KICK_BANNED );
        }
    }
}


Wie ihr seht, kann ich ganz einfach mit den Objekten Arbeiten, und muss mich nicht mit tausend verstreuten Werten herumschlagen. Bei einem Stats-System geht das ganze genau so effizient: Man kann Kills, Deaths, Money, KD, KillStreak usw in einem Objekt Speichern und dieses einfach weiterverwenden. So habe ich auch mein Hologramm-System programmiert, welches auf dem selben OOP System basiert.

Wie ihr seht, benutze ich für den Namen einfach "data-typ.<UUID>".

Ich hoffe, dass ich euch damit eventuell etwas Arbeit abnehmen konnte, da ihr jetzt effizienter Arbeiten könnt ( es sei denn ihr Cached bereits Objektorientiert ), :D.
LG BlvckBytes

BtoBastian

Moderator

  • "BtoBastian" is male

Posts: 3,612

Occupation: Softwareentwickler

  • Send private message

2

Sunday, September 3rd 2017, 8:15am

Standard Standart eines Flamingos ist einbeinig.

BlvckBytes

User / Kunde

  • "BlvckBytes" is male
  • "BlvckBytes" started this thread

Posts: 797

Location: (*blvckbytes).home

Occupation: Schüler -> HTL für Informationstechnologie und Netzwerktechnik

  • Send private message

3

Monday, September 4th 2017, 12:17am

#push

Bitte besucht unser neues Forum unter https://forum.nitrado.net| Please visit our new Forum at https://forum.nitrado.net