序)沉迷游戲,只因不安於現狀卻又無法改變已逝的過去
昨天下午開始做一些細化的東西,其中就包括了用戶注冊的時候,系統隨機分配一個昵稱,這樣的情況幾乎在所有游戲中都可見到,不算什么稀奇的東西,策划要求的是只有漢字,必須看着像名字,不能重復,這樣我最開始想的直接隨機產生x個漢字的想法看來就太簡單了,並且很多游戲的隨機昵稱看着還像模像樣,確實是一個名字。
另外在這里還有個細節問題,當用戶打開注冊界面選擇角色的時候從服務器獲得了唯一性的角色名后,該角色名則不能再分配給其他人,當用戶注冊后則與之綁定,如果用戶選擇刷新重新獲取角色名,則之前角色名如何處置?當然有兩種方案:
1)直接丟掉,只要分配了即使用戶沒有綁定該角色名也廢棄
2)丟回池里,可再分配給其他人
第一種情況使用率太低,當太多用戶不停刷的時候,使用率更低了,於是我覺得第二種比較好,鑒於名字要像模像樣這樣的情況看來必須走詞庫進行組合了,當然百家姓要拉下來,於是我自己動手整理了如下三個文件:
依次為:姓氏 ----> 女性角色名 ----> 男性角色名
之所以要把男女分開,那是因為男性老是分配到柔柔,瑤瑤,這樣的昵稱,估計瞬間就會開口大罵,為了我少挨點罵,於是把他們分開了,另外由於詞庫比較大,組合后的數據更加大,這樣的分開也能夠提高速度,在剛剛開始的時候我犯了一個很大的錯誤,那就是把問題想簡單了,我最開始的想法是:
1)根據性別從woman.txt或者man.txt里隨機選擇一個名字,再隨機在姓氏文件中選取一個姓氏,組合成名字
2)判斷該角色名是否已經被注冊,如果已經被注冊,跳轉到1
3)判斷該角色是否存在於Set集合(個Set集合用來保存當前已經分配出去的,但是用戶還未綁定的角色名)
4)丟入Set集合,發給客戶端
5)用戶刷新角色則將該角色從Set移除,以便復用
做完后客戶端測試了從功能上講沒啥問題,但是做完后馬上自己就發現這里有很大的問題,首先走了詞庫,有IO問題,這里的詞庫男女不同的角色名分別各有100萬左右,其次當用戶數量太大的時候,剩下的未使用的角色名被抽中的概率會變得很小很小,越靠后分配時間越久,特別是詞庫快要耗盡的時候,剩下未分配的名稱幾乎不會再被分配到,甚至會死在這里,想到這里瞬間精神了,粗心害死人啊。
於是打算重寫這一塊,首先不能使用隨機數,隨機就會出問題,另外因為詞庫的大小,正式上線的時候根據詞庫組合出來的用戶名可能會有上千萬,因此這樣的數據肯定不能直接丟內存,於是我的大概處理方式如下:
1)分別創建男性角色名表與女性角色名表,將所有昵稱初始化到數據庫,作為一個池。
2)分別創建男性角色名視圖與女性角色名視圖
3)服務器啟動后初始化角色名表的數據,保證里面都是沒有被分配的數據
4)初始化男性與女性昵稱訪問游標,游標與數據庫里的行index對應
5)每次用戶請求隨機昵稱的時候將游標的位置下移一位,取出里面的值發給客戶端
6)即使用戶不使用該名字,要求刷新名字,游標依舊保持下移
這樣游標始終取一下個值,則肯定不會重復,省掉了取出了還要和數據庫比對的情況,並且這里的游標一定不能后退,否則就會出現一個用戶刷新昵稱不停在兩個昵稱中切換的情況,這樣估計就要罵娘了,這樣做了后還有幾個問題就是如何保證使用率,就是游標移過了,但是該昵稱沒有被使用的數據,能夠再次被分配到,於是我的想法是刷新數據表,重置游標位置。每天或者每周刷新一次表,重置游標位置為1,這樣則可合理使用里面的數據。
這里還要處理一些其他並發問題,比如游標移動要保證唯一,另外刷新后台數據表的時候不能讓游標移動,否則會出現臟數據。
實現取隨機昵稱的代碼:
private volatile int currentMaleRoleNameCursorIndex = 1; private volatile int currentFemaleRoleNameCursorIndex = 1; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); private boolean isRefurbish = false; public static final int DEFAULT_INDEX_VALUE = 1; private interface SexType { int WOMAN = 0; int MAN = 1; } public String getRandomRoleName(int sex) { try { lock.lock(); while (isRefurbish) { condition.await(); } String name = null; switch (sex) { case SexType.MAN: name = this.jdbcAccess.findString("select * from v_male_role_name_table where index = ?", currentMaleRoleNameCursorIndex, sex); currentMaleRoleNameCursorIndex += 1; break; case SexType.WOMAN: name = this.jdbcAccess.findString("select * from v_female_role_name_table where index = ?", currentFemaleRoleNameCursorIndex, sex); currentFemaleRoleNameCursorIndex += 1; break; default: break; } return StringUtils.hasText(name) ? name : "notFound"; } catch (Exception e) { e.printStackTrace(); return "error"; } finally { lock.unlock(); } }
Quartz中的任務每天刷新池里的數據:
@Inject private RandomHan randomHan; private static class RoleItems { private int id; private String name; public static RoleItems setValues(ResultSet resultSet) throws SQLException { RoleItems item = new RoleItems(); item.id = resultSet.getInt("id"); item.name = resultSet.getString("name"); return item; } } public void refurbishRandomNameTable() { randomHan.setRefurbish(true); List<RoleItems> cacheMaleNameItems = this.jdbcAccess.find("select t.id, t.name from v_male_role_name_table where 1 = 1 and index <= ?", new RowMapper<RoleItems>() { @Override public RoleItems mapRow(ResultSet resultSet, int rowNum) throws SQLException { return RoleItems.setValues(resultSet); } }, randomHan.getCurrentMaleRoleNameCursorIndex()); for (RoleItems item : cacheMaleNameItems) { if (isExistRoleName(item.name)) { this.jdbcAccess.execute("delete from male_role_name_table where id = ?", item.id); } } randomHan.setCurrentMaleRoleNameCursorIndex(RandomHan.DEFAULT_INDEX_VALUE); List<RoleItems> cacheFemaleNameItems = this.jdbcAccess.find("select t.id, t.name from v_female_role_name_table where 1 = 1 and index <= ?", new RowMapper<RoleItems>() { @Override public RoleItems mapRow(ResultSet resultSet, int rowNum) throws SQLException { return RoleItems.setValues(resultSet); } }, randomHan.getCurrentMaleRoleNameCursorIndex()); for (RoleItems item : cacheFemaleNameItems) { if (isExistRoleName(item.name)) { this.jdbcAccess.execute("delete from female_role_name_table where id = ?", item.id); } } randomHan.setCurrentFemaleRoleNameCursorIndex(RandomHan.DEFAULT_INDEX_VALUE); randomHan.setRefurbish(false); } private boolean isExistRoleName(String nickName) { return this.jdbcAccess.findInteger("select count(*) from player_role where nick_name = ?", nickName) > 0; }
數據庫基本操作自己重新封裝了下,如下:
public class JDBCAccess { private final Logger logger = LoggerFactory.getLogger(JDBCAccess.class); private JdbcTemplate jdbcTemplate; public <T> List<T> find(String sql, RowMapper<T> rowMapper, Object... params) { StopWatch watch = new StopWatch(); try { return jdbcTemplate.query(sql, params, rowMapper); } finally { logger.debug("find, sql={}, params={}, elapsedTime={}", new Object[]{sql, params, watch.elapsedTime()}); } } public <T> T findUniqueResult(String sql, RowMapper<T> rowMapper, Object... params) { StopWatch watch = new StopWatch(); try { return jdbcTemplate.queryForObject(sql, params, rowMapper); } finally { logger.debug("findUniqueResult, sql={}, params={}, elapsedTime={}", new Object[]{sql, params, watch.elapsedTime()}); } } public int findInteger(String sql, Object... params) { StopWatch watch = new StopWatch(); try { return jdbcTemplate.queryForInt(sql, params); } finally { logger.debug("findInteger, sql={}, params={}, elapsedTime={}", new Object[]{sql, params, watch.elapsedTime()}); } } public boolean findBooleanFiled(String sql, Object... params) { StopWatch watch = new StopWatch(); try { return jdbcTemplate.queryForObject(sql, params, new RowMapper<Boolean>() { @Override public Boolean mapRow(ResultSet resultSet, int rowNum) throws SQLException { return resultSet.getBoolean(1); } }); } catch(Exception e) { return false; } finally { logger.debug("findString, sql={}, params={}, elapsedTime={}", new Object[]{sql, params, watch.elapsedTime()}); } } public Long findLongFiled(String sql, Object... params) { StopWatch watch = new StopWatch(); try { return jdbcTemplate.queryForObject(sql, params, new RowMapper<Long>() { @Override public Long mapRow(ResultSet resultSet, int rowNum) throws SQLException { return resultSet.getLong(1); } }); } finally { logger.debug("findString, sql={}, params={}, elapsedTime={}", new Object[]{sql, params, watch.elapsedTime()}); } } public int findIntegerFiled(String sql, Object... params) { StopWatch watch = new StopWatch(); try { return jdbcTemplate.queryForObject(sql, params, new RowMapper<Integer>() { @Override public Integer mapRow(ResultSet resultSet, int rowNum) throws SQLException { return resultSet.getInt(1); } }); } catch (Exception e) { return -1; } finally { logger.debug("findString, sql={}, params={}, elapsedTime={}", new Object[]{sql, params, watch.elapsedTime()}); } } public String findString(String sql, Object... params) { StopWatch watch = new StopWatch(); try { return jdbcTemplate.queryForObject(sql, params, new RowMapper<String>() { @Override public String mapRow(ResultSet resultSet, int rowNum) throws SQLException { return resultSet.getString(1); } }); } finally { logger.debug("findString, sql={}, params={}, elapsedTime={}", new Object[]{sql, params, watch.elapsedTime()}); } } public int execute(String sql, Object... params) { StopWatch watch = new StopWatch(); try { return jdbcTemplate.update(sql, params); } finally { logger.debug("execute, sql={}, params={}, elapsedTime={}", new Object[]{sql, params, watch.elapsedTime()}); } } public int[] batchExecute(String sql, List<Object[]> params) { StopWatch watch = new StopWatch(); try { return jdbcTemplate.batchUpdate(sql, params); } finally { logger.debug("batchExecute, sql={}, params={}, elapsedTime={}", new Object[]{sql, params, watch.elapsedTime()}); } } public void setDataSource(DataSource dataSource) { jdbcTemplate = new JdbcTemplate(dataSource); } public JdbcTemplate getJdbcTemplate() { return jdbcTemplate; } }
這里還有一個問題,那就是用戶刷新昵稱后應該是不一樣的姓氏的昵稱,這個可以使用游標解決,也可是在初始化數據里做手腳,不過再說吧,總覺得這個東西自己也搞的很挫了,先在這里記錄下吧。。。