淘客返利APP性能优化实战:从MySQL分库分表到Redis缓存穿透解决方案

大家好,我是阿可,微赚淘客系统及省赚客APP创始人,是个冬天不穿秋裤,天冷也要风度的程序猿!

一、背景介绍

在淘客返利APP的开发过程中,随着用户量和数据量的不断增长,系统性能面临巨大挑战。尤其是数据库和缓存的性能瓶颈,直接影响到用户体验和系统的稳定性。本文将分享我们在MySQL分库分表以及Redis缓存穿透问题上的实战经验,希望能为类似项目提供参考。
在这里插入图片描述

二、MySQL分库分表优化

(一)分库分表的必要性

随着业务的快速发展,单表数据量不断增加,导致查询和更新操作的性能大幅下降。分库分表可以有效缓解这一问题,通过将数据分散到多个数据库或表中,减少单表的数据量,提升查询效率。

(二)分库分表策略

我们采用水平分表的方式,将佣金记录表按时间维度进行拆分。例如,每月生成一个新的表,表名格式为commission_record_YYYYMM。同时,为了进一步提升性能,我们还对用户信息表进行了分库操作,将用户数据按照用户ID的哈希值分配到不同的数据库中。

分表代码示例:

package cn.juwatech.common.utils;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class TableNameUtil {
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMM");

    public static String getCommissionRecordTableName(LocalDate date) {
        return "commission_record_" + date.format(FORMATTER);
    }
}

分表查询代码示例:

package cn.juwatech.commission.repository;

import cn.juwatech.common.utils.TableNameUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.time.LocalDate;

@Repository
public class CommissionRecordRepository {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public List<CommissionRecord> getCommissionRecords(LocalDate date) {
        String tableName = TableNameUtil.getCommissionRecordTableName(date);
        String sql = "SELECT * FROM " + tableName;
        return jdbcTemplate.query(sql, new CommissionRecordRowMapper());
    }
}

(三)分库操作

对于用户信息表,我们采用分库策略,将用户数据按照用户ID的哈希值分配到不同的数据库中。例如,我们创建了10个数据库,用户ID的哈希值对10取模的结果决定了用户数据存储在哪个数据库中。

分库代码示例:

package cn.juwatech.common.utils;

public class DatabaseUtil {
    public static int getDatabaseIndex(Long userId) {
        return Math.abs(userId.hashCode()) % 10;
    }
}

分库查询代码示例:

package cn.juwatech.user.repository;

import cn.juwatech.common.utils.DatabaseUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class UserRepository {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public User getUserById(Long userId) {
        int dbIndex = DatabaseUtil.getDatabaseIndex(userId);
        String sql = "SELECT * FROM user WHERE id = ?";
        return jdbcTemplate.queryForObject(sql, new Object[]{userId}, new UserRowMapper());
    }
}

三、Redis缓存穿透解决方案

(一)缓存穿透问题

缓存穿透是指查询一个不存在的数据,由于缓存不会保存不存在的数据,每次都会查询数据库,导致数据库压力增大。在淘客返利APP中,用户可能会频繁查询不存在的返利规则或订单信息,导致缓存穿透问题。

(二)解决方案

我们采用以下几种方式解决缓存穿透问题:

  1. 缓存空对象:对于查询不到的数据,将空对象或特殊标记存储到缓存中,避免后续重复查询数据库。
  2. 布隆过滤器:使用布隆过滤器预先存储可能存在的数据ID,查询时先通过布隆过滤器判断是否存在,从而减少对数据库的无效查询。

缓存空对象代码示例:

package cn.juwatech.common.cache;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class RedisCacheService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public <T> T getFromCacheOrDb(String key, Class<T> clazz, Supplier<T> dbSupplier) {
        T value = (T) redisTemplate.opsForValue().get(key);
        if (value == null) {
            value = dbSupplier.get();
            if (value != null) {
                redisTemplate.opsForValue().set(key, value, 10, TimeUnit.MINUTES);
            } else {
                // 缓存空对象
                redisTemplate.opsForValue().set(key, "NOT_FOUND", 1, TimeUnit.MINUTES);
            }
        }
        return value;
    }
}

