1. Предпосылки
статистика просмотров статей,
low
Подход таков: каждый раз, когда пользователь просматривает страницу, внешний интерфейс отправляетGET
При запросе получения подробной информации о статье количество просмотров этой статьи будет+1
, хранится в базе данных.
1.1 При этом возникает несколько проблем:
- Операция записи данных выполняется в бизнес-логике GET-запроса!
- Если параллелизм высокий, нагрузка на базу данных слишком высока;
- В то же время, если статья кэшируется и поисковые системы, такие как
ElasticSearch
хранилище, синхронно обновлять кеш иElasticSearch
Обновление синхронизации обновления занимает слишком много времени, а отказ от обновления приведет к несогласованности данных.
1.2 Решения
HyperLogLog
HyperLogLog
даProbabilistic data Structures
Одной из основных идей этого типа структуры данных является использование алгоритмов статистической вероятности, жертвуя точностью данных для экономии места в памяти и повышения производительности связанных операций.
- Идеи дизайна
- Чтобы обеспечить реальное количество просмотров сообщений в блоге, согласно посещению пользователя
ip
и статьиid
, Выполнить уникальную проверку, то есть один и тот же пользователь посещает одну и ту же статью несколько раз, а количество посещений измененной статьи увеличивается только на 1; - использовать просмотры страниц пользователя
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 Технический обмен
Подпишитесь на официальный аккаунт, чтобы узнать больше: