SpringBoot + Redis 实现点赞
点赞
介绍
"点赞业务"是指一种旨在增加社交媒体平台上点赞或其他积极反应数量的服务或产品。这些服务可以涉及自动化机器人生成假的点赞和粉丝,也可以包括更为复杂的营销策略,旨在创建有吸引力的内容并建立忠实的追随者。然而,需要注意的是,许多社交媒体平台都严格禁止使用假的点赞和粉丝,参与此类行为可能会导致账户被封禁甚至面临法律后果。(GPT)
技术选择 (先考虑最简单的版本)
SpringBoot + Redis + IDEA + hutool + guava
使用技巧 :
责任链设计模式 + redis increment 操作实现原子性 + 策略模式 + (考虑实现幂等操作后面完善)
1、责任链模式:实现参数的非空判断、实现缓存数据的非负判断
2、redis increment 实现原子性增加
3、策略模式:通过分析我们发现点在、踩、取消点赞本质是一个操作所以我们可用使用策略模式来进行优化
创建工程
1、IDEA 新建 SpringBoot 工程
2、导入相关依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ayi</groupId>
<artifactId>boot-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.ayi</groupId>
<artifactId>start-application</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>start-application</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.github.mgunlogson</groupId>
<artifactId>cuckoofilter4j</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3、责任链模式相关编码
1、抽象责任链处理类
需要包含 handler 处理类和业务处理标记
public interface AbstractChainHandler<T> extends Ordered {
/**
* 执行责任链逻辑
*
* @param requestParam 责任链执行入参
*/
void handler(T requestParam);
/**
* @return 责任链组件标识
*/
String mark();
}
- 注意:由于责任链模式中每一个具体的 handler 都有特定的执行顺序,所以我们可用去继承 Spring 为我们提供的顺序接口 Ordered 来保证业务的正常运行
2、由于责任链中包含了多个处理逻辑流程,需要构建一个区分标识抽象类
public interface StarChainFilter<T extends StarCommond> extends AbstractChainHandler<StarCommond> {
/**
* @return 责任链组件标识
*/
@Override
default String mark() {
return TypeMarkEnum.START_FILTER_CHAIN.name();
}
}
3、编写具体的处理模式
@Component
public class StarParamNotNullChainHandler implements StarChainFilter<StarCommond>{
@Override
public int getOrder() {
return 0;
}
@Override
public void handler(StarCommond requestParam) {
if (StrUtil.isEmpty(requestParam.getUserId())) {
throw new RuntimeException("User id input is empty");
}
else if (StrUtil.isEmpty(requestParam.getBookId())) {
throw new RuntimeException("Book ID cannot be empty");
}
else if (ObjectUtil.isEmpty(requestParam.getCode())) {
throw new RuntimeException("The input number cannot be empty");
}
}
}
注意:参数的非空判断放在责任链模式的第一层
@Component
public class StarNotNegativeChainHandler implements StarChainFilter<StarCommond>{
@Resource
private RedisTemplate redisTemplate;
public String buildKey(StarCommond requestParam) {
return CacheUtil.buildKey(requestParam.getBookId() , requestParam.getUserId() , "like");
}
@Override
public void handler(StarCommond requestParam) {
String key = buildKey(requestParam);
checkExistOrCreate(key);
if (checkExistOrCreate(key)) {
final Integer starNum = (Integer)redisTemplate.opsForValue().get(key);
final String action = StarEnum.matchStatusByCode(requestParam.getCode());
if (action.equals(StarEnum.DOWN.name())) {
if (starNum <= 0) {
throw new RuntimeException("Have a pity that the current number of likes is less than 0");
}
}
}
}
public Boolean checkExistOrCreate(String key) {
Boolean hasKey = redisTemplate.hasKey(key);
if (!hasKey) {
redisTemplate.opsForValue().set(key , 0);
return Boolean.FALSE;
}
return Boolean.TRUE;
}
@Override
public int getOrder() {
return 1;
}
}
- 注意:业务逻辑检查缓存 key 是否存在,根据具体的操作行为来执行我们的业务逻辑,如果是踩需要判断此时如果数据是 <= 0 那么抛出异常此次操作无效
4、编写抽象责任链上下文并委托给 Spring 进行管理
@Component
public final class AbstractChainContext<T> implements CommandLineRunner {
private final Map<String, List<AbstractChainHandler>> abstractChainHandlerContainer = Maps.newHashMap();
public void handler(String mark, T requestParam) {
List<AbstractChainHandler> abstractChainHandlers = abstractChainHandlerContainer.get(mark);
if (CollUtil.isEmpty(abstractChainHandlers)) {
throw new RuntimeException(String.format("[%s] Chain of Responsibility ID is undefined.", mark));
}
abstractChainHandlers.forEach(each -> each.handler(requestParam));
}
@Override
public void run(String... args) throws Exception {
Map<String, AbstractChainHandler> chainFilterMap = SpringUtil
.getBeansOfType(AbstractChainHandler.class);
chainFilterMap.forEach((beanName, bean) -> {
List<AbstractChainHandler> abstractChainHandlers = abstractChainHandlerContainer.get(bean.mark());
if (CollectionUtils.isEmpty(abstractChainHandlers)) {
abstractChainHandlers = new ArrayList();
}
abstractChainHandlers.add(bean);
List<AbstractChainHandler> actualAbstractChainHandlers = abstractChainHandlers.stream()
.sorted(Comparator.comparing(Ordered::getOrder))
.collect(Collectors.toList());
abstractChainHandlerContainer.put(bean.mark(), actualAbstractChainHandlers);
});
}
}
- 1、通过实现 CommandLineRunner 能保证在 SpringBoot 启动时初始化我们的 handler 数据
- 2、通过 Hutool 为我们的提供的 SpringUtil 工具类来获取对应的 Spring 管理的 bean
编写业务方法
1、StarService
@Repository
public class StartRepository {
@Resource
private RedisTemplate redisTemplate;
@Resource
private AbstractChainContext<StarCommond> abstractChainContext;
/**
* 点赞 or 踩
* @param userId 用户 ID
* @param bookId 书籍 ID
* @param mark 标识
*/
public void startActive(StarCommond starCommond , String mark) {
/**
* 参数校验
*/
abstractChainContext.handler(mark , starCommond);
/**
* 点赞
*/
int value = starCommond.getCode() == StarEnum.UP.getCode() ? 1 : -1;
// 构建缓存 Key
String key = CacheUtil.buildKey(starCommond.getBookId() , starCommond.getUserId() , "like");
redisTemplate.opsForValue().increment(key, value);
}
}
测试
@SpringBootTest
@RunWith(SpringRunner.class)
public class StartApplicationTest {
@Resource
private StartRepository startRepository;
private static final Faker faker = new Faker();
@Test
public void testStarFunction() {
String userId = String.valueOf(faker.random().nextInt(5));
String bookId = String.valueOf(faker.random().nextInt(5));
StarCommond starCommond = new StarCommond(userId , bookId , 0);
// 校验
startRepository.startActive(starCommond , TypeMarkEnum.START_FILTER_CHAIN.name());
}
}