游戏开发之Redis篇(一)
游戏开发已近两年了,在这里总结一下游戏中常遇到的坑,希望能帮助到大家
1. 游戏高并发问题
在多线程并发的情况下,我们可以使用加锁的方式来实现在同一时间只能由一个线程进行访问,在这里使用了redis分布式锁进行实现
redis分布式锁常见问题:
(1)setnx刚成功,线程直接挂掉,没有设置过期时间,key锁死
(2)线程A成功加锁,设置超时时间,A线程较慢,锁过期B线程加锁成功,A线程解锁,删除为B线程锁
下面为解决方案:
分布式锁分为三个核心部分:加锁、锁过期、解锁,下面为基本流程
- 加锁
1)redis.setnx(lockkey, 当前时间+过期超时时间),如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向2
lockkey: 锁的唯一标识,根据业务命名,如房间号:lock+roomNum
- 锁过期
2)redis.get(lockkey)获取值oldExpireTime,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向3
- 新线程加锁
3.)计算newExpireTime=当前时间+过期超时时间,然后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime
4.)判断currentExpireTime与oldExpireTime 是否相等,如果相等,说明当前getset设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
5.)在获取到锁之后,当前线程可以开始做自增操作,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行delete释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。
- 解锁
.......代码逻辑执行完成
redis.del(key,newtime)
6.) 如果在执行过程中redis发生错误,则直接返回true,即不影响正常流程,不再考虑此操作的并发覆盖问题。
下面为分布式锁伪代码如下:
redis.setnx(lockkey,new Date()+200,function(err,result){
if(err){
....
}else{
if(result==1){
......
}else{
redis.get(lockkey,function(err,oldtime){
if(err){
....
}else{
if(oldtime<=new Date()){
redis.getset(locktime,new Data()+200,function(err,currentime){//currenttime是redis.set之前的值
if(currenttime==oldtime){
....
}else{
已锁住,重新加锁
}
})
}else{
已锁住,重新加锁
}
}
})
}
}
})
if (newtime >= new Date()) {
redis.del("lock" + roomid);
}
2. Redis键过期
- 修改redis配置文件
- 查找redis配置文件
whereis redis
- 修改配置文件
notify-keyspace-events "" 改为 notify-keyspace-events "Ex"
- 重启redis
systemctl restart redis
- 项目中添加redis订阅过期事件,然后监听
订阅:redis.subscribe('__keyevent@0__:expired')
监听:redis.on('message', function (err, result) { 键过期处理逻辑 })
注意:
1、监听代码每执行一次就会有一个监听,执行多次的话一个键过期会被监听多次,所有尽量不要重复执行监听代码(根据业务需求而定)
2、监听会监听这个redis数据库上发生的所有键过期事件(可以利用这个特性实现不同进程之间的交互),可以根据键名(上文的result)去区分是否是自己需要的键过期事件
(注:如果不监听键过期事件,就不用改redis配置,直接设置键过期即可:
// 键过期设置
redis.expire(键名(string),过期时间(number,秒));
或者
redis.set(键名(string),value,"Ex",过期时间(number,秒));
)
游戏开发之Socket篇(一)
在游戏开发中,必然会用到socket,在这里总结一下游戏中的巨无霸坑-心跳与断线重连机制
1. Socket重连机制
- socket初始化
function initSocket(){
let socket = new websocket(url);
socket.onclose = function(){//连接关闭
}
socket.onerror = function(){//连接异常
}
socket.onopen = function(){//连接成功
}
socket.onmessage = function(){//收到消息
}
}
在弱网、断网、切换后台,打电话等一些情况都可能会造成发送和接收消息异常,从而触发onclose和onerror,所以我们只需要在这两个方法中重新创建长连接即可
- 添加断线重连方法
function reconnect(url){ //断线重连,设置延迟时间避免频繁请求产生过多请求
let reconnectionDelay = 1000; //重新连接延迟时间
let reconnectTime = setTimeout(function(){
initSocket(); //重新连接
},reconnectionDelay)
}
- 多次触犯重连问题
解决方法:加锁
let lock = false; //锁
function reconnect(url){ //断线重连,设置延迟时间避免频繁请求产生过多请求
let reconnectionDelay = 1000; //重新连接延迟时间
if(lock) return;
lock = true;
let reconnectTime = setTimeout(function(){
initSocket(); //重新连接
lock = false;
},reconnectionDelay)
}
2. 心跳包机制
断开连接都是被动的,所以我们可以通过心跳机制,通过客户端与服务端约定时间间隔,如果在约定时间收不到心跳或者其他消息时,客户端主动进行重连操作
let hearbeatId; //心跳标识
var hearbeat = {
heartbeatInterval = 1000; //心跳间隔
heartbeatTimeout = heartbeatInterval * 2; //最大心跳超时
reset : function(){ //重置心跳
clearTimeout(hearbeatId); //清理定时
return;
}
start : function(){ //开启心跳
hearbeatId = setTimeout(function(){
socket.close(); //心跳超时,主动断开连接,进行重连
},heartbeatInterval)
}
}
- 当socket触发onopen、onmessage时重置心跳,开启下一个心跳
下面为伪代码如下:
function initSocket(){
let socket = new websocket(url);
socket.onclose = function(){//连接关闭
reconnect(url);
}
socket.onerror = function(){//连接异常
reconnect(url);
}
socket.onopen = function(){//连接成功
hearbeat.reset();
hearbeat.start();
}
socket.onmessage = function(){//收到消息
hearbeat.reset();
hearbeat.start();
}
}