1. Background
Article browsing statistics, low's practice is: each time a user browses, the front-end will send a GET request for details of an article, the article will be + 1 browsing volume, stored in the database.
1.1 There are several problems with this:
- Write data in the business logic of the GET request!
- If the concurrency is high, the database pressure is too high;
- At the same time, if articles are stored in caches and search engines such as ElasticSearch, synchronous updates cache and ElasticSearch updates are too time consuming to synchronize updates, not updating will result in data inconsistencies.
1.2 Solution
- HyperLogLog
HyperLog is one of the Probabilistic data Structures. The basic idea of such data structures is to use algorithms on statistical probability, sacrificing the accuracy of the data to save memory and improve the performance of related operations.
- Design Ideas
- In order to ensure true blog browsing, the only check is made according to the ip and article id accessed by the user, that is, the same user visits the same article multiple times and the amount of access to changed articles increases by only 1;
- Store users'views in Redis using opsForHyperLogLogLog (). add (key, value), and update the views to the database through a timed task at midnight when they have low views.
2. Hand-held implementation
2.1 Project Configuration
- sql
DROP TABLE IF EXISTS `article`; CREATE TABLE `article` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'Primary key', `title` varchar(100) NOT NULL COMMENT 'Title', `content` varchar(1024) NOT NULL COMMENT 'content', `url` varchar(100) NOT NULL COMMENT 'address', `views` bigint(20) NOT NULL COMMENT 'Views', `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Creation Time', PRIMARY KEY (`id`) ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; INSERT INTO article VALUES(1,'Test Article','content','url',10,NULL);
Insert a piece of data and design access is already 10 for easy testing.
- Projects depend on 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: # Database Configuration 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 # Connection timeout, type of parameter in 2.0 is Duration, where you need to specify the unit when configuring # Connection pool configuration, using jedis or lettuce directly in 2.0 (using lettuce, dependencies must contain commons-pool2 packages) lettuce: pool: # Maximum number of idle connections max-idle: 500 # Minimum number of idle connections min-idle: 50 # Maximum time to wait for available connections, negative unlimited max-wait: -1s # Maximum number of active connections, negative unlimited max-active: -1 # mybatis mybatis: mapper-locations: classpath:mapper/*.xml # type-aliases-package: cn.van.redis.view.entity
2.2 View Side Design
- Customize a comment to add new article views to Redis
@Target({ElementType.PARAMETER, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface PageView { /** * describe */ String description() default ""; }
- Cutting treatment
@Aspect @Configuration @Slf4j public class PageViewAspect { @Autowired private RedisUtils redisUtil; /** * breakthrough point */ @Pointcut("@annotation(cn.van.redis.view.annotation.PageView)") public void PageViewAspect() { } /** * Cut-in processing * @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; // Store views in redis Long num = redisUtil.add(key,ipAddr); if (num == 0) { log.info("this ip:{},Visits have been added", ipAddr); } obj = joinPoint.proceed(); } catch (Throwable e) { e.printStackTrace(); } return obj; } }
- Tool class RedisUtils.java
@Component public class RedisUtils { @Resource private RedisTemplate<String, Object> redisTemplate; /** * Delete Cache * @param key One or more values can be passed */ public void del(String... key) { redisTemplate.delete(key[0]); } /** * count * @param key * @param value */ public Long add(String key, Object... value) { return redisTemplate.opsForHyperLogLog().add(key,value); } /** * Get Total * @param key */ public Long size(String key) { return redisTemplate.opsForHyperLogLog().size(key); } }
- Tool class IpUtils.java
This tool class is OK for me to test under Mac, if there is any problem under Windows, please give me feedback
@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("Acquired ip Address:{}", ip.getHostAddress()); return ip.getHostAddress(); } } } } } catch (Exception e) { log.error("Obtain ip Address failure,{}",e); } return null; } }
2.3 Synchronization task ArticleViewTask.java
The code in ArticleService.java is relatively simple, as detailed in the source code at the end of the article.
@Component @Slf4j public class ArticleViewTask { @Resource private RedisUtils redisUtil; @Resource ArticleService articleService; // Execute at 1 a.m. every day @Scheduled(cron = "0 0 1 * * ? ") @Transactional(rollbackFor=Exception.class) public void createHyperLog() { log.info("Browse Inbound Start"); List<Long> list = articleService.getAllArticleId(); list.forEach(articleId ->{ // Get the number of views for each article in redis and store them in the database 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("Updated views of the database are:{}", views); redisUtil.del(key); } } }); log.info("Browse Inbound End"); } }
2.4 Test interface PageController.java
@RestController @Slf4j public class PageController { @Autowired private ArticleService articleService; @Autowired private RedisUtils redisUtil; /** * When you visit an article, increase its number of views: focused notes * @param articleId: Article 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 Number of browses in the cache:{}", view); //Get directly from the cache and add to the previous number Long views = view + blog.getViews(); log.info("Total article views:{}", views); } catch (Throwable e) { return "error"; } return "success"; } }
Here, the specific methods in the service are all handled by me in the Controller, so there are only simple Mapper calls left, which won't waste time. See the source code at the end of the article.(Logically, these logical processes should be placed in the Service process, please optimize them according to the actual situation)
3. Testing
Start the project, test access, request first http://localhost:8080/1 , the log is printed as follows:
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 : Acquired ip Address: 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 Number of browses in the cache: 1 2019-03-2623:50:50.900 INFO 2970 --- [nio-8080-exec-1] c.v.r.v.web.controller.PageController : Total articles viewed: 11
Looking at the database, the amount of access did not increase, local access again, found that the log prints as follows:
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 : Acquired ip Address: 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 : this ip:192.168.1.104,Visits have been added 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 Number of browses in the cache: 1 2019-03-2623:51:14.761 INFO 2970 --- [nio-8080-exec-3] c.v.r.v.web.controller.PageController : Total articles viewed: 11
- Timed task triggered, log printed as follows
2019-03-27 01:00:00.265 INFO 2974 --- [ scheduling-1] cn.van.redis.view.task.ArticleViewTask : Browse Inbound Start 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 : Updated views of the database are as follows:11 2019-03-27 01:00:00.682 INFO 2974 --- [ scheduling-1] cn.van.redis.view.task.ArticleViewTask : Browse Inbound End
Looking at the database, we find that the number of browses in the database has increased to 11. At the same time, the number of browses in Redis has disappeared, indicating success!