如何稍微优雅滴完成博文访问计数[SpringBoot+redis+分布式锁]

news2024/11/15 15:48:39

文章目录

  • 前言
  • 背景
    • 朴素做法
    • Redis方案
  • 流量统计
    • 接口演示
    • 自定义注解
    • 计数实现
      • 防刷
      • 加锁
      • 完整代码
  • 数据一致性
    • 分析
    • 自定义注解
    • 返回值分析
    • 解决方案
  • 总结

前言

okey,我们来收尾一下,这公历纪年2022年12月31日。这是本年度的最后一篇博文。那么这篇博文主要是用来实现博文的一个访问记数用的。

背景

这个是我现在还在写的一个项目,没办法事情太多,加上最近状态很不好所以一直在做。那么这个的话就是这个:
在这里插入图片描述
我们要对这个阅读做到一个实时的统计。

朴素做法

文章浏览量统计,最朴素的做法就是:用户每次浏览,前端会发送一个GET请求获取一篇文章详情时,会把这篇文章的浏览量+1,存进数据库里。

分析存在的问题:

在GET请求的业务逻辑里进行了数据的写操作!
高并发,数据库压力太大,文章浏览量+1会存在线程不安全问题,加锁会很慢。
同时如果文章的一些数据做了缓存操作,没有及时更新缓存当中的数据,会导致数据不一致的情况。

Redis方案

常规一点的思路,或者基本一点的做法可以和redis进行一个结合。
在这里插入图片描述
我们将访问的流量给缓存在Redis当中,当达到某一个阈值,例如流量为100或者别的数值的整数倍的时候,我们就刷新数据到数据库当中。这一来降低了对于写Mysql写的操作。同时按照我们的博文来说,博文的内容在短时间内是不太会发生变动的,因此这个东西也应该是做缓存处理的(实际上我也是这样处理的)。因此这里的话就有两个问题:

  1. 将访问量在redis当中进行存储,并且也需要做到持久化处理
  2. 保持数据一致性,对于一些历史缓存,必须保证里面的访问数据是最新的

同时由于涉及到的接口较多,对于代码级别的改动必须降低。

OK,那么接下来的话,咱们就针对这几个问题进行一个处理。

流量统计

okey,我们先来解决第一个问题,就是我们的一个流量的统计。
在这里的话我是这样设计的。
在这里插入图片描述

但是值得一提的是,在这里如果我们需要严格保证数据的一致性的话,那么我们的技术接口在访问的时候,必须要加上一个分布式锁,这个时候,你要考虑的就是值不值得了。如果说这个数据非常重要,那么我们就上锁,安全,如果你认为数据并不是很重要,并且幻读是可以被允许的话,那么就没有必要去加锁。

当然咱们这里还是加一把锁,同时咱们这里的实现的话也是要做到防止有人刷访问量,毕竟这活以前干过--

那么咱们的过程的话也是看到了,其实很简单就是你首先访问特定的接口,然后呢再去访问博文,或者其他的一些有显示这些数据的接口,然后就可以拿到最新的一个状态。

接口演示

okey,我们来看到我们的接口的一个演示:
在这里插入图片描述
之后我们访问那个可以+1的接口
在这里插入图片描述
此时回到刚刚的接口,查看数据:
在这里插入图片描述
现在+1了。
这样一来我们的基本目标就算明确了。
那么接下来我们要做的就是实现这个东西。

自定义注解

刚刚我们说了需要把对代码的修改降底,那么我们就需要去使用到咱们的一个切面来做处理。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ViewNumberUp {

    //传入模式
    String mode() default "";
}

之后的话,我们还需要去定义一下这个数据结构,就是咱们的这个流量数据长啥样:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class BlogViewNumber {
    private static final long serialVersionUID = 1L;
    private Integer viewNumber=0;
    private Integer likeNumber=0;
    private Integer collectNumber=0;
    private Integer forkNumber=0;
    private String ip;
}

这里的话,我们这块有4个需要计量的数据,因此的话,我们刚刚的注解里面有一个出入模式的东西。
不过值得一提的是,处理访问量这种东西是不能撤回的,其他的其实都是可以撤回的,比如你收藏,你可以收藏,也可以取消收藏。这些东西的话我这里使用类似方法处理过,但是为了让这个东西看起来“更强大”因此我还是做了个保留。

计数实现

那么接下来的话就是实现我们的一个博文的计数了。

防刷

这里防刷的手段有很多,那么我这里的话选择了比较简单的方案,这个方案就是通过IP去判断。假设A访问了,那么我就几下A的IP,当B访问的时候,我对比一下当前的IP和A的是不是一样的,如果不是那么访问量+1,同时刷新IP为B的,如果B的和A的一样,那么不好意思不加1,我认为是同一个人。当然也可以按照你的用户的id来。游客可能没有id,那么你可以选择分配一个临时id,或者干脆就游客不算。

         if (!blogViewNumber.getIp().equals(ipAddr)) {
                    blogViewNumber.setViewNumber(blogViewNumber.getViewNumber() + 1);
                    blogViewNumber.setIp(ipAddr);
                }

那么在这块我们需要使用到这个获取IP的工具类:


public class GetIPAddrUtils {

    public static HttpServletRequest GetHttpServletRequest(){
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
        assert servletRequestAttributes != null;
        return servletRequestAttributes.getRequest();
    }

    public static String GetIPAddr() {
        HttpServletRequest request = GetHttpServletRequest();
        return GetIPAddr(request);
    }

    public static String GetIPAddr(HttpServletRequest request) {
        String ipAddress = null;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getRemoteAddr();
                if (ipAddress.equals("127.0.0.1")) {
                    // 根据网卡取本机配置的IP
                    try {
                        ipAddress = InetAddress.getLocalHost().getHostAddress();
                    } catch (UnknownHostException e) {
                        e.printStackTrace();
                    }
                }
            }
            // 通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
            if (ipAddress != null) {
                if (ipAddress.contains(",")) {
                    return ipAddress.split(",")[0];
                } else {
                    return ipAddress;
                }
            } else {
                return "";
            }
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
    }
}

加锁

之后的话,我们加锁,这个是没有办法的,包括我们刚刚判断这个刷访问量的时候也是这个问题,如果不加锁,举个例子,A,B,C三个人。A访问了,现在访问量是1,当B,和C同时访问的时候,B,和C读取到的访问量都是1,加上1之后为2,当B,C都写回去数据的时候,访问量就2.但是实际上有3个人访问了。这个就不准。因此得想办法,我们必须得加锁,但是还是那句话,如果你认为这个是可以接受的,那么就不加锁,这样的话会节省资源。

   RLock lock = redissonClient.getLock(redisPrefix);
        lock.lock(10, TimeUnit.SECONDS);
        try {
            if(redisUtils.hasKey(redisPrefix)){
                this.addCache(redisPrefix,blogid,mode,false);
            }else {
                BlogViewNumber blogViewNumber = new BlogViewNumber();
                //通过数据库拿到博文的流量数据,然后放到我们的redis当中
                BlogEntity blogEntity = blogService.getById(blogid);
                BeanUtils.copyProperties(blogEntity,blogViewNumber);
                String ipAddr = GetIPAddrUtils.GetIPAddr();
                blogViewNumber.setIp(ipAddr);
                redisUtils.set(redisPrefix,blogViewNumber);
                this.addCache(redisPrefix,blogid,"pv",true);
            }
        } finally {
            lock.unlock();
        }

完整代码

这个代码的话,只需要看到一个case为pv的情况就好了,其他的是其他的,关系不大。


@Component
@Aspect
@Slf4j
public class BlogViewNumberAspect {

    private static final String viewNumberPrefix = RedisTransKey.viewNumberPrefix;

    @Autowired
    private RedisUtils redisUtils;

    @Autowired
    BlogService blogService;

    @Autowired
    RedissonClient redissonClient;

    @Value("${blog.pv}")
    private Integer blogPv;

    @Pointcut("@annotation(com.huterox.common.holeAnnotation.ViewNumberUp)")
    public void pageViewAspect() {}

