手遊服務端框架之配置與玩家數據庫設計

一款網絡遊戲的設計,至少需要兩種數據庫。策劃數據庫是表示遊戲玩法規則的數據庫;用戶數據庫是保存玩家個人信息的數據庫。除了這兩類基本的數據庫以外,還有其他數據庫。例如有些跨服玩法需要配置數據庫來尋找其他服務節點的鏈路地址;有些架構把日誌放到獨立的日誌數據庫進行統一管理,等等。

本文主要介紹玩法配置數據庫與玩家用戶數據庫。

策劃數據庫的概念

策劃數據庫,顧名思義,是策劃童鞋用於描述他心目中理想遊戲世界的手段,是遊戲的規則。例如,玩家當前級別可擁有的最大體力值是多少,長到下一級別需要獲得多少經驗,各種遊戲規則的設定都是通過該數據庫裏的各種數據表進行控制。也就是說,策劃配置表是遊戲的玩法,因此,除了策劃童鞋之外,絕不允許開發人員亂修改表內容。我曾呆過的一家遊戲項目,經常看到開發新手不小心在代碼裏修改了策劃數值,導致遊戲規則被修改了。這可是要扣績效的啊!!

用戶數據庫的概念

以前玩街機遊戲的時候,玩家的數據是無法保存的,一旦斷電了,那麼就GameOver了。在網絡遊戲時代,遊戲數據三是長時間保存的,那麼就需要數據庫來保存玩家的個人信息。打個比方,今天運氣非常好,打野怪刷到了一把極品裝備,如果沒有持久化機制,那麼玩家下線後再來玩,裝備就不見了。玩家數據是玩家的私有財產,如果代碼不小心把玩家的數據弄髒了,那麼就一定要想方設法來幫助玩家恢復數據或進行遊戲道具補償。玩家數據庫除了保存個人數據之外,還會保存一些公共數據,比如幫派數據是整個幫派成員共有的。

數據庫ORM方案

不管是什麼數據庫,都會涉及到數據的增刪查改操作。ORM(對象關係映射)是解決這些繁瑣重複工作的利器。需要注意的是,策劃配置表屬於遊戲規則,開發人員一般只有讀取的權限,而沒有修改的權限。

本文所採用的ORM框架在之前的文章 自定義orm框架解決玩家數據持久化問題 已有詳細介紹,這裏不作詳細介紹。

不同的是,orm工具這裏採用的數據庫連接池改爲Proxool庫;同時,爲了統一處理策劃庫與用戶庫,DbUtils工具類的多個方法增加一個參數,表示對應的數據庫別名,如下:

/**
	 * 查詢返回一個bean實體
	 * @param alias 數據庫別名
	 * @param sql
	 * @param entity
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static <T> T queryOne(String alias, String sql, Class<?> entity){
             
}

配置數據庫的設計

從策劃童鞋的角度上看,配置數據就是一張一張的excel表格。開發人員根據策劃的表設計,轉化成對應的數據庫表格式。程序啓動的時候,就會將所有的配置表都讀取到緩存裏,這樣程序的邏輯就會按給定的數值進行運行。當然,策劃表格不一定只能從數據庫讀取,有些項目連數據庫都取消了。他們把策劃配置的表格通過一種導表程序,轉換爲xml文件或csv文件,程序一樣可以讀取到內存。個人感覺,採用數據庫讀取配置比較方便,畢竟數據庫對開發人員來說比較友好。

下邊說明一下建立一張配置表的步驟:

1. 建立數據表結構(這個結果及即可以有程序制定,也可以由策劃制定,看項目),並加入若干測試數據

DROP TABLE IF EXISTS `configplayerlevel`;
CREATE TABLE `configplayerlevel` (
  `level` int(11) DEFAULT NULL,
  `needExp` bigint(20) DEFAULT NULL,
  `vitality` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of configplayerlevel
-- ----------------------------
INSERT INTO `configplayerlevel` VALUES ('1', '2345', '100');
INSERT INTO `configplayerlevel` VALUES ('2', '23450', '105');
2. 定義數據實體

/**
 * 玩家等級配置表
 * @author kingston
 */
