前言
上一节我们在项目中引入了MongoDB作为持久化数据库,然后用了注册和登录两个例子分别展示了如何使用spring-data-mongo库进行数据插入与查询。
持久化数据库将数据写入硬盘,存储安全性较高,并且硬盘成本便宜,可以长久存放大量数据并按需扩容。
但是他有个缺点在于硬盘的io速度,较于内存底了不是一个数量级。而游戏玩家对于延迟是很敏感的,因此经常会使用缓存来进行热数据的存放,减少数据库的压力。
当然,现在市面上还有大量的游戏没有使用缓存数据库,不是不会用,而是没必要。比如滚服制的游戏,一个服务器同时在线可能就百多人,在登录时直接将玩家从数据库取出,存放在程序内存中进行读取修改完全是支撑得住,特意使用缓存数据库反而徒增成本。不仅是服务器硬件成本,也有技术成本,越复杂的架构,开发时候黑盒的地方越多,越容易出现不可预期的错误。
但是如果同时在线玩家量上去,就非常有必要引入缓存数据库了。
正文
redis安装
一样我这边不给出redis安装相关流程,读者可以自己选择本地安装、云redis、或者用docker快速创建。
安装完毕后给redis设置密码。
redis-cli
config set requirepass 123456
导入依赖
这里我用Redission,读者们也可以选择自己喜欢的Redis客户端库。
implementation 'org.redisson:redisson:3.27.2'
添加Redis配置
修改common.conf, 添加redis相关的配置
redis.host=redis://localhost:6379
redis.password=123456
修改CommonConfig.java,自动装载redis配置进来
@Getter
@Component
@PropertySource("classpath:common.conf")
public class CommonConfig {
...
@Value("${redis.host}")
String redisHost;
@Value("${redis.password}")
String redisPassword;
}
添加RedisService类
创建一个RedisService类,用来进行Redis相关操作。
@Slf4j
@Component
public class RedisService {
private RedissonClient redissonClient;
public void initRedisService(String url, String password) {
Config config = new Config();
config.useSingleServer()
.setAddress(url)
.setPassword(password);
redissonClient = Redisson.create(config);
log.info("redis service ok!");
}
public void set(String key, Object value) {
RBucket<Object> bucket = redissonClient.getBucket(key);
bucket.set(value);
}
public <T> T get(String key, Class<T> clz) {
RBucket<T> bucket = redissonClient.getBucket(key);
return bucket.get();
}
public Long increase(String key) {
return increase(key, 1L);
}
public Long increase(String key, long addValue) {
RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
return atomicLong.addAndGet(addValue);
}
}
这里我们提供了一个初始化方法,服务器启动时传入redis的地址和密码,创建一个单点的redis服务连接。
同时我们下面简单提供了redis的get、set方法,以及一个递增接口,用于递增生成账号Id。
接下来我们修改一下LoginMain的服务器启动流程。
@Override
protected void initServer() {
...
// redis服务启动
RedisService redisService = SpringUtils.getBean(RedisService.class);
redisService.initRedisService(commonConfig.getRedisHost(), commonConfig.getRedisPassword());
redisService.set("test", 10);
int test = redisService.get("test", Integer.class);
log.info("value = {}", test);
Long testIncrease = redisService.increase("test_increase", 1L);
log.info("test increase = {}", testIncrease);
log.info("LoginServer start!");
}
这里加了一点测试代码,等下记得删掉。
测试redis连接
启动LoginServer,查看打印日志:
使用redis生成唯一id
我们在org.common.uitls包下创建GenIdUtils.java,用于生成Id
package org.common.utils;
import ...
/**
* 生成Id相关工具
*/
public class GenIdUtils {
/**
* 生成ConnectId
*/
public static long genConnectId() {
// 先只用uuid的64位来做connectId,只要不超过64位理论上不会重复
return UUID.randomUUID().getLeastSignificantBits();
}
/**
* 生成账号Id
*/
public static long genAccountId() {
// 玩家的账号Id从1000000开始, 1000000之前的id做预留,万一有什么后续功能要用到
long baseAccountId = 1000000L;
RedisService redisService = SpringUtils.getBean(RedisService.class);
return baseAccountId + redisService.increase("genAccountId");
}
}
我们顺便把ConnectId的生成也放进来,这边不展示了大家自己改一下就好。
修改LoginProtoHandler的注册逻辑, 将accountId = 1L修改掉.
public static void onPlayerRegisterMsg(ConnectActor actor, PlayerMsg.C2SPlayerRegister up) {
...
long accountId = GenIdUtils.genAccountId();
...
}
测试注册生成accountId
修改ClientMain.java 中的登录注册测试逻辑
@Override
protected void handleBackGroundCmd(String cmd) {
if (cmd.equals("test")) {
channel.writeAndFlush("test".getBytes());
} else if (cmd.startsWith("register_")) {
String[] s = cmd.split("_");
if (s.length != 3) {
return;
}
String accountName = s[1];
String password = s[2];
PlayerMsg.C2SPlayerRegister.Builder builder = PlayerMsg.C2SPlayerRegister.newBuilder();
builder.setAccountName(accountName);
builder.setPassword(password);
Pack pack = new Pack(ProtoEnumMsg.CMD.ID.PLAYER_REGISTER_VALUE, builder.build().toByteArray());
byte[] data = PackCodec.encode(pack);
channel.writeAndFlush(data);
} else if (cmd.equals("login_")) {
String[] s = cmd.split("_");
if (s.length != 3) {
return;
}
String accountName = s[1];
String password = s[2];
PlayerMsg.C2SPlayerLogin.Builder builder = PlayerMsg.C2SPlayerLogin.newBuilder();
builder.setAccountName(accountName);
builder.setPassword(password);
Pack pack = new Pack(ProtoEnumMsg.CMD.ID.PLAYER_LOGIN_VALUE, builder.build().toByteArray());
byte[] data = PackCodec.encode(pack);
channel.writeAndFlush(data);
}
}
改为手动输入账号密码。
起服测试一下:
可以看到id已经是从1000000开始的自增id了。
总结
本节我们往项目中添加了缓存数据库redis,并使用redisson对其进行了操作,代码量较少但是很重要。
将accountId的生成改为由redis进行递增,因为redis的单线程特性,使用原子操作increase可以线程安全地获得一个accountId,未来若是多个注册请求同时到达,也不会出现重复id的问题。
至此我们已经将所有关键的外部组件都给装配到我们的项目中,包括持久化数据库MongoDB、缓存数据库Redis、协议序列化工具Protobuf。
接下来要做的就是对服务器架构进行开发,下一节开始笔者将对截至目前开发的所有代码进行一次整理,包括但不限于:
- 包名修改——规范包名,修改代码结构
- 优化代码——使用自定义注解优化代码,减少重复工作量
- 补全MongoService的增删改查接口