    /**
     * 这里负责处理我们的切面,主要是处理我们一些流量信息的记录
     * 1. 初始化时将博文的数据写缓存当中
     * 2. 初始化后,更新缓存当中的数据
     * 3. 浏览量比较特殊,不需要每次都刷新数据库
     *      此外二外开放一个专门获取博文流量信息的接口(这个接口上的数据将从redis当中获取)
     */
    @AfterReturning(value = "pageViewAspect()&& @annotation(viewNumberUp)",
            returning = "result")
    public void around(JoinPoint joinPoint,ViewNumberUp viewNumberUp, R result) throws Exception {

        int code = Integer.parseInt(result.get("code").toString());
        if(code!=0){
            return;
        }
        assert viewNumberUp!=null;
        String mode = viewNumberUp.mode();
        Map<String, Object> nameAndValue = getNameAndValue(joinPoint);
        Long blogid = Long.valueOf(nameAndValue.get("blogid").toString());
        String redisPrefix = viewNumberPrefix+":"+blogid;
        RLock lock = redissonClient.getLock(redisPrefix);
        lock.lock(10, TimeUnit.SECONDS);
        try {
            if(redisUtils.hasKey(redisPrefix)){
                this.addCache(redisPrefix,blogid,mode,false);
            }else {
                BlogViewNumber blogViewNumber = new BlogViewNumber();
                //通过数据库拿到博文的流量数据,然后放到我们的redis当中
                BlogEntity blogEntity = blogService.getById(blogid);
                BeanUtils.copyProperties(blogEntity,blogViewNumber);
                String ipAddr = GetIPAddrUtils.GetIPAddr();
                blogViewNumber.setIp(ipAddr);
                redisUtils.set(redisPrefix,blogViewNumber);
                this.addCache(redisPrefix,blogid,"pv",true);
            }
        } finally {
            lock.unlock();
        }
    }

    private void addCache(String redisPrefix,Long blogid,String mode,boolean first){

        Object o = redisUtils.get(redisPrefix);
        BlogViewNumber blogViewNumber = JSON.parseObject(o.toString(), BlogViewNumber.class);
        switch (mode) {
            case "pv":
                Integer viewNumber = blogViewNumber.getViewNumber();
                if (viewNumber % blogPv == 0) {
                    //这个时候更新数据库
                    BlogEntity blogEntity = blogService.getById(blogid);
                    blogEntity.setViewNumber(viewNumber);
                    blogService.updateById(blogEntity);
                }
                blogViewNumber.setViewNumber(blogViewNumber.getViewNumber() + 1);
            if(first) {
                blogViewNumber.setViewNumber(blogViewNumber.getViewNumber() + 1);
            }else {
                String ipAddr = GetIPAddrUtils.GetIPAddr();
                blogViewNumber.setViewNumber(blogViewNumber.getViewNumber() + 1);
                if (!blogViewNumber.getIp().equals(ipAddr)) {
                    blogViewNumber.setViewNumber(blogViewNumber.getViewNumber() + 1);
                    blogViewNumber.setIp(ipAddr);
                }
            }
                break;
            case "cv": {
                BlogEntity blogEntity = blogService.getById(blogid);
                blogViewNumber.setCollectNumber(blogViewNumber.getCollectNumber() + 1);
                blogEntity.setCollectNumber(blogViewNumber.getCollectNumber());
                blogService.updateById(blogEntity);
                break;
            }
            case "fv": {
                BlogEntity blogEntity = blogService.getById(blogid);
                blogViewNumber.setForkNumber(blogViewNumber.getForkNumber() + 1);
                blogEntity.setForkNumber(blogViewNumber.getForkNumber());
                blogService.updateById(blogEntity);
                break;
            }
            default: {
                BlogEntity blogEntity = blogService.getById(blogid);
                blogViewNumber.setLikeNumber(blogViewNumber.getLikeNumber() + 1);
                blogEntity.setLikeNumber(blogViewNumber.getLikeNumber());
                blogService.updateById(blogEntity);
                break;
            }
        }
        redisUtils.set(redisPrefix,blogViewNumber);
    }

    /**
     * 获取某个Method的参数名称及对应的值
     * @return Map<参数名称, 参数值></参数名称,参数值>
     */
    public static Map<String, Object> getNameAndValue(JoinPoint joinPoint) {
        Map<String, Object> param = new HashMap<>();
        Object[] paramValues = joinPoint.getArgs();
        String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
        for (int i = 0; i < paramNames.length; i++) {
            param.put(paramNames[i], paramValues[i]);
        }
        return param;
    }

}

数据一致性

之后的话就是咱们的另一个重点,数据的一致性。
同样的我们也需要使用到切面来完成操作。

分析

首先的话,刚刚已经说了有些东西,是存放在缓存里面的,必须保证这里面的刚刚我们做的流量统计是最新的,不然当用户访问到这个缓存的时候,得到数据就不一致了。这样做的话,虽然没什么,但是实在是太难看了。

那么解决这个我们当然其实有三个方案嘛。

  1. 解决看数据的人
  2. 解决缓存的数据
  3. 解决返回的数据

第一点行不通。
第二点,可以但是修改数据的时候需要不断更新redis里面的缓存,对redis的写入比较繁琐。
第三点,也可以,但是需要不断对返回数据进行处理,对redis的度比较繁琐。

综合来看的话,返回的数据量不是很大,而且天知道缓存里面还有别的东西没有,我很难保证全部有更新。因此我选择了第三点,实现也比较简单。

自定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RefreshFlew {

    String type() default "";
    String key() default "";
}

我在这里定义了两个值,一个是type,还有一个是key。

原因的话是这样的,我们这边是有一个统一的返回类的。

public class R extends HashMap<String, Object> {
	private static final long serialVersionUID = 1L;
	
	public R() {
		put("code", 0);
		put("msg", "success");
	}
	
	public static R error() {
		return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
	}
	
	public static R error(String msg) {
		return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
	}
	
	public static R error(int code, String msg) {
		R r = new R();
		r.put("code", code);
		r.put("msg", msg);
		return r;
	}

	public static R warn() {
		R r = new R();
		r.put("code", 1);
		r.put("msg", "warning");
		return r;
	}

	public static R ok(String msg) {
		R r = new R();
		r.put("msg", msg);
		return r;
	}
	
	public static R ok(Map<String, Object> map) {
		R r = new R();
		r.putAll(map);
		return r;
	}
	
	public static R ok() {
		return new R();
	}

	public R put(String key, Object value) {
		super.put(key, value);
		return this;
	}
}

所以的话我们有两个东西。

返回值分析

刚刚说了返回的东西是R这个返回类,但是R是个MAP,然后这里面应该是啥呢。这里的话在我的这个设计当中呢,是这两个类的东西。
第一个是返回的是PageUtils。这个呢是MP分页一个东西,也是自定义的一个东西。

/**
 * 分页工具类
 */
public class PageUtils implements Serializable {
	private static final long serialVersionUID = 1L;
	/**
	 * 总记录数
	 */
	private int totalCount;
	/**
	 * 每页记录数
	 */
	private int pageSize;
	/**
	 * 总页数
	 */
	private int totalPage;
	/**
	 * 当前页数
	 */
	private int currPage;
	/**
	 * 列表数据
	 */
	private List<?> list;
	
	/**
	 * 分页
	 * @param list        列表数据
	 * @param totalCount  总记录数
	 * @param pageSize    每页记录数
	 * @param currPage    当前页数
	 */
	public PageUtils(List<?> list, int totalCount, int pageSize, int currPage) {
		this.list = list;
		this.totalCount = totalCount;
		this.pageSize = pageSize;
		this.currPage = currPage;
		this.totalPage = (int)Math.ceil((double)totalCount/pageSize);
	}

	/**
	 * 分页
	 */
	public PageUtils(IPage<?> page) {
		this.list = page.getRecords();
		this.totalCount = (int)page.getTotal();
		this.pageSize = (int)page.getSize();
		this.currPage = (int)page.getCurrent();
		this.totalPage = (int)page.getPages();
	}

	public int getTotalCount() {
		return totalCount;
	}

	public void setTotalCount(int totalCount) {
		this.totalCount = totalCount;
	}

	public int getPageSize() {
		return pageSize;
	}

	public void setPageSize(int pageSize) {
		this.pageSize = pageSize;
	}

