Spring Boot REST API单用户单日文章浏览量统计方案咨询
Great question! Since you want to avoid adding a new database table, leveraging Spring Cache (with either in-memory caching or a distributed cache like Redis) is the perfect approach here. The core idea is to track unique user-article-day combinations in cache, so we only increment the view count once per user per day for each article.
Here's a step-by-step implementation tailored to your needs:
1. Prerequisites
- Ensure your Spring project uses Spring Security to validate tokens and expose the authenticated user's ID (we'll pull this from the security context later).
- Add Spring Cache dependencies (plus Redis if you need distributed caching for multi-instance deployments).
2. Design the Cache Key
We need a unique key to flag if a user has already triggered the view count for an article on a given day. Use this format:
view:user:{userId}:article:{articleId}:date:{yyyy-MM-dd}
If this key exists in the cache, we skip incrementing the view count. If not, we increment the count and store the key.
3. Configure Spring Cache
Option A: In-Memory Cache (Caffeine)
For single-instance apps, Caffeine is lightweight and easy to set up. Add these dependencies to your pom.xml (Maven):
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency>
Enable caching in your main application class:
@SpringBootApplication @EnableCaching public class ArticleApplication { public static void main(String[] args) { SpringApplication.run(ArticleApplication.class, args); } }
Option B: Distributed Cache (Redis)
For multi-instance deployments, Redis ensures cache consistency across nodes. Add the Redis dependency:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
Configure Redis in application.properties:
spring.redis.host=localhost spring.redis.port=6379
4. Implement the View Count Logic
First, create a helper method to fetch the current user's ID from the authenticated token:
private String getCurrentUserId() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); // Adjust this to match your user principal implementation return ((CustomUserPrincipal) auth.getPrincipal()).getId(); }
Then, build the core logic in your article service:
@Service public class ArticleService { private final ArticleRepository articleRepo; private final CacheManager cacheManager; public ArticleService(ArticleRepository articleRepo, CacheManager cacheManager) { this.articleRepo = articleRepo; this.cacheManager = cacheManager; } public Article getArticle(Long articleId) { Article article = articleRepo.findById(articleId) .orElseThrow(() -> new RuntimeException("Article not found")); String userId = getCurrentUserId(); String today = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE); String cacheKey = String.format("view:user:%s:article:%d:date:%s", userId, articleId, today); Cache cache = cacheManager.getCache("dailyArticleViews"); if (cache != null && cache.get(cacheKey) == null) { // User hasn't viewed this article today - increment count article.setViewCount(article.getViewCount() + 1); articleRepo.save(article); // Calculate time until midnight to set cache expiration LocalDateTime midnight = LocalDate.now().plusDays(1).atStartOfDay(); long secondsUntilMidnight = ChronoUnit.SECONDS.between(LocalDateTime.now(), midnight); // Add key to cache with daily expiration if (cache instanceof RedisCache redisCache) { redisCache.getNativeCache().expire(cacheKey, Duration.ofSeconds(secondsUntilMidnight)); redisCache.put(cacheKey, Boolean.TRUE); } else if (cache instanceof CaffeineCache caffeineCache) { caffeineCache.getNativeCache().put(cacheKey, Boolean.TRUE, Expiration.after(secondsUntilMidnight, TimeUnit.SECONDS)); } } return article; } }
5. Controller Endpoint
Wire up the service to your API endpoint:
@RestController @RequestMapping("/api/articles") public class ArticleController { private final ArticleService articleService; public ArticleController(ArticleService articleService) { this.articleService = articleService; } @GetMapping("/{id}") public ResponseEntity<Article> fetchArticle(@PathVariable Long id) { Article article = articleService.getArticle(id); return ResponseEntity.ok(article); } }
Key Trade-Offs to Keep in Mind
- In-Memory Cache: Simple and fast, but cache is lost on app restart or scaling to multiple instances (could lead to duplicate view counts in these scenarios). Ideal for small, single-instance apps.
- Redis: Persistent and distributed, so no duplicate counts across restarts or instances. Requires running a Redis server, adding minor infrastructure overhead.
- Cache Expiration: By setting keys to expire at midnight, we ensure users can trigger the view count increment again the next day automatically.
This approach stays within Spring's ecosystem, avoids redundant database tables, and efficiently enforces the daily unique view constraint.
内容的提问来源于stack exchange,提问作者user2137817

