Дизайн хранилища просмотров страниц с высокой степенью параллелизма

Redis

1. Предпосылки

статистика просмотров статей,lowПодход таков: каждый раз, когда пользователь просматривает страницу, внешний интерфейс отправляетGETПри запросе получения подробной информации о статье количество просмотров этой статьи будет+1, хранится в базе данных.

1.1 При этом возникает несколько проблем:

  1. Операция записи данных выполняется в бизнес-логике GET-запроса!
  2. Если параллелизм высокий, нагрузка на базу данных слишком высока;
  3. В то же время, если статья кэшируется и поисковые системы, такие какElasticSearchхранилище, синхронно обновлять кеш иElasticSearchОбновление синхронизации обновления занимает слишком много времени, а отказ от обновления приведет к несогласованности данных.

1.2 Решения

  • HyperLogLog

HyperLogLogдаProbabilistic data StructuresОдной из основных идей этого типа структуры данных является использование алгоритмов статистической вероятности, жертвуя точностью данных для экономии места в памяти и повышения производительности связанных операций.

  • Идеи дизайна
  1. Чтобы обеспечить реальное количество просмотров сообщений в блоге, согласно посещению пользователяipи статьиid, Выполнить уникальную проверку, то есть один и тот же пользователь посещает одну и ту же статью несколько раз, а количество посещений измененной статьи увеличивается только на 1;
  2. использовать просмотры страниц пользователяopsForHyperLogLog().add(key,value)Хранится вRedisВ середине ночи, когда количество просмотров страниц невелико, просмотры страниц обновляются в базе данных с помощью запланированных задач.

2. Непосредственная реализация

2.1 Конфигурация проекта

  • sql
DROP TABLE IF EXISTS `article`;

CREATE TABLE `article` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `title` varchar(100) NOT NULL COMMENT '标题',
  `content` varchar(1024) NOT NULL COMMENT '内容',
  `url` varchar(100) NOT NULL COMMENT '地址',
	`views` bigint(20) NOT NULL COMMENT '浏览量',
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

INSERT INTO article VALUES(1,'测试文章','content','url',10,NULL);

Часть данных вставлена, и проектный трафик был10, для легкого тестирования.

  • зависимости проектаpom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--mysql-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.2</version>
</dependency>
<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.0</version>
</dependency>
<!-- lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
  • application.yml
spring:
  # 数据库配置
  datasource:
    url: jdbc:mysql://47.98.178.84:3306/dev
    username: dev
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
    host: 47.98.178.84
    port: 6379
    database: 1
    password: password
    timeout: 60s  # 连接超时时间,2.0 中该参数的类型为Duration,这里在配置的时候需要指明单位
    # 连接池配置,2.0中直接使用jedis或者lettuce配置连接池(使用lettuce,依赖中必须包含commons-pool2包)
    lettuce:
      pool:
        # 最大空闲连接数
        max-idle: 500
        # 最小空闲连接数
        min-idle: 50
        # 等待可用连接的最大时间,负数为不限制
        max-wait:  -1s
        # 最大活跃连接数,负数为不限制
        max-active: -1


# mybatis
mybatis:
  mapper-locations: classpath:mapper/*.xml
#  type-aliases-package: cn.van.redis.view.entity

2.2 Аспектный дизайн просмотров страниц

  • Пользовательская аннотация для новых просмотров статьиRedisсередина
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PageView {
    /**
     * 描述
     */
    String description()  default "";
}
  • огранка
 @Aspect
@Configuration
@Slf4j
public class PageViewAspect {

    @Autowired
    private RedisUtils redisUtil;

    /**
     * 切入点
     */
    @Pointcut("@annotation(cn.van.redis.view.annotation.PageView)")
    public void PageViewAspect() {

    }

    /**
     * 切入处理
     * @param joinPoint
     * @return
     */
    @Around("PageViewAspect()")
    public  Object around(ProceedingJoinPoint joinPoint) {
        Object[] object = joinPoint.getArgs();
        Object articleId = object[0];
        log.info("articleId:{}", articleId);
        Object obj = null;
        try {
            String ipAddr = IpUtils.getIpAddr();
            log.info("ipAddr:{}", ipAddr);
            String key = "articleId_" + articleId;
            // 浏览量存入redis中
            Long num = redisUtil.add(key,ipAddr);
            if (num == 0) {
                log.info("该ip:{},访问的浏览量已经新增过了", ipAddr);
            }
            obj = joinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return obj;
    }
}
  • ИнструментыRedisUtils.java
 @Component
public  class RedisUtils {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    public void del(String... key) {
        redisTemplate.delete(key[0]);
    }

    /**
     * 计数
     * @param key
     * @param value
     */
    public Long add(String key, Object... value) {
        return redisTemplate.opsForHyperLogLog().add(key,value);
    }
    /**
     * 获取总数
     * @param key
     */
    public Long size(String key) {
        return redisTemplate.opsForHyperLogLog().size(key);
    }

}
  • ИнструментыIpUtils.java

Класс инструментов у меня естьMacСледующий тест в порядке.WindowsЕсли у вас есть какие-либо вопросы, пожалуйста, дайте мне обратную связь

 @Slf4j
public class IpUtils {

    public static String getIpAddr() {
        try {
            Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
            InetAddress ip = null;
            while (allNetInterfaces.hasMoreElements()) {
                NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
                if (netInterface.isLoopback() || netInterface.isVirtual() || !netInterface.isUp()) {
                    continue;
                } else {
                    Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
                    while (addresses.hasMoreElements()) {
                        ip = addresses.nextElement();
                        if (ip != null && ip instanceof Inet4Address) {
                            log.info("获取到的ip地址:{}", ip.getHostAddress());
                            return ip.getHostAddress();
                        }
                    }
                }
            }
        } catch (Exception e) {
            log.error("获取ip地址失败,{}",e);
        }
        return null;
    }
}

2.3 Задача синхронизацииArticleViewTask.java

ArticleService.javaКод внутри относительно прост, смотрите исходный код в конце статьи.

@Component
@Slf4j
public class ArticleViewTask {

    @Resource
    private RedisUtils redisUtil;
    @Resource
    ArticleService articleService;

	// 每天凌晨一点执行
    @Scheduled(cron = "0 0 1 * * ? ")
    @Transactional(rollbackFor=Exception.class)
    public void createHyperLog() {
        log.info("浏览量入库开始");

        List<Long> list = articleService.getAllArticleId();
        list.forEach(articleId ->{
            // 获取每一篇文章在redis中的浏览量,存入到数据库中
            String key  = "articleId_"+articleId;
            Long view = redisUtil.size(key);
            if(view>0){
                ArticleDO articleDO = articleService.getById(articleId);
                Long views = view + articleDO.getViews();
                articleDO.setViews(views);
                int num = articleService.updateArticleById(articleDO);
                if (num != 0) {
                    log.info("数据库更新后的浏览量为:{}", views);
                    redisUtil.del(key);
                }
            }
        });
        log.info("浏览量入库结束");
    }

}

2.4 Тестовый интерфейсPageController.java

@RestController
@Slf4j
public class PageController {

    @Autowired
    private ArticleService articleService;

    @Autowired
    private RedisUtils redisUtil;

