当前位置:首页 » 《休闲阅读》 » 正文

统计用户在线时长(Redis、web-socket)

15 人参与  2024年11月13日 17:21  分类 : 《休闲阅读》  评论

点击全文阅读


所有方案均基于用户异常下线。非正常调用登出API下线

方案一

        心跳机制:用户调用登陆API后,持续向服务端发送心跳,向服务端告知自己健康。等服务端x分钟没有收到客户端的心跳。则视为用户下线,记录下线时间。

        实现:基于Redis的 zset特性

        用户调用登陆API后,将用户的登录时间记录在LOGIN_KEY

        客户端启用心跳(调用心跳API),记录HEART_KEY

        服务端启动轮询任务:

                1.通过 zset.rangeByScore 查询x分钟以前数据

                2.取出用户最新心跳时间(可能在x分钟内再次有了心跳,所以要取最新的)

                3.计算最后心跳时间和当前时间间隔,如果超过x分钟,则为离线

                4.如果离线,调用 logout,并计算用户时长,将用户时长记录在 ONLINE_KEY

@Slf4j@Componentpublic class OnlineService {    /**     * 登陆key     */    private static final String LOGIN_KEY = "online:login";    /**     * 在线时长     */    private static final String ONLINE_KEY = "online:live";    /**     * 心跳     */    private static final String HEART_KEY = "online:heart";    //默认4分钟判定为离线    private static final Integer DEAD_LINE = 4*60;    @Autowired    private RedisTemplate<String,Object> redisTemplate;    private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();    /**     * 退出     *     * @param userId     */    public void login(String userId) {        redisTemplate.opsForHash().putIfAbsent(LOGIN_KEY, userId, System.currentTimeMillis());    }    public void loginOut(String userId) {        Object loginTime = redisTemplate.opsForHash().get(LOGIN_KEY, userId);        if (loginTime != null) {            doLogout(userId, loginTime, Instant.now());            log.info("user {} is logout", userId);        }    }    private void doLogout(String userId, Object loginTime, Instant lastTime) {        Instant login = Instant.ofEpochMilli(Long.valueOf(loginTime.toString()));        Duration liveDuration = Duration.between(login, lastTime);        redisTemplate.opsForZSet().add(ONLINE_KEY, userId, liveDuration.getSeconds());        //删除登陆记录        redisTemplate.opsForHash().delete(LOGIN_KEY, userId);    }    /**     * 心跳     *     * @param userId     */    public void heartBeat(String userId) {        redisTemplate.opsForZSet().add(HEART_KEY, userId, System.currentTimeMillis());    }    /**     * 获取用户登陆时长     * @param userId     * @return     */    public Long getOnlineDuration(String userId) {        Double score = redisTemplate.opsForZSet().score(ONLINE_KEY, userId);        return score != null ? score.longValue() : null;    }    @PostConstruct    public void offlineJob() {        //假定3分钟没有心跳就是离线        scheduledExecutorService.scheduleAtFixedRate(() -> {            Instant now = Instant.now();            log.info("online scan: {}",                    LocalDateTime.ofInstant(now, ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("yyyy-MM" +                            "-dd HH:mm:ss")));            Instant deadline = now.minusSeconds(DEAD_LINE);            //查询4分钟以前数据            Set<Object> userListTmp = redisTemplate.opsForZSet().rangeByScore(HEART_KEY, 0, deadline.toEpochMilli());            if (CollectionUtils.isEmpty(userListTmp)) {                log.info("无记录");                return;            }            //查询所有用户记录            List<String> userList = userListTmp.stream().map(Object::toString).collect(Collectors.toList());            for (String u : userList) {                //取出用户最新心跳时间(可能在结束时间内再次有了心跳,所以要取最新的)                Double score = redisTemplate.opsForZSet().score(HEART_KEY, u);                if (score != null) {                    //只要有一条记录,就和当前时间比较,是否超过结束时间分钟                    Instant lastTime = Instant.ofEpochMilli(score.longValue());                    Duration duration = Duration.between(lastTime, now);                    if (duration.getSeconds() >= DEAD_LINE) {                        //判定为离线                        //取登陆时间   和用户最后一次心跳时间差即为登录时间                        Object loginTime = redisTemplate.opsForHash().get(LOGIN_KEY, u);                        if (loginTime != null) {                            doLogout(u, loginTime, lastTime);                            log.info("user {} is offline", u);                        } else {                                                    }                        //删除用户心跳记录                        redisTemplate.opsForZSet().remove(HEART_KEY,u);                    }                }            }//每30秒轮询一次        }, 5, 30, TimeUnit.SECONDS);    }}
@RestControllerpublic class OnlineController {    @Autowired    private OnlineService onlineService;    /**     * 登陆     * @param userId     */    @GetMapping("/online/{userId}")    public String login(@PathVariable("userId") String userId) {        onlineService.login(userId);        return "success";    }    /**     * 登出     * @param userId     */    @GetMapping("/online/logout/{userId}")    public String loginOut(@PathVariable("userId") String userId) {        onlineService.loginOut(userId);        return "success";    }    /**     * 心跳API     * @param userId     */    @GetMapping("/online/heart/{userId}")    public String heart(@PathVariable("userId") String userId) {        onlineService.heartBeat(userId);        return "success";    }    /**     * 获取用户在线时长     */    @GetMapping("/online/duration/{userId}")    public Long duration(@PathVariable("userId") String userId) {        return onlineService.getOnlineDuration(userId);    }}

 方案二

        基于Redis过期监听

                注意:

                在 Redis 官方手册的 keyspace-notifications: timing-of-expired-events 中明确指出:

                Basically expired events are generated when the Redis server deletes the key and not                 when the time to live theoretically reaches the value of zero

                Redis 自动过期的实现方式是:定时任务离线扫描并删除部分过期键;在访问键时惰性                    检查是否过期并删除过期键。

                Redis 从未保证会在设定的过期时间立即删除并发送过期通知。实际上,过期通知晚于                    设定的过期时间数分钟的情况也比较常见。

        redis过期监听设置方式:

                  1.打开conf/redis.conf 文件,取消注释:notify-keyspace-events Ex

                  2.重启redis

                  3.如果设置了密码需要重置密码:config set requirepass ****

                  4.验证配置是否生效

                   

进入redis客户端:redis-cli执行 CONFIG GET notify-keyspace-events ,如果有返回值证明配置成功,如果没有执行步骤三执行CONFIG SET notify-keyspace-events "Ex",再查看步骤二是否有值

              实现:用户登陆后,写进REDIS用户标识和过期时间,比如设置expire_time为3分钟,则用户每次操作,都会进行续期,保证不过期,等用户3分钟内不进行操作,则服务端监听到过期键。进行时间运算。当前时间减去登录时间,为最后的登陆时长。

             1.但是并发量大可能会产生重复消费,所以视情况加分布式锁等。

              2.redis延迟(redis机制问题)

方案三

        基于websocket

        逻辑:用户登陆后,客户端和服务端建立一个长连接。客户端关闭调用close关闭连接,进行时间计算。

        问题:1.如果是网页应用。当用户关闭TAB后。socket也会随之关闭。用另一个网页打开后,                       系统如果是免登陆或基于服务端存储登陆状态自动登陆。无法累积时长。时间又开始                      重新计算。所以登陆时长计算不正确。

                    2.服务端耗费资源大,当用户量巨大。每个用户挂一个长连接。服务端压力大。

                    3.部分浏览器版本,无法触发close事件,还需要重新做一套心跳机制,参靠方案一,哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈


点击全文阅读


本文链接:http://zhangshiyu.com/post/185680.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1