(原)游戏用户注册之随机昵称系统
序)沉迷游戏,只因不安于现状却又无法改变已逝的过去
昨天下午开始做一些细化的东西,其中就包括了用户注册的时候,系统随机分配一个昵称,这样的情况几乎在所有游戏中都可见到,不算什么稀奇的东西,策划要求的是只有汉字,必须看着像名字,不能重复,这样我最开始想的直接随机产生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; } }
这里还有一个问题,那就是用户刷新昵称后应该是不一样的姓氏的昵称,这个可以使用游标解决,也可是在初始化数据里做手脚,不过再说吧,总觉得这个东西自己也搞的很挫了,先在这里记录下吧。。。