	public int getTotalPage() {
		return totalPage;
	}

	public void setTotalPage(int totalPage) {
		this.totalPage = totalPage;
	}

	public int getCurrPage() {
		return currPage;
	}

	public void setCurrPage(int currPage) {
		this.currPage = currPage;
	}

	public List<?> getList() {
		return list;
	}

	public void setList(List<?> list) {
		this.list = list;
	}
	
}

还有一个东西就是我们的这个博文的一个实体类,或者是具备相同字段的实体类。
比如这个就是有相同字段的一个实体了。返回的是这个,或者就是博文实体类。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class BlogBody implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 这边的话userid啥的都是指文章的作者
     * */
    private String userid;
    private Long blogid;
    private String content;
    private String blogTitle;
    private String userNickname;
    private String userImg;
    private String createTime;
    private Integer viewNumber;
    private Integer likeNumber;
    private Integer collectNumber;
    private Integer forkNumber;
    private String blogtype;
    private String blogimg;

}

同时部分的接口,还是用了缓存的,这里使用的是SpringCache作为缓存技术。(确切的说是大部分接口,尤有钱就上ES(艹皿艹 ))

解决方案

那么这个时候我们就需要对这些接口的返回值进行处理了,这里值得庆幸的就是咱们有统一返回类。因此的话不管怎么样,方法返回的一定是R类的东西。我们要做的就是分别处理刚刚提到的两种类型的数据。这个的话我们就直接看到代码了,思路还是简单的。


@Component
@Aspect
@Slf4j
public class ReFreshFlewAspect {


    private static final String viewNumberPrefix = RedisTransKey.viewNumberPrefix;

    @Autowired
    private RedisUtils redisUtils;
    @Autowired
    BlogService blogService;
    @Pointcut("@annotation(com.huterox.common.holeAnnotation.RefreshFlew)")
    public void refreshAspect() {}

    @Around("refreshAspect() && @annotation(annotation)")
    public R verification(ProceedingJoinPoint joinPoint,RefreshFlew annotation) throws Throwable{

        assert annotation != null;
        String type = annotation.type();
        String key = annotation.key();
        Object pro = joinPoint.proceed();
        //我们这里最后一个依靠的是SpringCache的返回值,这个时候人家已经帮我们重新序列化为了一个R对象
        R proceed = (R) pro;
        if(type.equals("page")){
            PageUtils page = (PageUtils) proceed.get(key);
            List<BlogEntity> blogEntityList = (List<BlogEntity>) page.getList();
            List<BlogEntity> result = new ArrayList<>();
            for(BlogEntity blogEntity:blogEntityList){
                String redisPrefix = viewNumberPrefix+":"+blogEntity.getBlogid();
                if(redisUtils.hasKey(redisPrefix)){
                    Object red = redisUtils.get(redisPrefix);
                    BlogViewNumber blogViewNumber = JSON.parseObject(red.toString(), BlogViewNumber.class);
                    BeanUtils.copyProperties(blogViewNumber,blogEntity);
                    result.add(blogEntity);
                }else {
                    this.createCache(blogEntity,redisPrefix);
                    result.add(blogEntity);
                }
            }
            page.setList(result);
            proceed.put(key,page);
        }else if(type.equals("blog")){
            Object o =  proceed.get(key);
            BlogEntity ob = new BlogEntity();
            BeanUtils.copyProperties(o,ob);
            String redisPrefix = viewNumberPrefix+":"+ob.getBlogid();
            if(redisUtils.hasKey(redisPrefix)){
                Object red = redisUtils.get(redisPrefix);
                BlogViewNumber blogViewNumber = JSON.parseObject(red.toString(), BlogViewNumber.class);
                BeanUtils.copyProperties(blogViewNumber,o);
                proceed.put(key,o);
            }else {
                this.createCache(ob,redisPrefix);
            }
        }
        return proceed;
    }

    private void createCache(BlogEntity o,String redisPrefix){
        BlogViewNumber blogViewNumber = new BlogViewNumber();
        BeanUtils.copyProperties(o,blogViewNumber);
        blogViewNumber.setIp("Hello 2023");
        redisUtils.set(redisPrefix,blogViewNumber);
    }
}

