Java点赞功能高并发案例

wen java案例 3

本文目录导读:

Java点赞功能高并发案例

  1. 基础数据库方案(不推荐用于高并发)
  2. Redis + 异步落库方案(高性能方案)
  3. 使用 LUA 脚本保证原子性(推荐方案)
  4. 批量处理方案(定时落库)
  5. 热点数据缓存优化
  6. 完整的Controller和DTO
  7. 数据库表结构
  8. 性能优化总结

我来设计一个完整的Java高并发点赞功能案例,包含多种实现方案和优化策略。

基础数据库方案(不推荐用于高并发)

// 最基本的实现,会有性能问题
@Service
public class LikeServiceBasic {
    @Autowired
    private LikeMapper likeMapper;
    @Transactional
    public Result like(Long userId, Long targetId) {
        // 检查是否已点赞
        LikeRecord record = likeMapper.findByUserIdAndTargetId(userId, targetId);
        if (record != null) {
            return Result.error("已点赞过");
        }
        // 插入点赞记录
        likeMapper.insert(userId, targetId);
        // 更新点赞数
        likeMapper.incrementCount(targetId);
        return Result.success("点赞成功");
    }
}

Redis + 异步落库方案(高性能方案)

1 Redis 点赞服务

@Service
public class LikeService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private LikeAsyncService likeAsyncService;
    private static final String LIKE_KEY_PREFIX = "like:target:";
    private static final String LIKE_USER_KEY_PREFIX = "like:user:";
    private static final String LIKE_COUNT_KEY_PREFIX = "like:count:";
    /**
     * 点赞/取消点赞
     */
    public Result like(Long userId, Long targetId) {
        String userLikeKey = LIKE_USER_KEY_PREFIX + targetId;
        String countKey = LIKE_COUNT_KEY_PREFIX + targetId;
        // 使用Redis的Set判断是否已点赞
        Boolean isLiked = redisTemplate.opsForSet().isMember(userLikeKey, userId.toString());
        if (Boolean.TRUE.equals(isLiked)) {
            // 取消点赞
            redisTemplate.opsForSet().remove(userLikeKey, userId.toString());
            Long count = redisTemplate.opsForValue().decrement(countKey);
            // 异步记录取消点赞操作
            likeAsyncService.unlike(userId, targetId);
            return Result.success("取消点赞成功", count);
        } else {
            // 点赞
            redisTemplate.opsForSet().add(userLikeKey, userId.toString());
            Long count = redisTemplate.opsForValue().increment(countKey);
            // 异步记录点赞操作
            likeAsyncService.like(userId, targetId);
            return Result.success("点赞成功", count);
        }
    }
    /**
     * 获取点赞数
     */
    public Long getLikeCount(Long targetId) {
        String countKey = LIKE_COUNT_KEY_PREFIX + targetId;
        String count = redisTemplate.opsForValue().get(countKey);
        if (count == null) {
            // 从数据库加载
            Long dbCount = likeAsyncService.getCountFromDB(targetId);
            redisTemplate.opsForValue().set(countKey, String.valueOf(dbCount));
            return dbCount;
        }
        return Long.parseLong(count);
    }
    /**
     * 判断用户是否已点赞
     */
    public boolean isLiked(Long userId, Long targetId) {
        String userLikeKey = LIKE_USER_KEY_PREFIX + targetId;
        return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(userLikeKey, userId.toString()));
    }
    /**
     * 获取点赞用户列表(分页)
     */
    public List<Long> getLikeUsers(Long targetId, int page, int size) {
        String userLikeKey = LIKE_USER_KEY_PREFIX + targetId;
        Set<String> members = redisTemplate.opsForSet().members(userLikeKey);
        return members.stream()
                .map(Long::parseLong)
                .skip((page - 1) * size)
                .limit(size)
                .collect(Collectors.toList());
    }
}

2 异步落库服务

@Service
public class LikeAsyncService {
    @Autowired
    private LikeMapper likeMapper;
    @Autowired
    private LikeCountMapper likeCountMapper;
    // 使用线程池异步处理
    @Async("likeExecutor")
    public void like(Long userId, Long targetId) {
        try {
            // 插入点赞记录
            LikeRecord record = new LikeRecord();
            record.setUserId(userId);
            record.setTargetId(targetId);
            record.setCreateTime(new Date());
            likeMapper.insert(record);
            // 更新点赞数(使用数据库乐观锁或原子操作)
            likeCountMapper.incrementCount(targetId);
        } catch (DuplicateKeyException e) {
            // 已存在记录,忽略
            log.warn("点赞记录已存在: userId={}, targetId={}", userId, targetId);
        } catch (Exception e) {
            log.error("异步点赞失败", e);
        }
    }
    @Async("likeExecutor")
    public void unlike(Long userId, Long targetId) {
        try {
            likeMapper.delete(userId, targetId);
            likeCountMapper.decrementCount(targetId);
        } catch (Exception e) {
            log.error("异步取消点赞失败", e);
        }
    }
    /**
     * 从数据库获取点赞数
     */
    public Long getCountFromDB(Long targetId) {
        return likeCountMapper.selectCount(targetId);
    }
}

