程序员的悲哀: 996嫌累,摸鱼觉得没意思,使用开源库觉得没技术含量,自己造轮子又太累,写代码羡慕领导写PPT..
刚看到个贴子,说程序员的悲哀就是:996嫌累,摸鱼没意思,用开源库被嫌没技术,自己造轮子又太累,写代码羡慕写PPT,写PPT又怕没硬实力。转行嫌工资低,留下来又不喜欢,进体制怕平庸,私企又担心不稳……一辈子眼高手低,满是遗憾。
我觉得这事吧,说到底是欲望和现实的拉扯。想要安稳,又想要激情;想要自由,又想要高薪。可哪有那么完美的工作?网友有的说“程序员就注定是工具人”,也有人调侃“羡慕领导写PPT”。但我更认同一句话:职业没有完美,只有取舍。就像买菜,你要便宜的就别指望品质顶尖,要品质好的就得掏更多钱。
换个角度想,遗憾并不可怕,关键是你能不能找到一个自己愿意为之忍耐的点。别盯着对比,别被幻想绑架。能学点真东西,能攒点钱,能保留点自由,其实就挺好了。【备注:文末可领最新资料】
算法题:查找僵尸会话
昨天晚上十一点多在公司楼下吹风,手机还震个不停,小李又在群里喊:谁的会话表长到一百多万了啊,登录一片绿但是人都走光了…我一听就知道,八成是“僵尸会话”在作妖。就是那个…登录以后没正常登出、心跳也没上报,服务端却一直以为它活着,对吧。
先说清楚目标啊:我们要“找出来 + 安全清理”,别把还活着的干掉。思路别死抠数据库扫全表,太慢。更像一次小型 GC:先“标记”可疑,再“复核”,最后“清扫”。我习惯分两层:内存级极速判断,分布式上再做一次温和确认。
算法口径我就这么定(口头化一点哈):有 lastSeen(最后活跃),有 version(心跳递增),有 status(NORMAL/LOGOUT)。第一轮把 now-lastSeen>idleMs 的记成“疑似”;第二轮对疑似做一次“触碰”(CAS 改下 version 或发心跳请求),若仍不变 → 僵尸,删。期间要限流,别一口气把库打爆。等等我接个电话…好,继续。
来个 Java 小骨架,够用也好改:
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
publicclassSessionManager{
privatestaticfinallong IDLE_MS = TimeUnit.MINUTES.toMillis(30); // 闲置阈值
privatestaticfinalint BATCH = 2000; // 批次清理,防止抖
privatefinal ConcurrentHashMap<String, Sess> sessions = new ConcurrentHashMap<>();
privatefinal ScheduledExecutorService es = Executors.newScheduledThreadPool(2);
publicSessionManager(){
// 标记阶段:快扫
es.scheduleAtFixedRate(this::mark, 1, 5, TimeUnit.MINUTES);
// 复核+清扫阶段:慢一点但稳
es.scheduleAtFixedRate(this::sweep, 2, 5, TimeUnit.MINUTES);
}
publicvoidtouch(String sid){
Sess s = sessions.computeIfAbsent(sid, k -> new Sess());
s.lastSeen = System.currentTimeMillis();
s.version.incrementAndGet();
s.status = Status.NORMAL;
}
publicvoidlogout(String sid){
Sess s = sessions.get(sid);
if (s != null) s.status = Status.LOGOUT;
sessions.remove(sid);
}
privatevoidmark(){
long now = System.currentTimeMillis();
int n = 0;
for (Map.Entry<String, Sess> e : sessions.entrySet()) {
if (n >= BATCH) break;
Sess s = e.getValue();
if (s.status == Status.LOGOUT) { sessions.remove(e.getKey()); continue; }
if (now - s.lastSeen > IDLE_MS) {
s.suspect = true; // 标记疑似
n++;
}
}
}
privatevoidsweep(){
long now = System.currentTimeMillis();
int n = 0;
for (Map.Entry<String, Sess> e : sessions.entrySet()) {
if (n >= BATCH) break;
Sess s = e.getValue();
if (!s.suspect) continue;
long before = s.version.get();
// 轻触碰:尝试“虚拟心跳”,只有非并发更新时才会+1
boolean poked = s.version.compareAndSet(before, before + 1);
// 复核条件:仍旧超时且 version 没被真实业务改写
if (now - s.lastSeen > IDLE_MS && poked) {
sessions.remove(e.getKey()); // 清扫
} else {
s.suspect = false; // 误杀回退
}
n++;
}
}
staticclassSess{
volatilelong lastSeen = System.currentTimeMillis();
final AtomicLong version = new AtomicLong(0);
volatileboolean suspect = false;
volatile Status status = Status.NORMAL;
}
enum Status { NORMAL, LOGOUT }
}
这个小玩意儿几点小心思:一是两阶段,降低误杀;二是批次处理,防止“清理雪崩”;三是用 version 当“活性信号”,有真实请求会更新它,我们的“虚拟心跳”只是个探针,能测出是否有并发写入。你们知道吧,这比只看 lastSeen 稳多了。
要是分布式呢?我一般把“源数据”放 Redis,key 里带 TTL(自然过期最香),另外建个“迟迟不死”的索引集合(Sorted Set:score=lastSeen)。扫法就变成:按 score 范围拉一小段,做一次二次确认,然后用 Lua 原子删(带 sessionVersion 对比),避免删错别人的最新会话。伪代码就一句:if get(verKey)==ver && now-lastSeen>idle then delAll(sid*) end,锁都省了,哦对,节点多时要用消费者组或哈希槽路由把扫描分片,不然大家都去扫一个区间,会打架。
还有小李老忘的两个坑我顺嘴说下:WebSocket/长轮询要上心跳帧(例如 30s ping-pong),不然后端永远看不到活性;反向代理(Nginx/ELB)超时时间别比会话 TTL 大太多,不然网关复用连接,业务层以为链接还“亮着”。我现在困死了…先这样,回头谁把那张百万级会话的表跑个 TTL 覆盖脚本,别等凌晨了。
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html