这里的话就没必要上锁了,不然我就用读写锁了,为啥呢,因为本身有网络的延迟,在你获取数据的过程当中,可能又有人访问了,这个时候访问数据得到的也不是那么准了。所以这个也是为什么一开始我就是你能不能忍受,因为这个数据本身不会准(如果访问人多了)但是可以保证记录的准确性。这个如果是商品之类的东西的话,那么就非常有必要了。

总结

最后祝大家新年快乐~ 不说了,嗓子要废了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/130903.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【LeetCode】被围绕的区域 [M](深度优先遍历)

130. 被围绕的区域 - 力扣&#xff08;LeetCode&#xff09; 一、题目 给你一个 m x n 的矩阵 board &#xff0c;由若干字符 X 和 O &#xff0c;找到所有被 X 围绕的区域&#xff0c;并将这些区域里所有的 O 用 X 填充。 示例 1&#xff1a; 输入&#xff1a;board [[&quo…

浅谈Flink批模式Adaptive Hash Join

Flink批Hash Join递归超限问题 随着Flink流批一体能力的迅速发展以及Flink SQL易用性的提升&#xff0c;越来越多的厂商开始将Flink作为离线批处理引擎使用。在我们使用Flink进行大规模join操作时&#xff0c;也许会发生如下的异常&#xff0c;导致任务失败&#xff1a; Hash j…

Es进阶检索

本文用到的测试数据及所有代码链接&#xff1a; https://blog.csdn.net/m0_62436868/article/details/128505566?spm1001.2014.3001.5501 1、SearchAPI ES 支持两种基本方式检索 : 一个是通过使用 REST request URI 发送搜索参数&#xff08;uri检索参数&#xff09; 另…

基于华为eNSP的中小企业办公园区网络规划与设计

目录一、需求分析&#xff08;一&#xff09;项目背景&#xff08;二&#xff09;网络业务需求&#xff08;三&#xff09;网络应用需求二、网络结构设计三、网络拓扑图四、网络设备基本配置五、项目测试结语运用到的技术有&#xff1a; 1、虚拟局域网&#xff08;VLAN&#xf…

人工智能--你需要知道的一切

人工智能--你需要知道的一切 人工智能是当今最受关注的技术之一。但它究竟是什么&#xff1f;你为什么要关心&#xff1f; 人工智能是当今最受关注的技术之一。但它到底是什么&#xff1f;你为什么要关心&#xff1f;在这里&#xff0c;我们将介绍你需要知道的关于人工智能的…

java开发的美妆化妆品电商商城系统

简介 Java基于ssm(可以转springboot项目哦)开发的美妆商城系统&#xff0c;主要是卖化妆品的系统&#xff0c;用户可以浏览商品&#xff0c;加入购物车&#xff0c;下单&#xff0c;在个人中心管理自己的订单。管理员可以管理自己的商品&#xff0c;发布商品&#xff0c;上下架…

2023年留学基金委(CSC)公派访问学者博士后项目选派办法及解读

2023年即将伊始。知识人网祝大家新年快乐&#xff0c;心想事成&#xff01;同时提醒申请者关注国家留学基金委&#xff08;CSC&#xff09;的申报政策。目前CSC官网已经发布了2023年公派访问学者、博士后的项目通知&#xff0c;知识人网小编现将其选派工作流程及选派办法原文转…

C语言 自定义类型