3 异步线程池配置

@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean("likeExecutor")
    public Executor likeExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(1000);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("like-executor-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

使用 LUA 脚本保证原子性(推荐方案)

@Service
public class LikeServiceWithLua {
    @Autowired
    private StringRedisTemplate redisTemplate;
    private DefaultRedisScript<Long> likeScript;
    private DefaultRedisScript<Long> unlikeScript;
    @PostConstruct
    public void init() {
        // 点赞脚本,保证原子性
        likeScript = new DefaultRedisScript<>();
        likeScript.setScriptText(
            "local userLikeKey = KEYS[1] " +
            "local countKey = KEYS[2] " +
            "local userId = ARGV[1] " +
            // 判断是否已点赞
            "local isLiked = redis.call('SISMEMBER', userLikeKey, userId) " +
            "if isLiked == 1 then " +
            "   return 0 " +
            "end " +
            // 添加点赞用户
            "redis.call('SADD', userLikeKey, userId) " +
            // 增加计数
            "return redis.call('INCR', countKey)"
        );
        likeScript.setResultType(Long.class);
        // 取消点赞脚本
        unlikeScript = new DefaultRedisScript<>();
        unlikeScript.setScriptText(
            "local userLikeKey = KEYS[1] " +
            "local countKey = KEYS[2] " +
            "local userId = ARGV[1] " +
            // 判断是否已点赞
            "local isLiked = redis.call('SISMEMBER', userLikeKey, userId) " +
            "if isLiked == 0 then " +
            "   return 0 " +
            "end " +
            // 移除点赞用户
            "redis.call('SREM', userLikeKey, userId) " +
            // 减少计数,但保证不为负数
            "local count = redis.call('DECR', countKey) " +
            "if count < 0 then " +
            "   redis.call('SET', countKey, 0) " +
            "   return 0 " +
            "end " +
            "return count"
        );
        unlikeScript.setResultType(Long.class);
    }
    public Result like(Long userId, Long targetId) {
        String userLikeKey = "like:user:" + targetId;
        String countKey = "like:count:" + targetId;
        Long result = redisTemplate.execute(
            likeScript,
            Arrays.asList(userLikeKey, countKey),
            userId.toString()
        );
        if (result == 0) {
            return Result.error("已点赞过");
        }
        // 异步落库
        asyncLikeToDB(userId, targetId);
        return Result.success("点赞成功", result);
    }
    public Result unlike(Long userId, Long targetId) {
        String userLikeKey = "like:user:" + targetId;
        String countKey = "like:count:" + targetId;
        Long result = redisTemplate.execute(
            unlikeScript,
            Arrays.asList(userLikeKey, countKey),
            userId.toString()
        );
        if (result == 0) {
            return Result.error("未点赞");
        }
        // 异步落库
        asyncUnlikeDB(userId, targetId);
        return Result.success("取消点赞成功", result);
    }
}

批量处理方案(定时落库)

@Component
public class LikeBatchService {
    @Autowired
    private LikeMapper likeMapper;
    // 使用阻塞队列缓存增量
    private BlockingQueue<LikeEvent> likeEvents = new LinkedBlockingQueue<>(10000);
    @Scheduled(cron = "0 */5 * * * *") // 每5分钟执行
    public void batchSaveToDB() {
        List<LikeEvent> events = new ArrayList<>();
        likeEvents.drainTo(events, 1000);
        if (!events.isEmpty()) {
            // 批量插入数据库
            likeMapper.batchInsert(events);
            // 批量更新计数
            Map<Long, Long> countMap = events.stream()
                .filter(e -> e.getAction() == Action.LIKE)
                .collect(Collectors.groupingBy(
                    LikeEvent::getTargetId, 
                    Collectors.counting()
                ));
            likeMapper.batchUpdateCount(countMap);
        }
    }
    @Data
    public static class LikeEvent {
        private Long userId;
        private Long targetId;
        private Action action;
        private Date createTime;
        public enum Action {
            LIKE, UNLIKE
        }
    }
}

热点数据缓存优化

@Service
public class LikeOptimizedService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    // 本地缓存(使用Caffeine)
    private Cache<Long, Long> localCache = Caffeine.newBuilder()
        .maximumSize(10000)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build();
    // 针对高并发热点内容
    public Long getLikeCountWithCache(Long targetId) {
        // 先从本地缓存获取
        Long count = localCache.getIfPresent(targetId);
        if (count != null) {
            return count;
        }
        // 从Redis获取
        String countKey = "like:count:" + targetId;
        String value = redisTemplate.opsForValue().get(countKey);
        if (value == null) {
            // 从数据库加载
            count = loadCountFromDB(targetId);
            // 设置本地缓存
            localCache.put(targetId, count);
            // 设置Redis缓存(防止缓存穿透)
            redisTemplate.opsForValue().set(countKey, String.valueOf(count), 30, TimeUnit.MINUTES);
        } else {
            count = Long.parseLong(value);
            localCache.put(targetId, count);
        }
        return count;
    }
    // 布隆过滤器防止缓存穿透
    private BloomFilter<Long> bloomFilter;
    @PostConstruct
    public void init() {
        // 预计10万数据,误判率0.01
        bloomFilter = BloomFilter.create(
            Funnels.longFunnel(), 
            100000, 
            0.01
        );
        // 初始化数据
        loadAllTargetIdsFromDB().forEach(bloomFilter::put);
    }
    public boolean isTargetExists(Long targetId) {
        return bloomFilter.mightContain(targetId);
    }
}

