Skip to content

Commit 487f1d6

Browse files
author
yqz
committed
feat:将redis操作抽取成独立模块
1 parent fea6f11 commit 487f1d6

18 files changed

Lines changed: 426 additions & 205 deletions

File tree

OpenBlog-business/pom.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@
5151
</dependency>
5252

5353
<dependency>
54-
<groupId>org.springframework.boot</groupId>
55-
<artifactId>spring-boot-starter-data-redis</artifactId>
54+
<groupId>com.yqz</groupId>
55+
<artifactId>OpenBlog-framework-redis</artifactId>
56+
<version>${project.version}</version>
5657
</dependency>
5758

5859
<dependency>

OpenBlog-business/src/main/java/com/yqz/openblog/OpenBlogApplication.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.yqz.openblog;
22

33
import com.yqz.openblog.config.AuthSecurityProperties;
4-
import com.yqz.openblog.config.CacheProperties;
54
import com.yqz.openblog.config.CorsProperties;
65
import com.yqz.openblog.config.SiteProperties;
76
import org.springframework.boot.SpringApplication;
@@ -14,8 +13,7 @@
1413
@EnableConfigurationProperties({
1514
SiteProperties.class,
1615
CorsProperties.class,
17-
AuthSecurityProperties.class,
18-
CacheProperties.class
16+
AuthSecurityProperties.class
1917
})
2018
public class OpenBlogApplication {
2119

OpenBlog-business/src/main/java/com/yqz/openblog/article/service/ArticlePublishedContentCacheService.java

Lines changed: 37 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
import com.yqz.openblog.article.dto.ArticleListItemResponse;
66
import com.yqz.openblog.article.dto.ArticlePublishedContentCachePayload;
77
import com.yqz.openblog.common.PageResult;
8-
import com.yqz.openblog.config.CacheProperties;
8+
import com.yqz.openblog.redis.config.RedisProperties;
9+
import com.yqz.openblog.redis.core.RedisKeys;
10+
import com.yqz.openblog.redis.core.RedisOps;
911
import org.slf4j.Logger;
1012
import org.slf4j.LoggerFactory;
11-
import org.springframework.data.redis.core.StringRedisTemplate;
1213
import org.springframework.stereotype.Service;
1314

1415
import java.time.Duration;
@@ -28,26 +29,22 @@
2829
@Service
2930
public class ArticlePublishedContentCacheService {
3031

31-
private static final String KEY_PREFIX = "openblog:article:published:content:";
32-
private static final String LIST_KEY_PREFIX = "openblog:article:published:list:v";
33-
private static final String LIST_VERSION_KEY = "openblog:article:published:list:version";
34-
3532
private static final TypeReference<PageResult<ArticleListItemResponse>> LIST_PAGE_TYPE =
3633
new TypeReference<>() {};
3734

3835
private static final Logger log = LoggerFactory.getLogger(ArticlePublishedContentCacheService.class);
3936

40-
private final StringRedisTemplate redisTemplate;
37+
private final RedisOps redisOps;
4138
private final ObjectMapper objectMapper;
42-
private final CacheProperties cacheProperties;
39+
private final RedisProperties redisProperties;
4340

4441
public ArticlePublishedContentCacheService(
45-
StringRedisTemplate redisTemplate,
42+
RedisOps redisOps,
4643
ObjectMapper objectMapper,
47-
CacheProperties cacheProperties) {
48-
this.redisTemplate = redisTemplate;
44+
RedisProperties redisProperties) {
45+
this.redisOps = redisOps;
4946
this.objectMapper = objectMapper;
50-
this.cacheProperties = cacheProperties;
47+
this.redisProperties = redisProperties;
5148
}
5249

5350
// ==================== 文章正文缓存(个体) ====================
@@ -56,14 +53,14 @@ public Optional<ArticlePublishedContentCachePayload> get(Long articleId) {
5653
if (articleId == null) {
5754
return Optional.empty();
5855
}
56+
String json = redisOps.get(RedisKeys.articleBody(articleId)).orElse(null);
57+
if (json == null || json.isBlank()) {
58+
return Optional.empty();
59+
}
5960
try {
60-
String json = redisTemplate.opsForValue().get(key(articleId));
61-
if (json == null || json.isBlank()) {
62-
return Optional.empty();
63-
}
6461
return Optional.of(objectMapper.readValue(json, ArticlePublishedContentCachePayload.class));
6562
} catch (Exception e) {
66-
log.warn("读取文章正文缓存失败,降级为数据库。articleId={}", articleId, e);
63+
log.warn("反序列化文章正文缓存失败。articleId={}", articleId, e);
6764
return Optional.empty();
6865
}
6966
}
@@ -72,24 +69,20 @@ public void put(Long articleId, ArticlePublishedContentCachePayload payload) {
7269
if (articleId == null || payload == null) {
7370
return;
7471
}
75-
int minutes = Math.max(1, cacheProperties.getArticlePublishedTtlMinutes());
72+
int minutes = Math.max(1, redisProperties.getArticlePublishedTtlMinutes());
7673
try {
7774
String json = objectMapper.writeValueAsString(payload);
78-
redisTemplate.opsForValue().set(key(articleId), json, Duration.ofMinutes(minutes));
75+
redisOps.set(RedisKeys.articleBody(articleId), json, Duration.ofMinutes(minutes));
7976
} catch (Exception e) {
80-
log.warn("写入文章正文缓存失败(已忽略,不影响响应)。articleId={}", articleId, e);
77+
log.warn("序列化文章正文缓存失败(已忽略)。articleId={}", articleId, e);
8178
}
8279
}
8380

8481
public void evict(Long articleId) {
8582
if (articleId == null) {
8683
return;
8784
}
88-
try {
89-
redisTemplate.delete(key(articleId));
90-
} catch (Exception e) {
91-
log.warn("删除文章正文缓存失败(已忽略)。articleId={}", articleId, e);
92-
}
85+
redisOps.delete(RedisKeys.articleBody(articleId));
9386
}
9487

9588
// ==================== 文章列表缓存(分页) ====================
@@ -98,37 +91,35 @@ public void evict(Long articleId) {
9891
* 获取当前列表版本号。版本号用于构造缓存 key,不存在时默认为 0。
9992
*/
10093
private long getListVersion() {
101-
try {
102-
String v = redisTemplate.opsForValue().get(LIST_VERSION_KEY);
103-
return v == null ? 0L : Long.parseLong(v);
104-
} catch (Exception e) {
105-
log.warn("读取列表版本号失败,降级为 version=0。", e);
106-
return 0L;
107-
}
94+
return redisOps.get(RedisKeys.CONTENT_ARTICLE_LIST_VERSION)
95+
.map(v -> {
96+
try {
97+
return Long.parseLong(v);
98+
} catch (NumberFormatException e) {
99+
return 0L;
100+
}
101+
})
102+
.orElse(0L);
108103
}
109104

110105
/**
111106
* 递增列表版本号,使所有旧版本列表缓存自然过期(靠 TTL 清理)。
112107
* 任何影响已发布列表的写操作(发布、取消发布、更新已发布文章、删除)都应调用此方法。
113108
*/
114109
public void evictPublishedList() {
115-
try {
116-
redisTemplate.opsForValue().increment(LIST_VERSION_KEY);
117-
} catch (Exception e) {
118-
log.warn("递增列表版本号失败(已忽略)。", e);
119-
}
110+
redisOps.increment(RedisKeys.CONTENT_ARTICLE_LIST_VERSION);
120111
}
121112

122113
public Optional<PageResult<ArticleListItemResponse>> getList(Long categoryId, int page, int size) {
114+
long version = getListVersion();
115+
String json = redisOps.get(RedisKeys.articleList(version, categoryId, page, size)).orElse(null);
116+
if (json == null || json.isBlank()) {
117+
return Optional.empty();
118+
}
123119
try {
124-
long version = getListVersion();
125-
String json = redisTemplate.opsForValue().get(listKey(version, categoryId, page, size));
126-
if (json == null || json.isBlank()) {
127-
return Optional.empty();
128-
}
129120
return Optional.of(objectMapper.readValue(json, LIST_PAGE_TYPE));
130121
} catch (Exception e) {
131-
log.warn("读取文章列表缓存失败,降级为数据库。categoryId={}, page={}, size={}", categoryId, page, size, e);
122+
log.warn("反序列化文章列表缓存失败。categoryId={}, page={}, size={}", categoryId, page, size, e);
132123
return Optional.empty();
133124
}
134125
}
@@ -137,23 +128,13 @@ public void putList(Long categoryId, int page, int size, PageResult<ArticleListI
137128
if (payload == null) {
138129
return;
139130
}
140-
int minutes = Math.max(1, cacheProperties.getArticleListTtlMinutes());
131+
int minutes = Math.max(1, redisProperties.getArticleListTtlMinutes());
141132
try {
142133
long version = getListVersion();
143134
String json = objectMapper.writeValueAsString(payload);
144-
redisTemplate.opsForValue().set(listKey(version, categoryId, page, size), json, Duration.ofMinutes(minutes));
135+
redisOps.set(RedisKeys.articleList(version, categoryId, page, size), json, Duration.ofMinutes(minutes));
145136
} catch (Exception e) {
146-
log.warn("写入文章列表缓存失败(已忽略)。categoryId={}, page={}, size={}", categoryId, page, size, e);
137+
log.warn("序列化文章列表缓存失败(已忽略)。categoryId={}, page={}, size={}", categoryId, page, size, e);
147138
}
148139
}
149-
150-
// ==================== Key 构造 ====================
151-
152-
private static String key(Long articleId) {
153-
return KEY_PREFIX + articleId;
154-
}
155-
156-
private static String listKey(long version, Long categoryId, int page, int size) {
157-
return LIST_KEY_PREFIX + version + ":" + (categoryId == null ? "all" : categoryId) + ":" + page + ":" + size;
158-
}
159140
}

OpenBlog-business/src/main/java/com/yqz/openblog/article/service/ArticleViewService.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import com.yqz.openblog.article.entity.Article;
44
import com.yqz.openblog.article.entity.ArticleStatus;
5-
import com.yqz.openblog.article.limiter.SlidingWindowLimiter;
5+
import com.yqz.openblog.redis.limiter.SlidingWindowLimiter;
6+
import com.yqz.openblog.redis.core.RedisKeys;
67
import com.github.benmanes.caffeine.cache.Cache;
78
import com.github.benmanes.caffeine.cache.Caffeine;
89
import com.yqz.openblog.article.repo.ArticleMapper;
@@ -78,7 +79,7 @@ public boolean tryRecordView(Article article, String clientIp) {
7879
}
7980

8081
private String buildRedisKey(Long articleId, String clientIp) {
81-
return "openblog:article:view:" + articleId + ":" + clientIp;
82+
return RedisKeys.articleView(articleId, clientIp);
8283
}
8384
}
8485

OpenBlog-business/src/main/java/com/yqz/openblog/feedback/service/FeedbackService.java

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
import com.yqz.openblog.feedback.dto.FeedbackListItemResponse;
77
import com.yqz.openblog.feedback.entity.FeedbackEntry;
88
import com.yqz.openblog.feedback.repo.FeedbackRepository;
9+
import com.yqz.openblog.redis.core.RedisKeys;
10+
import com.yqz.openblog.redis.core.RedisOps;
911
import jakarta.servlet.http.HttpServletRequest;
1012
import org.slf4j.Logger;
1113
import org.slf4j.LoggerFactory;
1214
import org.springframework.dao.DataIntegrityViolationException;
1315
import org.springframework.data.domain.Page;
1416
import org.springframework.data.domain.PageRequest;
15-
import org.springframework.data.redis.core.StringRedisTemplate;
1617
import org.springframework.stereotype.Service;
1718
import org.springframework.transaction.annotation.Transactional;
1819

@@ -25,14 +26,12 @@ public class FeedbackService {
2526

2627
private static final Logger log = LoggerFactory.getLogger(FeedbackService.class);
2728

28-
private static final String KEY_PREFIX = "openblog:feedback:ipday:";
29-
3029
private final FeedbackRepository feedbackRepository;
31-
private final StringRedisTemplate redisTemplate;
30+
private final RedisOps redisOps;
3231

33-
public FeedbackService(FeedbackRepository feedbackRepository, StringRedisTemplate redisTemplate) {
32+
public FeedbackService(FeedbackRepository feedbackRepository, RedisOps redisOps) {
3433
this.feedbackRepository = feedbackRepository;
35-
this.redisTemplate = redisTemplate;
34+
this.redisOps = redisOps;
3635
}
3736

3837
@Transactional
@@ -43,7 +42,7 @@ public void create(FeedbackCreateRequest req, HttpServletRequest request) {
4342
ZoneId zone = ZoneId.systemDefault();
4443
LocalDate today = LocalDate.now(zone);
4544

46-
// 先用 Redis 进行每天一次快速判定;Redis 不可用时降级用 DB 判定
45+
// 先用 Redis 进行"每天一次"快速判定;Redis 不可用时降级用 DB 判定
4746
if (!tryAcquireRedis(ipKey, today, zone)) {
4847
throw new BizException(4290, "同一 IP 每天只能提交一次");
4948
}
@@ -89,20 +88,17 @@ private FeedbackListItemResponse toListItem(FeedbackEntry e) {
8988
}
9089

9190
private boolean tryAcquireRedis(String ipKey, LocalDate day, ZoneId zone) {
92-
String key = KEY_PREFIX + ipKey + ":" + day;
93-
try {
94-
Instant now = Instant.now();
95-
Instant nextDayStart = day.plusDays(1).atStartOfDay(zone).toInstant();
96-
Duration ttl = Duration.between(now, nextDayStart);
97-
if (ttl.isNegative() || ttl.isZero()) {
98-
ttl = Duration.ofHours(24);
99-
}
100-
Boolean ok = redisTemplate.opsForValue().setIfAbsent(key, "1", ttl);
101-
return ok == null || ok;
102-
} catch (Exception e) {
103-
log.warn("feedback rate limit skipped (redis unavailable), ipKey={}", ipKey, e);
104-
return true;
91+
String key = RedisKeys.feedbackIpDay(ipKey, day);
92+
Instant now = Instant.now();
93+
Instant nextDayStart = day.plusDays(1).atStartOfDay(zone).toInstant();
94+
Duration ttl = Duration.between(now, nextDayStart);
95+
if (ttl.isNegative() || ttl.isZero()) {
96+
ttl = Duration.ofHours(24);
10597
}
98+
boolean ok = redisOps.setIfAbsent(key, "1", ttl);
99+
if (!ok) {
100+
log.info("反馈限流触发。ipKey={}, day={}", ipKey, day);
101+
}
102+
return ok;
106103
}
107104
}
108-

OpenBlog-business/src/main/java/com/yqz/openblog/site/SiteVisitService.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import com.github.benmanes.caffeine.cache.Cache;
44
import com.github.benmanes.caffeine.cache.Caffeine;
5-
import com.yqz.openblog.article.limiter.SlidingWindowLimiter;
5+
import com.yqz.openblog.redis.limiter.SlidingWindowLimiter;
6+
import com.yqz.openblog.redis.core.RedisKeys;
67
import com.yqz.openblog.site.repo.SiteVisitCounterMapper;
78
import org.slf4j.Logger;
89
import org.slf4j.LoggerFactory;
@@ -43,7 +44,7 @@ public boolean tryRecordVisit(String clientIp) {
4344
if (clientIp == null || clientIp.isBlank()) {
4445
return false;
4546
}
46-
String redisKey = "openblog:site:view:" + clientIp;
47+
String redisKey = RedisKeys.siteVisit(clientIp);
4748
long nowMs = System.currentTimeMillis();
4849
String member = nowMs + "-" + UUID.randomUUID();
4950

0 commit comments

Comments
 (0)