文章版權歸騰訊GAD全部,禁止匿名轉載;禁止商業使用;禁止我的使用。
上文介紹了咱們的SLG手遊的服務器架構設計以及網絡通訊部分,本文介紹數據管理部分,在數據存儲方面,我選擇了Mysql、Memcache和Redis,利用這三種數據庫各自的優點,各自發揮所長,在項目中有着不一樣的應用。css
前文已經對遊戲數據作了大概的分析,以下圖所示:
這是我我的對遊戲中的數據進行的一種劃分。其中,遊戲數據直接使用代碼將表文件中的數據加載到內存,而後從內存中讀取數據便可。玩家數據則分爲了熱數據和冷數據兩部分分別進行存儲,最大程度利用Mysql和Redis各自的優點。html
在咱們的這款遊戲中,咱們的靜態數據配置在CSV表文件中,在服務器啓動時,我讀取全部的表文件,而後取出數據放到Map中,通常以一個標誌性列(如ID)爲key,一個JavaBean做爲value放入到Map中。
讀取CSV表文件步驟以下:
1.讀取CSV文件
2.按必定格式對文件內容進行解析
3.將內容封裝到JavaBean
4.存放數據到Map中
讀取完了以後,在代碼中若是須要,只需經過key在Map中來讀取便可。
在項目中,我將整個過程進行了封裝,封裝成了dataConfig.xml文件、*.csv文件、JavaBean、CsvLoader.java、CsvParser.java和TempletService.java,添加一個CSV文件的步驟以下:
1.在dataConfig.xml中添加csv文件路徑
2.建立一個和CSV文件中結構如出一轍的JavaBean
3.服務器啓動時調用CsvLoader的load()方法來加載全部的CSV文件
4.調用TempletService.listAll()方法,並傳入Javabean的simpleName來加載CSV文件內容到List中
5.將List中的內容按必定結構存儲(我通常都存爲Map結構)java
dataConfig.xml中存儲全部CSV表的路徑,在CsvLoader.java中直接對這個xml表中的路徑下的CSV文件進行讀取加載。node
<?xml version="1.0" encoding="UTF-8"?> <config> <file name="Card.csv" /> <file name="Equip.csv" /> <!--省略更多CSV表--> </config>
CSV文件中存儲具體的遊戲數據,這個數據表通常是由數值策劃來進行配置。CSV表的本質就是按逗號進行分割的數據,以下是卡牌表的前兩行數據。mysql
卡牌ID,卡牌名稱,英雄動畫名稱,卡牌兵種name,卡牌兵種類型,卡牌兵種美術資源ID,品質編號,所屬勢力,英雄等級,英雄升星等級,技能id,統,勇,智,初始兵力,初始攻擊力,兵力,攻擊力,剋制係數,被剋制係數,移動速度,爆擊率,爆擊倍數,普攻傷害加深,普攻傷害減免,技能傷害加深,技能傷害減免,普攻傷害點數,普攻免傷點數,技能傷害點數,技能減傷點數,KPI,技能名稱,技能描述:目標 目標個數(範圍)效果數值buff描述,英雄定位 CardId,CardName,CardFlashName,CardSoldName,CardSoldID,CardSoldFlashID,RMBID,CountryID,CardLv,CardStar,SkillID,Tong,Yong,Zhi,HpStar,AttackStar,Hp,Attack,Kezhi,Beikezhi,Speed,Crit,Double,AttackAddbaifen,Attackreducebaifen,SkillAddbaifen,Skillreducebaifen,AttackNum,AttackreduceNum,SkillAddNum,SkillreduceNum,KPI,SkillName,SkillDes,HeroLocal 200001,呂布,lvbu,騎兵,2,4,8,4,1,1,200001,105,110,90,344,70,2.22,2.26,0.17,0.43,385,0.16,1.44,0.176,0.144,0.176,0.144,0,0,0,0,1000,猛將無雙,對敵方全部目標形成 1倍傷害而且全體暈眩3秒 200002,趙雲,zhaoyun,步兵,3,1,8,2,1,1,200002,103,106,100,381,62,2.464,2.008,0.31,0.29,300,0.16,1.44,0.128,0.208,0.128,0.208,0,0,0,0,1000,槍出如龍,對敵方目標形成3次0.5倍傷害並提高自身50%爆擊率持續4秒
添加了CSV文件以後,咱們須要建立一個和CSV表結構如出一轍的JavaBean,以下是卡牌表對應的JavaBean。git
package com.hjc._36.template; public class Card { private int CardId; private String CardName; private String CardFlashName; private String CardSoldName; private int CardSoldID; private int CardSoldFlashID; private int RMBID; private int CountryID; private int CardLv; private int CardStar; private int SkillID; private int Tong; private int Yong; private int Zhi; private int HpStar; private int AttackStar; private float Hp; private float Attack; private float Kezhi; private float Beikezhi; private int Speed; private float Crit; private float Double; private float AttackAddbaifen; private float Attackreducebaifen; private float SkillAddbaifen; private float Skillreducebaifen; private float AttackreduceNum; private float AttackNum; private float SkillAddNum; private float SkillreduceNum; private int KPI; // getter/setter }
CsvDataLoader封裝了對CSV數據的載入,包括使用SAXReader對dataConfig.xml文件的讀取,以及對其中的CSV文件的內容的讀取,代碼以下:github
package com.hjc._36.util.csv; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.hjc._36.core.GameInit; public class CsvDataLoader { public static Logger logger = LoggerFactory.getLogger(CsvDataLoader.class); private String packageName; private String config; // 每一行就是一個配置文件名字 public static int ActivityMaxValue; private static CsvDataLoader inst; public CsvDataLoader(String packageName, String config) { this.packageName = packageName; this.config = config; } public static CsvDataLoader getInstance(String packageName, String config) { if (inst == null) { inst = new CsvDataLoader(packageName, config); } return inst; } /** * 調用load方法加載全部的配置文件 */ public void load() { SAXReader reader = new SAXReader(); InputStream resourceStream = this.getClass().getResourceAsStream( this.config); InputStreamReader resourceStreamReader = new InputStreamReader( resourceStream); try { Document doc = reader.read(resourceStreamReader); List<?> nodes = doc.selectNodes("/config/file"); Map<String, List<?>> dataMap = new HashMap<String, List<?>>(); List<String> files = new LinkedList<String>(); for (Object n : nodes) { Element t = (Element) n; String f = t.attributeValue("name"); List<?> dataList = this.loadFile(f, true); for (Object o : dataList) { TempletService.getInstance().registerObject(o, dataMap); } files.add(f); } logger.info("讀取配置完畢,準備afterLoad"); TempletService.templetMap = dataMap; TempletService.getInstance().afterLoad(); logger.info("afterLoad 完畢"); } catch (DocumentException e) { e.printStackTrace(); } finally { if (resourceStreamReader != null) { try { resourceStreamReader.close(); } catch (IOException e) { e.printStackTrace(); } } if (resourceStream != null) { try { resourceStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } private List<?> loadFile(String file, boolean exitWhenFail) {// 讀文件 InputStream resourceAsStream = null; try { String clzName = file.replaceAll(".csv", ""); file = GameInit.confFileBasePath + file; logger.info("load file: {}", file); // resourceAsStream = this.getClass().getResourceAsStream(file); resourceAsStream = this.getClass().getClassLoader() .getResource(file).openStream(); if (resourceAsStream == null) { logger.error("文件不存在:" + file); if (exitWhenFail) { System.exit(0); } return null; } return loadFromStream(resourceAsStream, clzName); } catch (Exception e) { logger.error("載入文件出錯:" + file); e.printStackTrace(); System.exit(0); } finally { if (resourceAsStream != null) { try { resourceAsStream.close(); } catch (IOException e) { e.printStackTrace(); } } } return Collections.EMPTY_LIST; } public List<?> loadFromStream(InputStream resourceAsStream, String clzName) throws DocumentException, InstantiationException, IllegalAccessException, IOException {// 讀csv文件 CsvParser csvParser = new CsvParser(resourceAsStream); List<String> nodes = csvParser.getListWithNoHeader(); // get clazz String className = this.packageName + clzName; try { Class<?> classObject = Class.forName(className); if (classObject == null) { logger.error("未找到類" + className); return null; } // Get all the declared fields Field[] fields = classObject.getDeclaredFields(); LinkedList<Field> fieldList = new LinkedList<Field>(); int length = fields.length; for (int i = -1; ++i < length;) { boolean isStaticField = Modifier.isStatic(fields[i] .getModifiers()); if (isStaticField) continue; boolean isTransientField = Modifier.isTransient(fields[i] .getModifiers()); if (isTransientField) continue; fieldList.add(fields[i]); } // Get all the declared fields of supper class Class<?> tmp = classObject; while ((tmp = tmp.getSuperclass()) != Object.class) { System.out.print("the extends class is" + tmp.getName()); fields = tmp.getDeclaredFields(); length = fields.length; if (length == 0) continue; for (int i = -1; ++i < length;) { boolean isStaticField = Modifier.isStatic(fields[i] .getModifiers()); if (isStaticField) continue; boolean isTransientField = Modifier.isTransient(fields[i] .getModifiers()); if (isTransientField) continue; fieldList.add(fields[i]); } } // The truly need to return object List<Object> instances = new ArrayList<Object>(nodes.size()); Object instance = null; String fieldName = null; String fieldValue = null; for (String node : nodes) { if (node != null) { instance = classObject.newInstance(); boolean ok = false; // Element row = (Element) node; String[] values = node.split(",");// csv文件以英文逗號分割值 for (int i = 0; i < fieldList.size(); i++) { Field field = fieldList.get(i); fieldName = field.getName(); fieldValue = values[i]; if (fieldValue == null) continue; try { this.setField(instance, field, fieldValue); ok = true; } catch (Exception e) { logger.error("類名稱是" + className + "的屬性" + fieldName + "沒有被成功賦予靜態數據"); continue; } } if (ok) { instances.add(instance); } } } return instances; } catch (ClassNotFoundException e1) { e1.printStackTrace(); logger.error("未找到類" + className); return null; } } /** * * @Title: setUnknowField * @Description: * @param ob * @param f * @param v * @throws IllegalArgumentException * @throws IllegalAccessException */ private void setField(Object obj, Field f, String v) throws IllegalArgumentException, IllegalAccessException { f.setAccessible(true); if (f.getType() == int.class) { f.setInt(obj, Integer.parseInt(v)); } else if (f.getType() == short.class) { f.setShort(obj, Short.parseShort(v)); } else if (f.getType() == byte.class) { f.setByte(obj, Byte.parseByte(v)); } else if (f.getType() == long.class) { f.setLong(obj, Long.parseLong(v)); } else if (f.getType() == double.class) { f.setDouble(obj, Double.parseDouble(v)); } else if (f.getType() == float.class) { f.setFloat(obj, Float.parseFloat(v)); } else if (f.getType() == Timestamp.class) { f.set(obj, Timestamp.valueOf(v)); } else { f.set(obj, f.getType().cast(v)); } } /** * Test Code * * @param args */ public static void main(String[] args) { CsvDataLoader dl = new CsvDataLoader("com.hjc._36.template.", "/dataConfig.xml"); dl.load(); } }
CsvDataLoader中用到的CsvParser是具體對CSV文件按逗號分割的格式的解析的類,代碼以下:web
package com.hjc._36.util.csv; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.List; //JAVA 操做 excel 中的 .csv文件格式 public class CsvParser { private BufferedReader bufferedreader = null; private List list = new ArrayList(); public CsvParser() { } public CsvParser(InputStream inStream) throws IOException { InputStreamReader isr = new InputStreamReader(inStream, "UTF-8"); bufferedreader = new BufferedReader(isr); String stemp; while ((stemp = bufferedreader.readLine()) != null) { list.add(stemp); } } public List getList() throws IOException { return list; } public List getListWithNoHeader() throws IOException { return list.subList(2, list.size()); } // 獲得csv文件的行數 public int getRowNum() { return list.size(); } // 獲得csv文件的列數 public int getColNum() { if (!list.toString().equals("[]")) { if (list.get(0).toString().contains(",")) { // csv文件中,每列之間的是用','來分隔的 return list.get(0).toString().split(",").length; } else if (list.get(0).toString().trim().length() != 0) { return 1; } else { return 0; } } else { return 0; } } // 取得指定行的值 public String getRow(int index) { if (this.list.size() != 0) return (String) list.get(index); else return null; } // 取得指定列的值 public String getCol(int index) { if (this.getColNum() == 0) { return null; } StringBuffer scol = new StringBuffer(); String temp = null; int colnum = this.getColNum(); if (colnum > 1) { for (Iterator it = list.iterator(); it.hasNext();) { temp = it.next().toString(); scol = scol.append(temp.split(",")[index] + ","); } } else { for (Iterator it = list.iterator(); it.hasNext();) { temp = it.next().toString(); scol = scol.append(temp + ","); } } String str = new String(scol.toString()); str = str.substring(0, str.length() - 1); return str; } // 取得指定行,指定列的值 public String getString(int row, int col) { String temp = null; int colnum = this.getColNum(); if (colnum > 1) { temp = list.get(row).toString().split(",")[col]; } else if (colnum == 1) { temp = list.get(row).toString(); } else { temp = null; } return temp; } public void CsvClose() throws IOException { this.bufferedreader.close(); } public List readCvs(String filename) throws IOException { CsvParser cu = new CsvParser(new FileInputStream(new File(filename))); List list = cu.getList(); return list; } public void createCsv(String biao, List list, String path) throws IOException { List tt = list; String data = ""; SimpleDateFormat dataFormat = new SimpleDateFormat("yyyyMMdd"); Date today = new Date(); String dateToday = dataFormat.format(today); File file = new File(path + "resource/expert/" + dateToday + "importerrorinfo.csv"); if (!file.exists()) file.createNewFile(); else file.delete(); String str[]; StringBuilder sb = new StringBuilder(""); sb.append(biao); FileOutputStream writerStream = new FileOutputStream(file, true); BufferedWriter output = new BufferedWriter(new OutputStreamWriter( writerStream, "UTF-8")); for (Iterator itt = tt.iterator(); itt.hasNext();) { String fileStr = itt.next().toString(); // str = fileStr.split(","); // for (int i = 0; i <= str.length - 1; i++) { // 拆分紅數組 用於插入數據庫中 // System.out.print("str[" + i + "]=" + str[i] + " "); // } // System.out.println(""); sb.append(fileStr + "\r\n"); } output.write(sb.toString()); output.flush(); output.close(); } }
服務器啓動時,調用CsvDataLoader的load()方法,以完成對CSV文件的加載,以後就須要使用TempletService的listAll方法來說數據加載到List中,TempletService根據JavaBean的simpleName來對數據進行加載,代碼以下:redis
package com.hjc._36.util.csv; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 添加一個數據表須要作如下幾步 * 1.在包com.hjc._36.template下建立對應的模板類,類名與數據文件一致 * 2.在src/main/resources/csv/中添加模板數據文件 * 3.在src/main/resources/dataConfig.xml加入剛纔的模板數據文件 * * @author 何金成 * */ public class TempletService { public static Logger log = LoggerFactory.getLogger(TempletService.class); public static TempletService templetService = new TempletService(); /** * key:實體名 value:該實體下的全部模板數據 */ public static Map<String, List<?>> templetMap = new HashMap<String, List<?>>(); public TempletService() { } public static TempletService getInstance() { return templetService; } /** * 獲取該實體類下全部模板數據 * * @param beanName * @return */ @SuppressWarnings("unchecked") public static List listAll(String beanName) { return templetMap.get(beanName); } /** * @Title: registerObject * @Description: 註冊對象到對應類的List中 * @param o * @param dataMap * @return void * @throws */ public void registerObject(Object o, Map<String, List<?>> dataMap) { add(o.getClass().getSimpleName(), o, dataMap); } @SuppressWarnings("unchecked") private void add(String key, Object data, Map<String, List<?>> dataMap) { List list = dataMap.get(key); if (list == null) { list = new ArrayList(); dataMap.put(key, list); } list.add(data); } public void afterLoad() { // 加載後處理 // List tests = TempletService.listAll(Test.class.getSimpleName()); // for (Object object : tests) { // Test test = (Test)object; // System.out.print(test.getEquipLv()); // System.out.print(","+test.getLv1()); // System.out.print(","+test.getLv2()); // System.out.print(","+test.getLv3()); // System.out.print(","+test.getLv4()); // System.out.print(","+test.getLv5()); // System.out.print(","+test.getLv6()); // System.out.print(","+test.getLv7()); // System.out.print(","+test.getLv8()); // System.out.print(","+test.getLv9()); // System.out.println(","+test.getLv10()); // } } public void loadCanShu() { // 加載全局參數xml配置 } }
在完成了添加和加載等一系列操做以後,就能夠在代碼中調用CSV表中加載進來的數據了,例如上文提到的卡牌數據表,加載代碼以下:spring
// 卡牌數據表 List<Card> cardList = TempletService .listAll(Card.class.getSimpleName()); Map<Integer, Card> cardMap = new HashMap<Integer, Card>(); for (Card card : cardList) { cardMap.put(card.getCardId(), card); } this.cardMap = cardMap;
使用時只須要根據卡牌的Id,就能夠取到這張卡牌的全部數據。
咱們使用Mysql做爲冷數據的存儲數據庫,並使用Druid和Hibernate來建立數據庫的鏈接以及增刪改查的操做。在遊戲數據中,我對遊戲中的冷數據作了一個總結,以下圖所示:
完成要存儲的遊戲數據的分析以後,咱們就能夠進行具體建模建表的工做,完成對數據的設計。因爲在遊戲服務器的數據存儲中,數據庫基本上只是一個遊戲數據臨時存放的地方,因此遊戲數據中的關聯性並非特別強,因此不須要嚴密的數據庫設計,只需簡單的將玩家全部的數據按照一個userid進行關聯便可,在使用Hibernate的時候,咱們使用了Hibernate4,使用了它註解JavaBean自動建表的功能,咱們只需將須要存儲的Model寫成JavaBean,並寫上註解,在啓動時,Hibernate掃描到JavaBean會自動爲咱們建立或更新表。
遊戲服務器運行中常常是多個玩家同時在線的,可想而知,若是同時進行某一項涉及數據庫的操做時,也會併發請求數據庫,多個數據庫請求就須要咱們對多個數據庫鏈接進行有效的管理,固然,咱們能夠本身寫一個數據庫卡鏈接池來進行數據庫管理,但好在之後前輩爲咱們作足了工做,有不少成型的開源數據庫鏈接池可供咱們選擇,常見的有c3p0、dbcp、proxool和driud等,這裏咱們使用阿里巴巴公司的開源產品Druid,這是我我的認爲最好用的數據庫鏈接池,它不只提供了數據庫鏈接池應有的功能,更是提供了良好的數據庫監控性能,這是咱們做爲開發人員在遇到性能瓶頸時最須要的東西,感興趣的朋友能夠參考下官方github,根據官方wiki配置一個Druid的數據監控系統,經過系統能夠查看數據庫的各類性能指標。
Druid在github中的地址是:https://github.com/alibaba/druid
在項目中使用druid,首先咱們須要導入druid所需jar包以及Mysql的驅動jar包,因爲咱們是maven項目,咱們就直接添加pom依賴,代碼以下:
<!--Mysql驅動--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.22</version> <scope>runtime</scope> </dependency> <!--數據庫鏈接池--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>0.2.26</version> </dependency>
在spring的xml中對druid進行配置
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.user}" /> <property name="password" value="${jdbc.password}" /> <property name="maxActive" value="${jdbc.maxActive}" /> <property name="initialSize" value="${jdbc.initialSize}" /> <!-- 配置獲取鏈接等待超時的時間 --> <property name="maxWait" value="${jdbc.maxWait}" /> <!-- 配置間隔多久才進行一次檢測,檢測須要關閉的空閒鏈接,單位是毫秒 --> <property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}" /> <!-- 配置一個鏈接在池中最小生存的時間,單位是毫秒 --> <property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}" /> <!--過濾 --> <property name="filters" value="config,wall,mergeStat" /> <!--密碼加密 --> <!-- property name="connectionProperties" value="config.decrypt=true" / --> <!--合併多個數據源 --> <property name="useGloalDataSourceStat" value="true" /> <property name="proxyFilters"> <list> <ref bean="log-filter" /> <ref bean="stat-filter" /> </list> </property> </bean> <bean id="log-filter" class="com.alibaba.druid.filter.logging.Log4jFilter"> <property name="statementLogErrorEnabled" value="true" /> <property name="statementLogEnabled" value="true" /> </bean> <bean id="stat-filter" class="com.alibaba.druid.filter.stat.StatFilter"> <property name="slowSqlMillis" value="1000" /> <property name="logSlowSql" value="true" /> </bean>
而後須要在web.xml中再對druid的filter進行配置:
<filter> <filter-name>DruidWebStatFilter</filter-name> <filter-class>com.alibaba.druid.support.http.WebStatFilter</filter-class> <init-param> <param-name>exclusions</param-name> <param-value>weburi.json,.html,.js,.gif,.jpg,.png,.css,.ico,/fonts/*,/datas/*,images/*</param-value> </init-param> <init-param> <param-name>sessionStatMaxCount</param-name> <param-value>1000</param-value> </init-param> <init-param> <param-name>principalSessionName</param-name> <param-value>FRONT_USER</param-value> </init-param> </filter> <filter-mapping> <filter-name>DruidWebStatFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>DruidStatView</servlet-name> <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>DruidStatView</servlet-name> <url-pattern>/druid/*</url-pattern> </servlet-mapping>
至此爲止,Druid的配置就算是完成,啓動工程以後咱們還能經過/druid路徑來訪問Druid提供的監控系統,更多關於Druid的使用能夠參照github中的wiki介紹,瞭解更多Druid配置及參數設置。
使用Hibernate做爲Mysql數據庫的ORM框架,主要是由於其良好的封裝,首先我我的認爲Hibernate的性能是不足與和原生JDBC以及MyBatis這樣的框架所匹敵的,封裝的更好卻帶來了更多的性能損失,但我使用他也是看中他良好的封裝性,由於我對性能的需求尚未達到很高的級別;其次,Hibernate很難寫出複雜的SQL查詢,而MyBatis卻能夠寫出一些複雜的SQL,但在個人設計中,我不須要太複雜的查詢,基本上我全部的SQL語句的where條件都是"where userid=?",所以在性能需求上以及易用的對比上,我選擇了Hibernate。
我使用的版本你是Hibernate4,由於Hibernate4提供了註解自動建立表的功能,Hibernate集成在spring的配置的xml代碼以下:
<!-- 定義事務管理 --> <bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory" /> </bean> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!-- 事務執行方式 REQUIRED:指定當前方法必需在事務環境中運行, 若是當前有事務環境就加入當前正在執行的事務環境, 若是當前沒有事務,就新建一個事務。 這是默認值。 --> <tx:method name="create*" propagation="REQUIRED" /> <tx:method name="save*" propagation="REQUIRED" /> <tx:method name="add*" propagation="REQUIRED" /> <tx:method name="update*" propagation="REQUIRED" /> <tx:method name="remove*" propagation="REQUIRED" /> <tx:method name="del*" propagation="REQUIRED" /> <tx:method name="import*" propagation="REQUIRED" /> <!-- 指定當前方法以非事務方式執行操做,若是當前存在事務,就把當前事務掛起,等我以非事務的狀態運行完,再繼續原來的事務。 查詢定義便可 read-only="true" 表示只讀 --> <tx:method name="*" propagation="NOT_SUPPORTED" read-only="true" /> </tx:attributes> </tx:advice> <!-- hibernate SessionFactory --> <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"> <!-- 數據源 --> <property name="dataSource" ref="dataSource" /> <!-- hibernate的相關屬性配置 --> <property name="hibernateProperties"> <value> <!-- 設置數據庫方言 --> hibernate.dialect=org.hibernate.dialect.MySQLDialect <!-- 設置自動建立|更新|驗證數據庫表結構 --> hibernate.hbm2ddl.auto=update <!-- 是否在控制檯顯示sql --> hibernate.show_sql=false <!-- 是否格式化sql,優化顯示 --> hibernate.format_sql=false <!-- 是否開啓二級緩存 --> hibernate.cache.use_second_level_cache=false <!-- 是否開啓查詢緩存 --> hibernate.cache.use_query_cache=false <!-- 數據庫批量查詢最大數 --> hibernate.jdbc.fetch_size=50 <!-- 數據庫批量更新、添加、刪除操做最大數 --> hibernate.jdbc.batch_size=50 <!-- 是否自動提交事務 --> hibernate.connection.autocommit=true <!-- 指定hibernate在什麼時候釋放JDBC鏈接 --> hibernate.connection.release_mode=auto <!-- 建立session方式 hibernate4.x 的方式 --> hibernate.current_session_context_class=thread <!-- javax.persistence.validation.mode默認狀況下是auto的,就是說若是不設置的話它是會自動去你的classpath下面找一個bean-validation**包 因此把它設置爲none便可 --> javax.persistence.validation.mode=none </value> </property> <!-- 自動掃描實體對象 tdxy.bean的包結構中存放實體類 --> <property name="packagesToScan" value="com.hjc._36" /> </bean>
配置完了以後,咱們須要對咱們須要進行存儲的數據進行註解,如君主信息的Model以下:
package com.hjc._36.manager.junzhu; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import com.hjc._36.util.cache.MCSupport; @Entity @Table(name = "JunZhu") public class JunZhu implements MCSupport { // id,用戶id,用戶名字,用戶所屬國家,用戶頭像(用數字表示),用戶等級,用戶vip等級,用戶軍令數,用戶金寶,用戶銀寶,用戶糧食,用戶精鐵,用戶木材, // 用戶兵數量,用戶軍工數,用戶將魂數 private static final long serialVersionUID = -5385044598250102957L; @Id public long id;// —用戶id public String name;// — 用戶名 @Column(columnDefinition = "INT default 1") public int headImg;// —用戶頭像 public int role;// —用戶角色 public int country;// —用戶所屬國家 1表示蜀國 2表示魏國 3表示吳國 @Column(columnDefinition = "INT default 0") public int exp;// —用戶等級 —君主等級 @Column(columnDefinition = "INT default 1") public int level;// —用戶等級 —君主等級 @Column(columnDefinition = "INT default 0") public int vip;// —用戶vip等級 @Column(columnDefinition = "INT default 0") public int vipExp;// —用戶vip經驗 @Column(columnDefinition = "INT default 0") public int junling;// —用戶軍令數 @Column(columnDefinition = "INT default 0") public int coin;// —用戶金幣 @Column(columnDefinition = "INT default 0") public int yuanbao;// —用戶元寶 @Column(columnDefinition = "INT default 0") public int food;// —用戶糧食 @Column(columnDefinition = "INT default 0") public int iron;// —用戶精鐵 @Column(columnDefinition = "INT default 0") public int wood;// —用戶木材 @Column(columnDefinition = "INT default 0") public int soldierNum;// —用戶兵力 @Column(columnDefinition = "INT default 0") public int jungongNum;// —用戶軍功數 @Column(columnDefinition = "INT default 0") public int jianghunNum;// —將魂數量 @Column(columnDefinition = "INT default 0") public int shengwang;// —聲望 @Column(columnDefinition = "INT default 0") public int zxId;// —陣型id @Column(columnDefinition = "INT default 0") public int paySum;// -充值總額 @Column(columnDefinition = "INT default 0") public int conduction;// -新手引導 // getter/setter }
以上代碼中,@Entity和@Table(name = "JunZhu")就可使Hibernate在啓動時自動建立一個JunZhu表,@Id的屬性即設爲主鍵的字段。建立好Model以後,就可使用Hibernate的session進行數據庫操做,這裏我將數據庫的操做封裝爲一個工具類HibernateUtil,這個工具類你們能夠拿去直接使用,具體代碼以下:
package com.hjc._36.util.hibernate; import java.lang.reflect.Field; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import net.sf.json.JSONObject; import org.hibernate.Query; import org.hibernate.SQLQuery; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.hibernate.annotations.Where; import org.hibernate.transform.Transformers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; import com.alibaba.fastjson.JSON; import com.hjc._36.core.GameInit; import com.hjc._36.manager.card.CardInfo; import com.hjc._36.manager.equip.EquipInfo; import com.hjc._36.manager.mail.MailInfo; import com.hjc._36.manager.pve.PveInfo; import com.hjc._36.manager.zhenxing.ZhenXingInfo; import com.hjc._36.manager.zhenxing.ZhenxingMgr; import com.hjc._36.task.ExecutorPool; import com.hjc._36.util.cache.MC; import com.hjc._36.util.cache.MCSupport; import com.hjc._36.util.memcached.MemcachedCRUD; public class HibernateUtil { public static boolean showMCHitLog = false; public static Logger log = LoggerFactory.getLogger(HibernateUtil.class); public static Map<Class<?>, String> beanKeyMap = new HashMap<Class<?>, String>(); public static long synDelayT = 0;// 延遲同步週期 private static SessionFactory sessionFactory; public static void init() { sessionFactory = buildSessionFactory(); } public static SessionFactory getSessionFactory() { return sessionFactory; } public static Throwable insert(Object o, long id) { Session session = sessionFactory.getCurrentSession(); session.beginTransaction(); try { session.save(o); session.getTransaction().commit(); if (MC.cachedList.contains(o.getClass().getSimpleName())) { synMC4Insert(o.getClass().getSimpleName(), o, String.valueOf(id)); } } catch (Throwable e) { log.error("0要insert的數據{}", o == null ? "null" : JSONObject .fromObject(o).toString()); log.error("0保存出錯", e); session.getTransaction().rollback(); return e; } return null; } /** * FIXME 不要這樣返回異常,沒人會關係返回的異常。 * * @param o * @return */ public static Throwable save(Object o, long id) { Session session = sessionFactory.getCurrentSession(); Transaction t = session.beginTransaction(); boolean mcOk = false; try { if (o instanceof MCSupport) { MCSupport s = (MCSupport) o;// 須要對控制了的對象在第一次存庫時調用MC.add MC.update(o, String.valueOf(s.getIdentifier()));// MC中控制了哪些類存緩存。 mcOk = true; // session.update(o); session.saveOrUpdate(o); } else { session.saveOrUpdate(o); } t.commit(); if (o instanceof MCSupport) { if (MC.cachedList.contains(o.getClass().getSimpleName())) { synMC4Save(o.getClass().getSimpleName(), o, String.valueOf(id)); } } } catch (Throwable e) { log.error("1要save的數據{},{}", o, o == null ? "null" : JSONObject .fromObject(o).toString()); if (mcOk) { log.error("MC保存成功後報錯,多是數據庫條目丟失。"); } log.error("1保存出錯", e); t.rollback(); return e; } return null; } public static Throwable update(Object o, String id) { Session session = sessionFactory.getCurrentSession(); Transaction t = session.beginTransaction(); try { if (o instanceof MCSupport) { MCSupport s = (MCSupport) o;// 須要對控制了的對象在第一次存庫時調用MC.add MC.update(o, String.valueOf(s.getIdentifier()));// MC中控制了哪些類存緩存。 session.update(o); } else { session.update(o); } t.commit(); if (o instanceof MCSupport) { if (MC.cachedList.contains(o.getClass().getSimpleName())) { synMC4Update(o.getClass().getSimpleName(), o, String.valueOf(id)); } } } catch (Throwable e) { log.error("1要update的數據{},{}", o, o == null ? "null" : JSONObject .fromObject(o).toString()); log.error("1保存出錯", e); t.rollback(); return e; } return null; } public static <T> T find(Class<T> t, long id) { String keyField = getKeyField(t); if (keyField == null) { throw new RuntimeException("類型" + t + "沒有標註主鍵"); } if (!MC.cachedClass.contains(t)) { return find(t, "where " + keyField + "=" + id, false); } T ret = MC.get(t, String.valueOf(id)); if (ret == null) { if (showMCHitLog) log.info("MC未命中{}#{}", t.getSimpleName(), id); ret = find(t, "where " + keyField + "=" + id, false); if (ret != null) { if (showMCHitLog) log.info("DB命中{}#{}", t.getSimpleName(), id); MC.add(ret, String.valueOf(id)); } else { if (showMCHitLog) log.info("DB未命中{}#{}", t.getSimpleName(), id); } } else { if (showMCHitLog) log.info("MC命中{}#{}", t.getSimpleName(), id); } return ret; } public static <T> T find(Class<T> t, String where) { return find(t, where, true); } public static <T> T find(Class<T> t, String where, boolean checkMCControl) { // if (checkMCControl && MC.cachedClass.contains(t)) { // // 請使用static <T> T find(Class<T> t,long id) // throw new BaseException("由MC控制的類不能直接查詢DB:" + t); // } Session session = sessionFactory.getCurrentSession(); Transaction tr = session.beginTransaction(); T ret = null; try { // FIXME 使用 session的get方法代替。 String hql = "from " + t.getSimpleName() + " " + where; Query query = session.createQuery(hql); ret = (T) query.uniqueResult(); tr.commit(); } catch (Exception e) { tr.rollback(); log.error("list fail for {} {}", t, where); log.error("list fail", e); } return ret; } public static <T> List<T> list(Class<T> t, long id, String where) { String keyField = getKeyField(t); if (keyField == null) { throw new RuntimeException("類型" + t + "沒有標註主鍵"); } if (!MC.cachedList.contains(t.getSimpleName())) { return list(t, where, false); } List<T> ret = MC.getList(t, String.valueOf(id), where); if (ret == null) { if (showMCHitLog) log.info("MC未命中{}#{}", t.getSimpleName(), where); ret = list(t, where, false); if (ret != null) { if (showMCHitLog) log.info("DB命中{}#{}", t.getSimpleName(), where); MC.addList(ret, t.getSimpleName(), String.valueOf(id), where); } else { if (showMCHitLog) log.info("DB未命中{}#{}", t.getSimpleName(), where); } } else { if (showMCHitLog) log.info("MC命中{}#{}", t.getSimpleName(), where); } return ret; } /** * @param t * @param where * 例子: where uid>100 * @return */ public static <T> List<T> list(Class<T> t, String where, boolean checkMCControl) { // if (checkMCControl && MC.cachedList.contains(t)) { // // 請使用static <T> T find(Class<T> t,long id) // throw new BaseException("由MC控制的類不能直接查詢DB:" + t); // } Session session = sessionFactory.getCurrentSession(); Transaction tr = session.beginTransaction(); List<T> list = Collections.EMPTY_LIST; try { String hql = "from " + t.getSimpleName() + " " + where; Query query = session.createQuery(hql); list = query.list(); tr.commit(); } catch (Exception e) { tr.rollback(); log.error("list fail for {} {}", t, where); log.error("list fail", e); } return list; } public static SessionFactory buildSessionFactory() { log.info("開始構建hibernate"); String path = "classpath*:spring-conf/applicationContext.xml"; ApplicationContext ac = new FileSystemXmlApplicationContext(path); sessionFactory = (SessionFactory) ac.getBean("sessionFactory"); log.info("結束構建hibernate"); return sessionFactory; } public static Throwable delete(Object o, long id) { if (o == null) { return null; } Session session = sessionFactory.getCurrentSession(); session.beginTransaction(); try { session.delete(o); session.getTransaction().commit(); if (o instanceof MCSupport) { MCSupport s = (MCSupport) o;// 須要對控制了的對象在第一次存庫時調用MC.add MC.delete(o.getClass(), String.valueOf(s.getIdentifier()));// MC中控制了哪些類存緩存。 if (MC.cachedList.contains(o.getClass().getSimpleName())) { synMC4Delete(o.getClass().getSimpleName(), o, String.valueOf(id)); } } } catch (Throwable e) { log.error("要刪除的數據{}", o); log.error("出錯", e); session.getTransaction().rollback(); return e; } return null; } /** * 注意這個方法會返回大於等於1的值。數據庫無記錄也會返回1,而不是null * * @param t * @return */ public static <T> Long getTableIDMax(Class<T> t) { Long id = null; Session session = sessionFactory.getCurrentSession(); Transaction tr = session.beginTransaction(); String hql = "select max(id) from " + t.getSimpleName(); try { Query query = session.createQuery(hql); Object uniqueResult = query.uniqueResult(); if (uniqueResult == null) { id = 1L; } else { id = Long.parseLong(uniqueResult + ""); id = Math.max(1L, id); } tr.commit(); } catch (Exception e) { tr.rollback(); log.error("query max id fail for {} {}", t, hql); log.error("query max id fail", e); } return id; } public static List<Map<String, Object>> querySql(String hql) { Session session = sessionFactory.getCurrentSession(); Transaction tr = session.beginTransaction(); List list = Collections.emptyList(); try { SQLQuery query = session.createSQLQuery(hql); query.setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP); list = query.list(); tr.commit(); } catch (Exception e) { tr.rollback(); log.error("query failed {}", hql); log.error("query count(type) fail", e); } return list; } }
以上代碼中,除了調用session的數據庫操做API以外,我還使用了Memcache進行結果集的緩存,具體關係Memcache和Mysql的集合使用,在下文中在進行講解。上文代碼中首先在服務器啓動時,須要構建SessionFactory,而後經過操做session開始事務,經過session調用CRUD方法進行操做,以後再調用commit方法提交併結束事務,中間若是發生異常則進行rollback操做回滾事務。
遊戲中的熱數據的存儲我選用了Redis,Redis不只是運行在內存上的內存數據庫,而且它的數據存儲結構也是很豐富的,包括String,Set,List,Sorted Set和Hash五種數據結構,我對遊戲數據的熱數據進行了分析,以下圖:
使用Redis首先得了解Redis的五種基本數據類型,每一種數據類型都對應不一樣的Redis操做API,在Java中使用Redis可使用官方提供的Jedis客戶端,Jedis客戶端中包含了各類數據類型的操做,我將全部的Redis操做都封裝在了Redis類中,啓動時調用init方法進行Redis鏈接,使用時經過getInstance獲取實例,再調用相應的API便可完成相關的Redis操做,在init方法中,我是經過調用JedisSentinelPool去獲取Redis的鏈接,由於我在服務器對Redis作了Sentinel的集羣部署,你們能夠直接拿這個Redis工具類去使用,Redis類的方法以下:
package com.hjc._36.util.redis; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.beanutils.BeanUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import redis.clients.jedis.HostAndPort; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisSentinelPool; import redis.clients.jedis.Tuple; import com.hjc._36.core.GameInit; public class Redis { private static Redis instance; public static Logger log = LoggerFactory.getLogger(Redis.class); public static final int GLOBAL_DB = 0;// 全局 public static final int LOGIC_DB = GameInit.serverId;// 模塊庫 public static String password = null; public static Redis getInstance() { if (instance == null) { instance = new Redis(); } return instance; } // private JedisPool pool; private JedisSentinelPool sentinelPool; public String host; public int port; // public int getDB(long userid){ // // } public Jedis getJedis() { return this.sentinelPool.getResource(); } public void returnResource(Jedis jedis) { jedis.close(); // this.sentinelPool.returnResource(jedis); } public void init() { String redisServer = null; if (GameInit.cfg != null) { redisServer = GameInit.cfg.get("redisServer"); password = GameInit.cfg.get("redisPwd"); } if (redisServer == null) { redisServer = "127.0.0.1:6440"; } redisServer = redisServer.trim(); String[] tmp = redisServer.split(":"); host = tmp[0]; port = Integer.parseInt(tmp[1]); if (tmp.length == 2) { port = Integer.parseInt(tmp[1].trim()); } log.info("Redis sentinel at {}:{}", host, port); // sentinelPool = new JedisPool(host, port); Set sentinels = new HashSet(); sentinels.add(new HostAndPort(host, port).toString()); sentinelPool = new JedisSentinelPool("master1", sentinels); } // private void init() { // String redisServer = null; // if (GameInit.cfg != null) { // redisServer = GameInit.cfg.get("redisServer"); // password = GameInit.cfg.get("redisPwd"); // } // if (redisServer == null) { // redisServer = "127.0.0.1:6379"; // } // redisServer = redisServer.trim(); // String[] tmp = redisServer.split(":"); // host = tmp[0]; // port = Integer.parseInt(tmp[1]); // if (tmp.length == 2) { // port = Integer.parseInt(tmp[1].trim()); // } // log.info("Redis at {}:{}", host, port); // // sentinelPool = new JedisPool(host, port); // JedisPoolConfig config = new JedisPoolConfig(); // sentinelPool = new JedisPool(config, host, port, 100000, password); // } public void test() { Jedis j = getJedis(); j.auth(password); returnResource(j); } public void select(int index) { Jedis j = getJedis(); j.auth(password); j.select(index); returnResource(j); } public boolean hexist(int db, String key, String field) { if (key == null) { return false; } Jedis redis = getJedis(); redis.auth(password); redis.select(db); boolean ret = redis.hexists(key, field); returnResource(redis); return ret; } public Long hdel(int db, String key, String... fields) { Jedis redis = getJedis(); redis.auth(password); redis.select(db); Long cnt = redis.hdel(key, fields); returnResource(redis); return cnt; } public String hget(int db, String key, String field) { if (key == null) { return null; } Jedis redis = getJedis(); redis.auth(password); redis.select(db); String ret = redis.hget(key, field); returnResource(redis); return ret; } public Map<String, String> hgetAll(int db, String key) { if (key == null) { return null; } Jedis redis = getJedis(); redis.auth(password); redis.select(db); Map<String, String> ret = redis.hgetAll(key); returnResource(redis); return ret; } public void hset(int db, String key, String field, String value) { if (field == null || field.length() == 0) { return; } if (value == null || value.length() == 0) { return; } Jedis redis = getJedis(); redis.auth(password); redis.select(db); redis.hset(key, field, value); returnResource(redis); } public void add(int db, String group, String key, String value) { if (value == null || key == null) { return; } Jedis redis = getJedis(); redis.auth(password); redis.select(db); redis.hset(group, key, value); returnResource(redis); } public void set(int db, String key, String value) { if (value == null || key == null) { return; } Jedis redis = getJedis(); redis.auth(password); redis.select(db); redis.set(key, value); returnResource(redis); } public String get(int db, String key) { Jedis redis = getJedis(); redis.auth(password); redis.select(db); String ret = redis.get(key); returnResource(redis); return ret; } /** * 添加元素到集合中 * * @param key * @param element */ public boolean sadd(int db, String key, String... element) { if (element == null || element.length == 0) { return false; } Jedis redis = getJedis(); redis.auth(password); redis.select(db); boolean success = redis.sadd(key, element) == 1; returnResource(redis); return success; } public boolean smove(int db, String oldKey, String newKey, String element) { if (element == null) { return false; } Jedis redis = getJedis(); redis.auth(password); redis.select(db); boolean success = (redis.smove(oldKey, newKey, element) == 1); returnResource(redis); return success; } public static void destroy() { getInstance().sentinelPool.destroy(); } // 中間省略一萬字:中間都是Redis的各類API的操做,須要瞭解的朋友能夠去看看Jedis的API文檔
上文在介紹Hibernate中說到了Memcache對Mysql結果集的緩存,Memcache做爲一種內存數據庫,常常用做應用系統的緩存系統,我也將Memcache引入到項目做爲Mysql數據結果集的緩存系統,其實在實現Memcache對Mysql查詢的緩存的過程當中,我曾進行了多種嘗試,具體有如下幾種緩存模型:
1.無緩存
這種方式不使用Memcache緩存,遊戲服務器的操做直接穿透到Mysql中,這種方式在高併發環境下容易引發Mysql服務器高負載狀況,以下圖所示:
2.查詢使用緩存,更新穿透到數據庫,數據庫同步數據到緩存
這種方式在客戶端表現來看能夠提供一部分速度,由於查詢操做都是基於緩存的,但實際上Mysql的負擔反而加大了,由於每個更新請求,都須要Mysql同步最新的查詢結果集給Memcache,由於每個更新操做都會帶來一個查詢操做,固然這個同步過程可使異步,可是就算咱們感覺不到這個同步的過程,但在實際上也是加大了數據庫的負載,以下圖所示:
3.更新和查詢都使用緩存,緩存按策略與數據庫進行同步
這種方式是比較好的方式,由於客戶端的全部操做都是被緩存給攔截下來了,全部操做均是基於緩存,不會穿透到數據庫,而緩存與數據庫之間能夠按照必定策略進行同步,如每5分鐘同步一次數據到數據庫等,具體同步策略可根據狀況具體調整,固然這種方式的缺陷就是一旦服務器宕機,那麼在上次同步到宕機這段時間之間的數據都會丟失,以下圖所示:
4.更新和查詢都是用緩存,更新操做同時穿透到數據庫,數據庫同步緩存的查詢
這種方式是我最終使用的方式,雖然更新操做穿透到數據庫,可是我能夠在保證查詢效率的同時,也保證數據的安全穩定性,由於每一步更新操做都是要進行數據庫存儲的,而且全部的查詢操做能夠直接在緩存中進行,以下圖所示:
須要支持緩存的類需實現MCSupport接口:
package com.hjc._36.util.cache; import java.io.Serializable; /** * 實現此接口後還須要再MC類中增長cachedClass,並使用 * com.qx.persistent.HibernateUtil.find(Class<T>, long) * 代替where進行查詢。 * 須要對控制了的對象在第一次存庫時調用MC.add,再調用HIbernateUtil.insert * @author 何金成 * */ public interface MCSupport extends Serializable{ long getIdentifier(); }
Memcache的工具類MemcacheCRUD實現了Memcache的鏈接,以及add,update,delete等操做,具體代碼以下:
package com.hjc._36.util.memcached; import java.util.Arrays; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.danga.MemCached.MemCachedClient; import com.danga.MemCached.SockIOPool; import com.hjc._36.core.GameInit; /** * @author 何金成 */ public class MemcachedCRUD { protected static Logger logger = LoggerFactory .getLogger(MemcachedCRUD.class); public static String poolName = "gameDBPool"; protected static MemCachedClient memCachedClient; protected static MemcachedCRUD memcachedCRUD = null; public static SockIOPool sockIoPool; private MemcachedCRUD() { } public void init() { sockIoPool = init(poolName, "cacheServer"); memCachedClient = new MemCachedClient(poolName); // if true, then store all primitives as their string value. memCachedClient.setPrimitiveAsString(true); } public static SockIOPool init(String poolName, String confKey) { // 緩存服務器 String cacheServers = null; if (GameInit.cfg != null) { cacheServers = GameInit.cfg.getServerByName(confKey); } String server[] = { "127.0.0.1:11211" }; if (cacheServers == null || "".equals(cacheServers)) { } else { server[0] = cacheServers; } // 建立一個鏈接池 SockIOPool pool = SockIOPool.getInstance(poolName); logger.info("鏈接池{}緩存配置 {}", poolName, Arrays.toString(server)); pool.setServers(server);// 緩存服務器 pool.setInitConn(50); // 初始化連接數 pool.setMinConn(50); // 最小連接數 pool.setMaxConn(500); // 最大鏈接數 pool.setMaxIdle(1000 * 60 * 60);// 最大處理時間 pool.setMaintSleep(3000);// 設置主線程睡眠時,每3秒甦醒一次,維持鏈接池大小 pool.setNagle(false);// 關閉套接字緩存 pool.setSocketTO(3000);// 連接創建後超時時間 pool.setSocketConnectTO(0);// 連接創建時的超時時間 pool.initialize(); return pool; } public static void destroy() { sockIoPool.shutDown(); } public static MemcachedCRUD getInstance() { if (memcachedCRUD == null) { memcachedCRUD = new MemcachedCRUD(); } return memcachedCRUD; } private static final long INTERVAL = 100; public boolean exist(String key) { return memCachedClient.keyExists(key); } public boolean add(String key, Object o) { return memCachedClient.add(key, o); } public boolean update(String key, Object o) { return memCachedClient.replace(key, o); } public boolean saveObject(String key, Object msg) { boolean o = memCachedClient.keyExists(key); if (o) {// 存在替換掉 return memCachedClient.replace(key, msg); } else { return memCachedClient.add(key, msg); } } public boolean keyExist(String key) { return memCachedClient.keyExists(key); } /** * delete * * @param key */ public boolean deleteObject(String key) { return memCachedClient.delete(key); } public Object getObject(String key) { Object obj = memCachedClient.get(key); return obj; } public static MemCachedClient getMemCachedClient() { return memCachedClient; } }
使用Memcache進行緩存,則須要封裝一個緩存的操做工具類MC,封裝各類緩存操做方法,具體代碼以下:
package com.hjc._36.util.cache; import java.util.HashSet; import java.util.List; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.alibaba.fastjson.JSON; import com.hjc._36.core.GameInit; import com.hjc._36.manager.bag.ItemInfo; import com.hjc._36.manager.building.BuildingInfo; import com.hjc._36.manager.card.CardInfo; import com.hjc._36.manager.equip.EquipInfo; import com.hjc._36.manager.junzhu.JunZhu; import com.hjc._36.manager.mail.MailInfo; import com.hjc._36.manager.pve.PveInfo; import com.hjc._36.manager.task.MainTaskInfo; import com.hjc._36.manager.tec.TecInfo; import com.hjc._36.manager.zhenxing.ZhenXingInfo; import com.hjc._36.util.memcached.MemcachedCRUD; public class MC { /** * 控制哪些類進行memcached緩存。 被控制的類在進行建立時,須要注意調用MC的add和hibernate的insert。 */ public static Set<Class<? extends MCSupport>> cachedClass = new HashSet<Class<? extends MCSupport>>(); public static Set<String> cachedList = new HashSet<String>(); static { cachedClass.add(JunZhu.class); // 添加須要緩存find操做的類 } static { cachedList.add(CardInfo.class.getSimpleName()); // 添加須要緩存list操做的類 } public static <T> T get(Class<T> t, String id) { if (!cachedClass.contains(t)) { return null; } StringBuffer key = new StringBuffer(); key.append(GameInit.serverId).append("#").append(t.getSimpleName()) .append("#").append(id); Object o = MemcachedCRUD.getInstance().getObject(key.toString()); return (T) o; } public static <T> List<T> getList(Class<T> t, String id, String where) { if (!cachedList.contains(t.getSimpleName())) { return null; } StringBuffer key = new StringBuffer(); key.append(GameInit.serverId).append("#").append(id).append("#") .append(t.getSimpleName()).append("#").append(where); Object o = MemcachedCRUD.getInstance().getObject(key.toString()); return (List<T>) o; } public static <T> String getListKeys(String tName, String id) { if (!cachedList.contains(tName)) { return null; } StringBuffer key = new StringBuffer(); key.append(GameInit.serverId).append("#").append(id).append("#") .append(tName); Object o = MemcachedCRUD.getInstance().getObject(key.toString()); return (String) o; } public static Object getValue(String key) { Object o = MemcachedCRUD.getInstance().getObject(key); return o; } public static boolean add(Object t, String id) { if (!cachedClass.contains(t.getClass())) { return false; } StringBuffer key = new StringBuffer(); key.append(GameInit.serverId).append("#") .append(t.getClass().getSimpleName()).append("#").append(id); return MemcachedCRUD.getInstance().add(key.toString(), t); } public static boolean addList(List list, String tName, String id, String where) { if (!cachedList.contains(tName)) { return false; } StringBuffer key = new StringBuffer(); key.append(GameInit.serverId).append("#").append(id).append("#") .append(tName); String c = key.toString(); key.append("#").append(where); Object tmp = MemcachedCRUD.getInstance().getObject(c); String keys = tmp == null ? "" : (String) tmp; if (keys.equals("")) { MemcachedCRUD.getInstance().add(c, key.toString()); } else if (!keys.contains(key.toString())) { MemcachedCRUD.getInstance().update(c, keys + "," + key.toString()); } return MemcachedCRUD.getInstance().add(key.toString(), list); } public static boolean addKeyValue(String key, Object value) { return MemcachedCRUD.getInstance().add(key, value); } public static void update(Object t, String id) { if (!cachedClass.contains(t.getClass())) { return; } StringBuffer key = new StringBuffer(); key.append(GameInit.serverId).append("#") .append(t.getClass().getSimpleName()).append("#").append(id); MemcachedCRUD.getInstance().update(key.toString(), t); } public static boolean updateList(List list, String tName, String id, String where) { if (!cachedList.contains(tName)) { return false; } StringBuffer key = new StringBuffer(); key.append(GameInit.serverId).append("#").append(id).append("#") .append(tName); String c = key.toString(); key.append("#").append(where); Object tmp = MemcachedCRUD.getInstance().getObject(c); String keys = tmp == null ? "" : (String) tmp; if (keys.equals("")) { MemcachedCRUD.getInstance().add(c, key.toString()); } else if (!keys.contains(key)) { MemcachedCRUD.getInstance().update(c, keys + "," + key.toString()); } return MemcachedCRUD.getInstance().update(key.toString(), list); } /** * 根據主鍵刪除緩存 * * @param obj * 刪除對象 * @param id * 主鍵id */ public static void delete(Class clazz, String id) { if (!cachedClass.contains(clazz)) { return; } StringBuffer key = new StringBuffer(); key.append(GameInit.serverId).append("#").append(clazz.getSimpleName()) .append("#").append(id); MemcachedCRUD.getInstance().deleteObject(key.toString()); } }
以上代碼中,find方法的緩存和list方法的緩存須要分別實現,find方法緩存只須要將類名和id做爲key,對象做爲value便可,而list的緩存不只須要緩存全部的結果集,還須要緩存全部的where查詢條件,根據類型查詢出where條件,而後根據where條件分別進行緩存。
HibernateUtil中使用緩存部分的java代碼以下,其中註釋的方法爲上面第二種緩存模型的實現(現已被我淘汰):
public static void synMC4Insert(String tName, Object o, String id) { String keys = MC.getListKeys(tName, id); if (keys != null && keys.length() > 0) {// 遍歷全部的where緩存 String[] wheres = keys.split(","); for (String where : wheres) { where = where.split("#")[3]; List list = MC.getList(o.getClass(), id, where); list.add(o); MC.updateList(list, tName, id, where); } } } public static void synMC4Save(String tName, Object o, String id) { String keys = MC.getListKeys(tName, id); if (keys != null && keys.length() > 0) {// 遍歷全部的where緩存 String[] wheres = keys.split(","); for (String where : wheres) { where = where.split("#")[3]; List list = MC.getList(o.getClass(), id, where); MCSupport mc = (MCSupport) o; boolean flag = false; int index = 0; for (Iterator iterator = list.iterator(); iterator.hasNext(); index++) { MCSupport tmpObj = (MCSupport) iterator.next(); if (tmpObj.getIdentifier() == mc.getIdentifier()) { list.set(index, o); flag = true; } } if (!flag) { list.add(o); } MC.updateList(list, tName, id, where); } } } public static void synMC4Update(String tName, Object o, String id) { String keys = MC.getListKeys(tName, id); if (keys != null && keys.length() > 0) {// 遍歷全部的where緩存 String[] wheres = keys.split(","); for (String where : wheres) { where = where.split("#")[3]; List list = MC.getList(o.getClass(), id, where); MCSupport mc = (MCSupport) o; int index = 0; for (Iterator iterator = list.iterator(); iterator.hasNext(); index++) { MCSupport tmpObj = (MCSupport) iterator.next(); if (tmpObj.getIdentifier() == mc.getIdentifier()) { list.set(index, o); break; } } MC.updateList(list, tName, id, where); } } } public static void synMC4Delete(String tName, Object o, String id) { String keys = MC.getListKeys(tName, id); if (keys != null && keys.length() > 0) {// 遍歷全部的where緩存 String[] wheres = keys.split(","); for (String where : wheres) { where = where.split("#")[3]; List list = MC.getList(o.getClass(), id, where); MCSupport mc = (MCSupport) o; for (Iterator iterator = list.iterator(); iterator.hasNext();) { MCSupport tmpObj = (MCSupport) iterator.next(); if (tmpObj.getIdentifier() == mc.getIdentifier()) { iterator.remove(); break; } } MC.updateList(list, tName, id, where); } } } // public static <T> void synchronizeDB2MemAsy(final Class<T> t, // final String id) { // ExecutorPool.dbThread.execute(new Runnable() { // @Override // public void run() { // String keys = MC.getListKeys(t.getSimpleName(), id); // if (keys != null && keys.length() > 0) {// 遍歷全部的where緩存 // String[] wheres = keys.split(","); // for (String where : wheres) { // where = where.split("#")[3]; // List<T> list = list(t, where, false); // MC.updateList(list, t.getSimpleName(), id, where); // if (showMCHitLog) { // log.info("DB 同步 MC 成功 t:{},where:{}", // t.getSimpleName(), where); // } // } // } // } // }); // } // // public static <T> void synchronizeDB2MemSyn(final Class<T> t, // final String id) { // String keys = MC.getListKeys(t.getSimpleName(), id); // if (keys != null && keys.length() > 0) {// 遍歷全部的where緩存 // String[] wheres = keys.split(","); // for (String where : wheres) { // where = where.split("#")[3]; // List<T> list = list(t, where, false); // MC.updateList(list, t.getSimpleName(), id, where); // if (showMCHitLog) { // log.info("DB 同步 MC 成功 t:{},where:{}", t.getSimpleName(), // where); // } // } // } // }
以上是本文對咱們這款遊戲中的數據管理的介紹,遊戲服務器中的各類數據是多種多樣的,咱們應該根據各類數據的各類性質,合理利用進行存取,以保證無論什麼類型的遊戲數據,在咱們的遊戲服務器中均可以安全穩定的運行,數據安全,玩家纔會放心!