@Entity(readOnly = true)
public class ConfigPlayerLevel {

	/**
	 * 等級
	 */
	@Column
	private int level;
	
	/**
	 * 升到下一級別需要的經驗
	 */
	@Column
	private long needExp;
	
	/**
	 * 最大體力
	 */
	@Column
	private int vitality;

	public int getLevel() {
		return level;
	}

	public void setLevel(int level) {
		this.level = level;
	}

	public long getNeedExp() {
		return needExp;
	}

	public void setNeedExp(long needExp) {
		this.needExp = needExp;
	}

	public int getVitality() {
		return vitality;
	}

	public void setVitality(int vitality) {
		this.vitality = vitality;
	}
	
}
3. 爲了方便管理表數據,對於每一張表都定義一個容器

/**
 * 玩家等級配置表
 * @author kingston
 */
public class ConfigPlayerLevelContainer implements Reloadable{
	
	private Map<Integer, ConfigPlayerLevel> levels = new HashMap<>();

	@Override
	public void reload() {
		String sql = "SELECT * FROM ConfigPlayerLevel";
		List<ConfigPlayerLevel> datas = DbUtils.queryMany(DbUtils.DB_DATA, sql, ConfigPlayerLevel.class);
		//使用jdk8,將list轉爲map
		levels = datas.stream().collect(
				Collectors.toMap(ConfigPlayerLevel::getLevel, e -> e));
	}
	
	public ConfigPlayerLevel getConfigBy(int level) {
		return levels.get(level);
	}
	
}

4. 容器表都實現Reloadable接口,該接口只有一個抽象方法,這樣方便服務啓動的時候能統一管理

public interface Reloadable {

	/**
	 * 重載數據
	 */
	void reload();
	
}
5. 爲了方便管理所有表數據,我們再定義一個配置數據池,每一個配置容器都在這裏進行申明。這樣做可以很方便在生產環境進行熱更新配置,關於熱更新配置的做法,後面文章再詳細介紹。該數據池還需要提供一個公有方法用於讀取全部配置數據。

/**
 * 所有策劃配置的數據池
 * @author kingston
 */
public class ConfigDatasPool {
	
	private static ConfigDatasPool instance = new ConfigDatasPool(); 
	
	private ConfigDatasPool() {}
	
	public static ConfigDatasPool getInstance() {
		return instance;
	}
	
	public ConfigPlayerLevelContainer configPlayerLevelContainer = new ConfigPlayerLevelContainer();

	/**
	 * 起服讀取所有的配置數據
	 */
	public void loadAllConfigs() {
		Field[] fields = ConfigDatasPool.class.getDeclaredFields();
		ConfigDatasPool instance = getInstance();
		for (Field f:fields) {
			try {
			if (Reloadable.class.isAssignableFrom(f.getType())) {
				Reloadable container = (Reloadable) f.getType().newInstance();
				System.err.println(f.getType());
				container.reload();
				f.set(instance, container);
			}
			}catch (Exception e) {
				LoggerUtils.error("策劃配置數據有誤,請檢查", e);
				System.exit(0);
			}
		}
		
	}
	
	
}

用戶數據庫設計

1. 用戶數據表的設計是由開發人員在實現業務需求時自行設計的。以前的一篇文章遊戲服務器關於玩家數據的解決方案 詳細說明了兩種用戶數據設計策略。由於當前涉及的用戶信息非常少,作爲演示,我們只用到一張數據表。(針對不同業務所需要的用戶信息保存方式,以後再作詳細展開)。用戶表的設計如下