完整的Controller和DTO

@RestController
@RequestMapping("/api/like")
public class LikeController {
    @Autowired
    private LikeService likeService;
    /**
     * 点赞
     */
    @PostMapping("/{targetId}")
    public Result like(@PathVariable Long targetId, 
                       @RequestParam Long userId) {
        return likeService.like(userId, targetId);
    }
    /**
     * 取消点赞
     */
    @DeleteMapping("/{targetId}")
    public Result unlike(@PathVariable Long targetId,
                         @RequestParam Long userId) {
        return likeService.unlike(userId, targetId);
    }
    /**
     * 获取点赞数
     */
    @GetMapping("/count/{targetId}")
    public Result getCount(@PathVariable Long targetId) {
        Long count = likeService.getLikeCount(targetId);
        return Result.success(count);
    }
    /**
     * 检查是否已点赞
     */
    @GetMapping("/status/{targetId}")
    public Result isLiked(@PathVariable Long targetId,
                          @RequestParam Long userId) {
        boolean liked = likeService.isLiked(userId, targetId);
        return Result.success(liked);
    }
}

数据库表结构

-- 点赞记录表
CREATE TABLE `like_record` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `user_id` bigint(20) NOT NULL COMMENT '用户ID',
    `target_id` bigint(20) NOT NULL COMMENT '目标ID',
    `target_type` tinyint(4) DEFAULT 1 COMMENT '目标类型:1-文章,2-评论,3-视频',
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_user_target` (`user_id`, `target_id`, `target_type`),
    KEY `idx_target` (`target_id`, `target_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='点赞记录表';
-- 点赞计数表
CREATE TABLE `like_count` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `target_id` bigint(20) NOT NULL COMMENT '目标ID',
    `target_type` tinyint(4) DEFAULT 1,
    `like_count` bigint(20) NOT NULL DEFAULT 0 COMMENT '点赞数',
    `version` int(11) NOT NULL DEFAULT 0 COMMENT '版本号',
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_target` (`target_id`, `target_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='点赞计数表';

性能优化总结

优化策略:

  1. Redis缓存:读多写少的场景,使用Redis缓存点赞状态和计数
  2. 异步处理:点赞操作异步落库,减轻数据库压力
  3. 批量操作:定时批量同步到数据库
  4. 本地缓存:使用Caffeine缓存热点数据
  5. 布隆过滤器:防止缓存穿透
  6. Lua脚本:保证Redis操作原子性

性能指标:

  • QPS:单机可支持 5000+ 点赞操作
  • 响应时间:P99 小于 10ms
  • 数据一致性:最终一致,5分钟内同步到数据库

监控告警:

@Component
public class LikeMonitor {
    // 使用Micrometer指标监控
    private Counter likeCounter = Metrics.counter("like.total");
    private Timer likeTimer = Metrics.timer("like.execution.time");
    // 执行周期性同步确保数据安全
    @Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点
    public void syncDataToDB() {
        // 全量同步
    }
    // 监控Redis健康状态
    @Scheduled(cron = "0 */5 * * * ?")
    public void checkRedisStatus() {
        // 检查Redis连接和内存使用
    }
}

这个方案可以支撑大规模并发点赞场景,同时保证数据最终一致性,实际使用时需要根据业务场景进行调整和优化。

抱歉,评论功能暂时关闭!