结构体内存对齐 解释1 从内存开始位置存放 解释二 int对齐数是4 vs默认对齐数是8 取其较小值的倍数 那就是4的位置存放 char 对数1 vs是8默认 谁的较小值对数是1 那就是任意数 所以c2防砸8 如下图绿色部分 struct S2 {char c1;int i;double d;//8 };输出结果16 struct S4 …

当我们身边没有示波器就无法测量频率与占空比了?一招教你解决身边没有示波器时如何测量STM32定时器产生PWM的频率与占空比

当我们身边没有示波器就无法测量频率与占空比了&#xff1f;这篇文件小编就教大家如何使用定时器输入捕获功能测量频率与占空比。 原理解析 定时器输入捕获一般应用在两个方面&#xff0c;一个方面是脉冲跳变沿时间测量&#xff0c;另一方面是 PWM输入测量。下面将要使用就是测…

LaTeX快速入门

文章目录LaTeX快速入门一、 概述1、 简介2、 环境配置3、文件结构4、 文档结构二、 基本概念1、 第一个LaTeX程序2、 宏包和文档类2.1 宏包2.2 文档类3、 文件组织的方式4、 相关术语和概念三、 排版文字1、 文字编码2、 排版中文3、 LaTeX中的字符3.1 空格和分行3.2 注释3.3 特…

Redis(Ⅰ)【学习笔记】

&#xff08;仅作为个人学习笔记&#xff09; 1.什么是Redis&#xff1f; 1.Redis 是用C语言开发的一个开源的高性能键值对&#xff08; key-value &#xff09;内存数据库&#xff0c;它是一种 NoSQL 数据库。 2.它是【单进程单线程】的内存数据库&#xff0c;所以说不存在线…

charAt()方法的使用

charAt()函数 Java charAt() 方法属于Java String类 charAt() 方法用于返回指定索引处的字符。索引范围为从 0 到 length() - 1。 语法 public char charAt(int index) 参数 index – 字符的索引。 返回值 返回指定索引处的字符。 举个例子&#xff1a; package 做题;…

基于思维进化算法优化BP神经网络(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑…

Kubernetes之PV与PVC

1. 综述 当前&#xff0c;存储的方式和种类有很多&#xff0c;并且各种存储的参数也需要非常专业的技术人员才能够了解。在Kubernetes集群中&#xff0c;放了方便我们的使用和管理&#xff0c;Kubernetes提出了PV和PVC的概念&#xff0c;这样Kubernetes集群的管理人员就可以将注…

linux系统中利用设备树完成对LED的控制

大家好&#xff0c;今天主要和大家聊一聊&#xff0c;如何使用linux系统中的设备树控制led。 目录 第一&#xff1a;设备树LED基本驱动原理 第二&#xff1a;LED灯驱动程序的实现 第一&#xff1a;设备树LED基本驱动原理 本次实验采用设备树向linux内核传递相关的寄存器物理…

MATLAB演示梯度上升寻找极值

MATLAB演示梯度上升寻找极值 梯度 梯度的本意是一个向量&#xff08;矢量&#xff09;&#xff0c;表示某一函数在该点处的方向导数沿着该方向取得最大值&#xff0c;即函数在该点处沿着该方向&#xff08;此梯度的方向&#xff09;变化最快&#xff0c;变化率最大&#xff08…

dubbo源码解析-SPI机制

SPI&#xff0c;Service Provider Interface&#xff0c;服务提供者接口&#xff0c;是一种服务发现机制。 JDK 的 SPI 规范 JDK 的 SPI 规范规定&#xff1a;  接口名&#xff1a;可随意定义  实现类名&#xff1a;可随意定义  提供者配置文件路径&#xff1a;其查…

剑指offer(简单)

目录 数组中重复的数字 替换空格 从尾到头打印链表 用两个栈实现队列 斐波那契数列 青蛙跳台阶问题 旋转数组的最小数字 二进制中的1的个数 打印从1到最大的n位数 删除链表的节点 调整数组顺序使奇数位于偶数前面 链表中倒数第k个节点 反转链表 合并两个排序的链…

一文读懂tensorflow: 基本概念和API

文章目录前言tensorflow发展历程基本概念张量神经网络、层模型超参数损失函数交叉熵函数激活函数梯度和自动微分优化器tensorflow 2.x 和 tensorflow 1.xtensorflow开发流程tensorflow API张量的定义和运算示例张量的初始化方式梯度计算模型的搭建示例&#xff1a;MINST手写数字…

Casting out Primes: Bignum Arithmetic for Zero-Knowledge Proofs学习笔记

1. 引言 Polygon zero团队 Daniel Lubarov 和 Polygon zkEVM团队 Jordi Baylina 2022年10月联合发表的论文 《Casting out Primes: Bignum Arithmetic for Zero-Knowledge Proofs》。 受“casting out nines” 技术——做对9取模运算并提供概率性结果&#xff0c;启发&#x…