    /**
     * 访问一篇文章时,增加其浏览量:重点在的注解
     * @param articleId:文章id
     * @return
     */
    @PageView
    @RequestMapping("/{articleId}")
    public String getArticle(@PathVariable("articleId") Long articleId) {
        try{
            ArticleDO blog = articleService.getById(articleId);
            log.info("articleId = {}", articleId);
            String key = "articleId_"+articleId;
            Long view = redisUtil.size(key);
            log.info("redis 缓存中浏览数:{}", view);
            //直接从缓存中获取并与之前的数量相加
            Long views = view + blog.getViews();
            log.info("文章总浏览数:{}", views);
        } catch (Throwable e) {
            return  "error";
        }
        return  "success";
    }
}

Здесь конкретныйServiceМетоды, потому что все они размещены мнойControllerОбработано посередине, так что остальное простоMapperЗвоните, не тратите время здесь, см. Конец текстового источника. (Возможно, эта логическая обработка должна быть размещенаServiceобработано, пожалуйста, оптимизируйте в соответствии с фактической ситуацией)

3. Тест

Запускаем проект, тестируем трафик, сначала запрашиваемhttp://localhost:8080/1, журнал печатается следующим образом:

2019-03-2623:50:50.047  INFO 2970 --- [nio-8080-exec-1]  cn.van.redis.view.aspect.PageViewAspect  : articleId:1
2019-03-2623:50:50.047  INFO 2970 --- [nio-8080-exec-1] cn.van.redis.view.utils.IpUtils          : 获取到的ip地址:192.168.1.104
2019-03-2623:50:50.047  INFO 2970 --- [nio-8080-exec-1] cn.van.redis.view.aspect.PageViewAspect  : ipAddr:192.168.1.104
2019-03-2623:50:50.139  INFO 2970 --- [nio-8080-exec-1] io.lettuce.core.EpollProvider            : Starting without optional epoll library
2019-03-2623:50:50.140  INFO 2970 --- [nio-8080-exec-1] io.lettuce.core.KqueueProvider           : Starting without optional kqueue library
2019-03-2623:50:50.349  INFO 2970 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2019-03-2623:50:50.833  INFO 2970 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2019-03-2623:50:50.872  INFO 2970 --- [nio-8080-exec-1] c.v.r.v.web.controller.PageController    : articleId = 1
2019-03-2623:50:50.899  INFO 2970 --- [nio-8080-exec-1] c.v.r.v.web.controller.PageController    : redis 缓存中浏览数:1
2019-03-2623:50:50.900  INFO 2970 --- [nio-8080-exec-1] c.v.r.v.web.controller.PageController    : 文章总浏览数:11

Обратите внимание, что количество обращений к базе данных не увеличилось, и к машине снова обращаются.Обнаружено, что журнал печатается следующим образом:

2019-03-2623:51:14.658  INFO 2970 --- [nio-8080-exec-3] 
cn.van.redis.view.aspect.PageViewAspect  : articleId:1
2019-03-2623:51:14.658  INFO 2970 --- [nio-8080-exec-3] cn.van.redis.view.utils.IpUtils          : 获取到的ip地址:192.168.1.104
2019-03-2623:51:14.658  INFO 2970 --- [nio-8080-exec-3] cn.van.redis.view.aspect.PageViewAspect  : ipAddr:192.168.1.104
2019-03-2623:51:14.692  INFO 2970 --- [nio-8080-exec-3] cn.van.redis.view.aspect.PageViewAspect  : 该ip:192.168.1.104,访问的浏览量已经新增过了
2019-03-2623:51:14.752  INFO 2970 --- [nio-8080-exec-3] c.v.r.v.web.controller.PageController    : articleId = 1
2019-03-2623:51:14.760  INFO 2970 --- [nio-8080-exec-3] c.v.r.v.web.controller.PageController    : redis 缓存中浏览数:1
2019-03-2623:51:14.761  INFO 2970 --- [nio-8080-exec-3] c.v.r.v.web.controller.PageController    : 文章总浏览数:11
  • Запланированная задача запускается, и журнал печатается следующим образом
2019-03-27 01:00:00.265  INFO 2974 --- [   scheduling-1] cn.van.redis.view.task.ArticleViewTask   : 浏览量入库开始
2019-03-27 01:00:00.448  INFO 2974 --- [   scheduling-1] io.lettuce.core.EpollProvider            : Starting without optional epoll library
2019-03-27 01:00:00.449  INFO 2974 --- [   scheduling-1] io.lettuce.core.KqueueProvider           : Starting without optional kqueue library
2019-03-27 01:00:00.663  INFO 2974 --- [   scheduling-1] cn.van.redis.view.task.ArticleViewTask   : 数据库更新后的浏览量为:11
2019-03-27 01:00:00.682  INFO 2974 --- [   scheduling-1] cn.van.redis.view.task.ArticleViewTask   : 浏览量入库结束

Посмотрите на базу данных, база данных обнаружила, что количество просмотров страниц увеличилось.11,в то же время,RedisКоличество просмотров страниц исчезло, что указывает на успех!

4. Резюме

4.1 Технический обмен

  1. Пыль Блог
  2. Пыль Блог - Блог Парк
  3. Блог Пыли - Самородки

Подпишитесь на официальный аккаунт, чтобы узнать больше:

风尘博客

4.2 Адрес источника

Пример кода на гитхабе