布隆过滤器代码示例:

package cn.juwatech.common.filter;

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

import java.util.concurrent.ConcurrentHashMap;

public class BloomFilterService {
    private static final ConcurrentHashMap<String, BloomFilter<Long>> filters = new ConcurrentHashMap<>();

    public static void createFilter(String key, int expectedInsertions, double fpp) {
        BloomFilter<Long> filter = BloomFilter.create(Funnels.longFunnel(), expectedInsertions, fpp);
        filters.put(key, filter);
    }

    public static boolean mightContain(String key, Long id) {
        BloomFilter<Long> filter = filters.get(key);
        return filter != null && filter.mightContain(id);
    }

    public static void put(String key, Long id) {
        BloomFilter<Long> filter = filters.get(key);
        if (filter != null) {
            filter.put(id);
        }
    }
}

使用布隆过滤器优化查询代码示例:

package cn.juwatech.commission.service;

import cn.juwatech.common.cache.RedisCacheService;
import cn.juwatech.common.filter.BloomFilterService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CommissionService {
    @Autowired
    private RedisCacheService redisCacheService;

    public Commission getCommissionById(Long id) {
        if (!BloomFilterService.mightContain("commission_ids", id)) {
            return null;
        }
        return redisCacheService.getFromCacheOrDb("commission_" + id, Commission.class, () -> {
            // 查询数据库
            return queryCommissionFromDb(id);
        });
    }

    private Commission queryCommissionFromDb(Long id) {
        // 数据库查询逻辑
        return new Commission();
    }
}

(三)缓存雪崩与失效问题

缓存雪崩是指大量缓存同时失效,导致大量请求直接打到数据库,引发数据库崩溃。我们通过以下方式解决:

  1. 设置不同的过期时间:为缓存设置随机的过期时间,避免大量缓存同时失效。
  2. 本地缓存兜底:使用本地缓存(如Guava Cache)作为二级缓存,当Redis缓存失效时,本地缓存可以提供临时数据。

本地缓存代码示例:

package cn.juwatech.common.cache;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

import java.util.concurrent.TimeUnit;

public class LocalCacheService {
    private static final Cache<String, Object> cache = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build();

    public static <T> T getFromLocalCache(String key, Class<T> clazz, Supplier<T> dbSupplier) {
        T value = (T) cache.getIfPresent(key);
        if (value == null) {
            value = dbSupplier.get();
            if (value != null) {
                cache.put(key, value);
            }
        }
        return value;
    }
}

结合本地缓存与Redis缓存的查询代码示例:

package cn.juwatech.commission.service;

import cn.juwatech.common.cache.LocalCacheService;
import cn.juwatech.common.cache.RedisCacheService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CommissionService {
    @Autowired
    private RedisCacheService redisCacheService;

    public Commission getCommissionById(Long id) {
        return LocalCacheService.getFromLocalCache("commission_" + id, Commission.class, () -> {
            return redisCacheService.getFromCacheOrDb("commission_" + id, Commission.class, () -> {
                // 查询数据库
                return queryCommissionFromDb(id);
            });
        });
    }

    private Commission queryCommissionFromDb(Long id) {
        // 数据库查询逻辑
        return new Commission();
    }
}

四、总结

通过MySQL分库分表和Redis缓存穿透解决方案的实践,我们成功优化了淘客返利APP的性能,显著提升了系统的响应速度和稳定性。分库分表有效缓解了数据库的压力,而缓存穿透解决方案则减少了不必要的数据库查询,保障了系统的高可用性。未来,我们还将继续探索更多优化手段,进一步提升系统的性能和用户体验。

本文著作权归聚娃科技省赚客app开发者团队,转载请注明出处!

Logo

更多推荐