前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如果代码有段位,来看看你是什么段位?青铜?白银?还是黄金?【有源码】

如果代码有段位,来看看你是什么段位?青铜?白银?还是黄金?【有源码】

作者头像
烟雨平生
发布2023-12-11 18:41:01
1520
发布2023-12-11 18:41:01
举报
文章被收录于专栏:数字化之路数字化之路
  • 需求:用redis实现计数器
  • 代码1:time++
  • 代码2:redisTemplate.incr
  • 代码3:?
  • 小结

在软件开发中,我们经常需要统计接口的访问次数,以便了解系统的运行状态,优化性能,或者进行数据分析。本文将show三种不同的方法来统计一小时内的接口访问次数,抛砖引玉

time++

代码语言:javascript
复制
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.text.MessageFormat;
import java.util.Calendar;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
@RequiredArgsConstructor
public class MethodCallStatisticsUtils {

    private static String METHOD_CALL_FREQUENCY = "v1:method:{0}:{1}:{2}";

    @Autowired
    private static RedisTemplate<String, Long> redisTemplate;

    @Autowired
    public void setRedisTemplate(RedisTemplate<String, Long> redisTemplate1) {
        MethodCallStatisticsUtils.redisTemplate = redisTemplate1;
    }

    public static void calculateTimes(String shopNo, String method) {
        try {
            long now = System.currentTimeMillis();
            Calendar date = Calendar.getInstance();
            date.setTimeInMillis(now);
            int day = date.get(Calendar.DAY_OF_MONTH);
            int house = date.get(Calendar.HOUR);
            Long time = redisTemplate.opsForValue().get(MessageFormat.format(METHOD_CALL_FREQUENCY, shopNo, day, house));
            if (Objects.isNull(time)) {
                redisTemplate.opsForValue().set(MessageFormat.format(METHOD_CALL_FREQUENCY, shopNo,  day, house), 1L, 2, TimeUnit.HOURS);
                time = 1L;
            } else {
                time++;
                redisTemplate.opsForValue().set(MessageFormat.format(METHOD_CALL_FREQUENCY, shopNo,  day, house), time, 2, TimeUnit.HOURS);
            }
            log.info("使用统计 ->  {},方法 : {},所有接口总次数 : {}", shopNo, method, time);
        }
        catch (Exception e) {
            log.info("统计失败");
        }
    }

}

点评: 段位:青铜

具备了计数的能力,但在并发高时有一定概率少统计。 如果只是想大概了解下访问次数,并不要求很准确,这个代码也满足了需求。

问题剖析:有线程安全问题。 上面用来计数的代码拆解一下,是分了3步:

  1. 获取当前的访问次数 time
  2. 在当前线程栈中time++【其它线程看不到++后的值】
  3. 将time新值写入redis

本例中,这三个操作不是原子的,但只有三步同时完成,才能满足计数的累加。 所有线程都可以同时访问Redis中的值,且这三步操作结束才能完成对这个临界资源的更新,如果没有加锁来确保这三个动作是原子的,则必然存在线程安全问题。 详细解析:

场景:1个接口收到3个请求。如下图所求

INCR

代码语言:javascript
复制
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.text.MessageFormat;
import java.util.Calendar;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
@RequiredArgsConstructor
public class MethodCallStatisticsUtils {

    private static final String METHOD_CALL_FREQUENCY = "v1:method:{0}:{1}:{2}";

    @Autowired
    private static RedisTemplate<String, Long> redisTemplate;

    @Autowired
    public void setRedisTemplate(RedisTemplate<String, Long> redisTemplate1) {
        MethodCallStatisticsUtils.redisTemplate = redisTemplate1;
    }

    public static Long calculateTimes(String shopNo, String method, int timeoutPeriod, TimeUnit timeUnit) {
        try {
            long now = System.currentTimeMillis();
            Calendar date = Calendar.getInstance();
            date.setTimeInMillis(now);
            int day = date.get(Calendar.DAY_OF_MONTH);
            int house = date.get(Calendar.HOUR);

            String redisKey = MessageFormat.format(METHOD_CALL_FREQUENCY, shopNo, day, house);
            Long time = redisTemplate.opsForValue().get(redisKey);
            if (Objects.isNull(time)) {
                redisTemplate.opsForValue().set(redisKey, 1L, timeoutPeriod, timeUnit);
                return 1L;
            }
            Long times = redisTemplate.opsForValue().increment(redisKey);
            log.info("调用接口使用统计 ->  {},方法 : {},所有接口总次数 : {}  timeoutPeriod {} timeUnit {} ", shopNo, method, times, timeoutPeriod, timeUnit);
            return times;
        } catch (Exception e) {
            log.info("调用接口使用统计失败 shopNo {} method {} timeoutPeriod {} timeUnit {} msg {} ", shopNo, method, timeoutPeriod, timeUnit, e.getMessage());
        }
        return 1L;
    }

}