DROP TABLE IF EXISTS `player`;
CREATE TABLE `player` (
  `id` bigint(20) NOT NULL,
  `name` varchar(255) DEFAULT NULL  COMMENT '暱稱',
  `job` tinyint(4) DEFAULT NULL  COMMENT '職業',
  `level` int(11) DEFAULT '1' COMMENT '等級',
  `exp` bigint(20) DEFAULT 0  COMMENT '經驗' ,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2. 用戶數據是需要持久化的,所以我們需要藉助orm框架的 AbstractCacheable類。同時爲了能夠將用戶數據放入哈希容器,我們有必要重寫object類的equals()和hashCode()方法。於是,有了下面的抽象類

/**
 * db實體基類
 * @author kingston
 */
public abstract class BaseEntity<Id extends Comparable<Id>> extends AbstractCacheable 
			implements Serializable {

	private static final long serialVersionUID = 5416347850924361417L;

	public abstract Id getId() ;

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((getId()==null)?0:getId().hashCode());
		return result;
	}

	@SuppressWarnings("rawtypes")
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		BaseEntity other = (BaseEntity) obj;
		if (getId() != other.getId())
			return false;
		return true;
	}

}
3. 定義用戶模型Player類,該類只需要繼承上面的BaseEntity抽象類即可。是不是很方便 ^_^

@Entity
public class Player extends BaseEntity<Long>{

	private static final long serialVersionUID = 8913056963732639062L;

	@Id
	@Column
	private long id;
	
	@Column
	private String name;
	
	/**
	 * 職業
	 */
	@Column 
	private int job;
	
	@Column
	private int level;
	
	@Column
	private long exp;
	
	public Player() {
		this.id = IdGenerator.getUid();
	}

	@Override
	public Long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getJob() {
		return job;
	}

	public void setJob(int job) {
		this.job = job;
	}

	public int getLevel() {
		return level;
	}

	public void setLevel(int level) {
		this.level = level;
	}

	public long getExp() {
		return exp;
	}

	public void setExp(long exp) {
		this.exp = exp;
	}

	@Override
	public String toString() {
		return "Player [id=" + id + ", name=" + name + ", job=" + job
				+ ", level=" + level + ", exp=" + exp + "]";
	}
	
}

用戶數據異步持久化

當玩家的數據發生變動時,我們需要將最新的數據保存到數據庫。這裏有一個問題,當玩家數據有部分變動的時候,我們不可能即使保存到數據庫的,這樣對數據庫的壓力太大。所以,我們需要有獨立線程來完成數據的異步保存。這裏又要搬出我們可愛的生產者消費者模型啦。

/**
 * 用戶數據異步持久化的服務
 * @author kingston
 */
public class DbService {
	
	private static volatile DbService instance;
	
	public static DbService getInstance() {
		if (instance ==  null) {
			synchronized (DbService.class) {
				if (instance ==  null) {
					instance = new DbService();
				}
			}
		}
		return instance;
	}
	
	/**
	 * 啓動消費者線程
	 */
	public void init() {
		new Thread(new Worker()).start();
	}
	
	@SuppressWarnings("rawtypes")
	private BlockingQueue<BaseEntity> queue = new BlockingUniqueQueue<>();
	
	private final AtomicBoolean run = new AtomicBoolean(true);
	
	public void add2Queue(BaseEntity<?> entity) {
		this.queue.add(entity);
	}
	
	
	private class Worker implements Runnable {
		@Override
		public void run() {
			while(run.get()) {
				try {
					BaseEntity<?> entity = queue.take();
					saveToDb(entity);
				} catch (InterruptedException e) {
					LoggerUtils.error("", e);
				}
			}
		}
	}
	
	/**
	 * 數據真正持久化
	 * @param entity
	 */
	private void saveToDb(BaseEntity<?> entity) {
		entity.save();
	}

}

到這裏,關於配置數據庫和用戶數據庫的概念及實現就介紹完畢了。


文章預告:下一篇主要介紹如何藉助谷歌的guanva工具來定製自己的緩存系統。
手遊服務端開源框架系列完整的代碼請移步github ->>game_server








發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章