点评: 段位:白银

只是实现了功能,仅在记录第一次访问时有一定概率漏统计访问次数。

问题剖析:有线程安全问题。

Redis+Lua

代码语言:javascript
复制
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.List;

/**
 * @Auther: cheng.tang
 * @Date: 2023/11/22
 * @Description: gbb-centering-incr
 */
@Service
@Slf4j
public class RateLimiter {

    @Autowired
    private RedisTemplate<String, Long> redisTemplate;

    private static final String RATE_LIMITER_LUA_SCRIPT = "local current " +
            "current = redis.call(\"incr\",KEYS[1]) " +
            "if current == 1 then \n" +
            "    redis.call(\"expire\",KEYS[1],ARGV[1] ) \n" +
            "end \n" +
            "return current \n";

    /**
     * 统计expireTimeSeconds期间有多少次请求
     *
     * @param key               一类数据的标识
     * @param expireTimeSeconds 统计时间期间
     * @return 已经调用了多少次
     */
    public Long limitCall(String key, Integer expireTimeSeconds) {
        return limitCall(RATE_LIMITER_LUA_SCRIPT, Collections.singletonList(key), expireTimeSeconds);
    }

    /**
     * 统计expireTimeSeconds期间有多少次请求
     *
     * @param luaScript 一个lua脚本
     * @param keys      lua脚本需要的KEYS
     * @param args      lua脚本需要的参数
     * @return 已经调用了多少次
     */
    public Long limitCall(String luaScript, List<String> keys, Object... args) {
        RedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
        Long currentTimes = redisTemplate.execute(script, keys, args);
        String redisKey = keys.get(0);
        log.info("redisKey {} currentTimes {} ", redisKey, currentTimes);
        return currentTimes;
    }

}

In the above code there is a race condition. This can be fixed easily turning the INCR with optional EXPIRE into a Lua script that is send using the EVAL command (only available since Redis version 2.6). 唐成,公众号:的数字化之路干货|RedisTemplate调lua踩了个坑

点评: 这是Redis官方给的方案,访问次数统计准确。

该是哪个段位?黄金

问题: 1、对Redis的版本有依赖,需要2.6+ 2、涉及到的知识点多且引入了一门新语言Lua,使用的门槛变高。

补充:

如果要抽取组件,建议自定义一个RedisTemplate实例。想一想为什么? 代码示例:

代码语言:javascript
复制

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;

/**
 * @Auther: cheng.tang
 * @Date: 2023/11/22
 * @Description: gbb-centering-incr
 */
@Configuration
public class RedisTemplateConfig {

    @Bean
    public RedisTemplate<String, Object> centeringIncrToolRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setEnableDefaultSerializer(true);
        redisTemplate.setDefaultSerializer(RedisSerializer.json());
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setValueSerializer(RedisSerializer.json());
        redisTemplate.setHashKeySerializer(RedisSerializer.string());
        redisTemplate.setHashValueSerializer(RedisSerializer.json());
        return redisTemplate;
    }

}

小结

总的来说,统计接口访问次数是一个常见的需求,但是实现的方法有很多种。我们需要根据实际的需求和环境来选择最合适的方法。无论选择哪种方法,都需要考虑到并发问题,确保统计数据的准确性。 另外,在技术方案上,也可以使用redis的zset,同一维度的数据使用一个key,value存一个保证唯一性的做任意值,score存放访问的时间毫秒粒度的时间戳,,通过score来圈出指定时间窗口的记录数,就得到访问次数了。

在代码设计上,怎么样才能算是好的代码?“There are a thousand Hamlets in a thousand people's eyes.”,我认为,好的代码要在阅读上赏心悦目,在修改上得心应手。

如果一定要定一个标准,那么最常用到几个评判代码质量的标准是:可维护性、可读性、可扩展性、灵活性、简洁性、可复用性、可测试性。其中,可维护性、可读性、可扩展性又是提到最多的、最重要的三个评价标准。

“任何傻瓜都会编写计算机能理解的代码。好的程序员能够编写人能够理解的代码。”

好代码的要求:

  1. 有意义的命名。“见名知意”,代码要在字面上表达其含义,字面编程
  2. 单一职责。每个函数、类、模块都专注于做一件事,且这些函数、类、方法、实体等尽可能少
  1. 消除重复。重复可能是软件中一切邪恶的根源
  2. 有单元测试并能通过测试

如果每个例程都让你感到深合己意,那就是整洁代码,就是好的代码。如果代码让编程语言看上去像是专为解决那个问题而存在,就可以称之为漂亮的代码。

REFERENCE

https://redis.io/commands/incr/

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-12-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 的数字化之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • time++
  • INCR
  • Redis+Lua
  • 小结
  • REFERENCE
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档