MySQL/Redis等6大数据库,在7种Java业务中的选型与调优---youkeit.xyz/898/
从单库到多模协同:Java业务如何用MySQL+Redis构建"关系+缓存"双引擎
引言:双引擎架构的必要性
在当今高并发、大数据的应用场景下,单纯依赖关系型数据库已难以满足业务需求。MySQL作为可靠的关系型数据库,与Redis这一高性能内存数据库的结合,形成了"关系+缓存"的双引擎架构,能够同时保证数据一致性和系统高性能。本文将深入探讨这种架构的设计思路,并通过完整代码示例展示实现方案。
一、基础架构设计
1.1 整体架构图
[客户端]
↓ HTTP请求
[Spring Boot应用层]
↓ 双引擎协同
[Redis缓存层] ↔ [MySQL持久层]
1.2 依赖配置
首先在pom.xml中添加必要依赖:
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Lettuce连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
二、MySQL关系型存储实现
2.1 实体类设计
@Entity
@Table(name = "products")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private BigDecimal price;
@Column(nullable = false)
private Integer stock;
@Column(updatable = false)
private LocalDateTime createTime = LocalDateTime.now();
@Version
private Integer version; // 乐观锁版本号
}
2.2 JPA Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
// 自定义查询方法
List<Product> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);
@Modifying
@Query("UPDATE Product p SET p.stock = p.stock - :quantity WHERE p.id = :id AND p.stock >= :quantity")
int reduceStock(@Param("id") Long id, @Param("quantity") Integer quantity);
}
三、Redis缓存层实现
3.1 Redis配置类
@Configuration
@EnableCaching
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port);
LettuceConnectionFactory factory = new LettuceConnectionFactory(config);
factory.setShareNativeConnection(false); // 每个操作独立连接
return factory;
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());
// 使用Jackson序列化
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(om.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(om);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
@Bean
public CacheManager cacheManager() {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)) // 默认缓存30分钟
.disableCachingNullValues() // 不缓存null值
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(redisConnectionFactory())
.cacheDefaults(config)
.transactionAware()
.build();
}
}
3.2 缓存服务封装
@Service
public class RedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 设置缓存
*/
public <T> void set(String key, T value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, timeout, unit);
}
/**
* 获取缓存
*/
@SuppressWarnings("unchecked")
public <T> T get(String key) {
ValueOperations<String, Object> ops = redisTemplate.opsForValue();
return (T) ops.get(key);
}
/**
* 删除缓存
*/
public boolean delete(String key) {
return redisTemplate.delete(key);
}
/**
* 批量删除
*/
public void delete(Collection<String> keys) {
redisTemplate.delete(keys);
}
/**
* 设置哈希结构
*/
public <T> void hSet(String key, String hashKey, T value) {
redisTemplate.opsForHash().put(key, hashKey, value);
}
/**
* 获取哈希结构
*/
@SuppressWarnings("unchecked")
public <T> T hGet(String key, String hashKey) {
HashOperations<String, String, T> ops = redisTemplate.opsForHash();
return (T) ops.get(key, hashKey);
}
}
四、双引擎协同策略
4.1 缓存穿透解决方案
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Autowired
private RedisService redisService;
private static final String PRODUCT_CACHE_PREFIX = "product:";
/**
* 获取商品详情 - 解决缓存穿透
*/
public Product getProductDetail(Long id) {
String cacheKey = PRODUCT_CACHE_PREFIX + id;
// 1. 先查缓存
Product product = redisService.get(cacheKey);
if (product != null) {
// 缓存命中
return product;
}
// 2. 缓存未命中,查询数据库
product = productRepository.findById(id).orElse(null);
// 3. 数据库不存在,缓存空对象防止穿透
if (product == null) {
redisService.set(cacheKey, new NullValue(), 5, TimeUnit.MINUTES);
return null;
}
// 4. 数据库存在,写入缓存
redisService.set(cacheKey, product, 30, TimeUnit.MINUTES);
return product;
}
// 空对象标识
private static class NullValue implements Serializable {
private static final long serialVersionUID = 1L;
}
}
4.2 缓存雪崩防护
/**
* 获取商品列表 - 解决缓存雪崩
*/
public List<Product> listProducts() {
String cacheKey = "product:list";
// 1. 先查缓存
List<Product> products = redisService.get(cacheKey);
if (products != null) {
return products;
}
// 2. 缓存未命中,查询数据库
products = productRepository.findAll();
// 3. 写入缓存,随机过期时间防止雪崩
if (!products.isEmpty()) {
Random random = new Random();
int timeout = 30 + random.nextInt(15); // 30-45分钟随机
redisService.set(cacheKey, products, timeout, TimeUnit.MINUTES);
}
return products;
}
4.3 热点数据缓存策略
/**
* 获取热门商品 - 热点数据多级缓存
*/
public List<Product> getHotProducts() {
// 一级缓存:本地缓存
List<Product> hotProducts = localCache.get("hot_products");
if (hotProducts != null) {
return hotProducts;
}
// 二级缓存:Redis缓存
hotProducts = redisService.get("hot_products");
if (hotProducts != null) {
// 刷新本地缓存
localCache.put("hot_products", hotProducts);
return hotProducts;
}
// 三级缓存:数据库
hotProducts = productRepository.findTop10ByOrderBySalesDesc();
// 写入多级缓存
if (!hotProducts.isEmpty()) {
redisService.set("hot_products", hotProducts, 10, TimeUnit.MINUTES);
localCache.put("hot_products", hotProducts);
}
return hotProducts;
}
五、事务一致性保障
5.1 双写一致性方案
@Transactional
public void updateProduct(Product product) {
// 1. 更新数据库
productRepository.save(product);
// 2. 删除缓存
String cacheKey = PRODUCT_CACHE_PREFIX + product.getId();
redisService.delete(cacheKey);
// 3. 异步更新缓存
CompletableFuture.runAsync(() -> {
Product updated = productRepository.findById(product.getId()).orElse(null);
if (updated != null) {
redisService.set(cacheKey, updated, 30, TimeUnit.MINUTES);
}
});
}
5.2 分布式锁防并发
public boolean reduceStock(Long productId, Integer quantity) {
String lockKey = "product:lock:" + productId;
String requestId = UUID.randomUUID().toString();
try {
// 获取分布式锁
boolean locked = redisTemplate.opsForValue().setIfAbsent(
lockKey, requestId, 10, TimeUnit.SECONDS);
if (!locked) {
throw new RuntimeException("操作太频繁,请稍后再试");
}
// 查询商品
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("商品不存在"));
// 检查库存
if (product.getStock() < quantity) {
throw new RuntimeException("库存不足");
}
// 扣减库存
int affected = productRepository.reduceStock(productId, quantity);
if (affected == 0) {
throw new RuntimeException("库存扣减失败");
}
// 删除缓存
redisService.delete(PRODUCT_CACHE_PREFIX + productId);
return true;
} finally {
// 释放锁
if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
六、性能优化实践
6.1 批量操作优化
public List<Product> batchGetProducts(List<Long> ids) {
// 1. 先从缓存批量获取
List<String> keys = ids.stream()
.map(id -> PRODUCT_CACHE_PREFIX + id)
.collect(Collectors.toList());
List<Product> cachedProducts = redisTemplate.opsForValue().multiGet(keys);
// 2. 找出未命中的ID
List<Long> missingIds = new ArrayList<>();
List<Product> result = new ArrayList<>();
for (int i = 0; i < ids.size(); i++) {
if (cachedProducts.get(i) != null) {
result.add(cachedProducts.get(i));
} else {
missingIds.add(ids.get(i));
}
}
// 3. 批量查询数据库
if (!missingIds.isEmpty()) {
List<Product> dbProducts = productRepository.findAllById(missingIds);
// 4. 批量写入缓存
Map<String, Product> cacheMap = new HashMap<>();
for (Product product : dbProducts) {
cacheMap.put(PRODUCT_CACHE_PREFIX + product.getId(), product);
}
redisTemplate.opsForValue().multiSet(cacheMap);
// 5. 设置过期时间
for (String key : cacheMap.keySet()) {
redisTemplate.expire(key, 30, TimeUnit.MINUTES);
}
result.addAll(dbProducts);
}
return result;
}
6.2 管道技术提升吞吐量
public void refreshProductCache(List<Long> productIds) {
redisTemplate.executePipelined(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
for (Long id : productIds) {
Product product = productRepository.findById(id).orElse(null);
if (product != null) {
String key = PRODUCT_CACHE_PREFIX + id;
byte[] value = redisTemplate.getValueSerializer().serialize(product);
connection.set(key.getBytes(), value);
connection.expire(key.getBytes(), 1800); // 30分钟
}
}
return null;
}
});
}
七、监控与运维
7.1 缓存命中率统计
@Aspect
@Component
public class CacheHitAspect {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String CACHE_HIT_KEY = "cache:stats:hit";
private static final String CACHE_MISS_KEY = "cache:stats:miss";
@Around("@annotation(org.springframework.cache.annotation.Cacheable)")
public Object trackCacheHit(ProceedingJoinPoint joinPoint) throws Throwable {
String cacheName = getCacheName(joinPoint);
try {
Object result = joinPoint.proceed();
if (result != null) {
redisTemplate.opsForValue().increment(CACHE_HIT_KEY + ":" + cacheName);
}
return result;
} catch (Exception e) {
redisTemplate.opsForValue().increment(CACHE_MISS_KEY + ":" + cacheName);
throw e;
}
}
private String getCacheName(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Cacheable cacheable = signature.getMethod().getAnnotation(Cacheable.class);
return cacheable.value()[0];
}
public Map<String, Double> getCacheHitRate() {
Set<String> cacheNames = redisTemplate.keys(CACHE_HIT_KEY + ":*");
Map<String, Double> hitRates = new HashMap<>();
for (String hitKey : cacheNames) {
String cacheName = hitKey.substring(CACHE_HIT_KEY.length() + 1);
String missKey = CACHE_MISS_KEY + ":" + cacheName;
Long hitCount = (Long) redisTemplate.opsForValue().get(hitKey);
Long missCount = (Long) redisTemplate.opsForValue().get(missKey);
hitCount = hitCount != null ? hitCount : 0L;
missCount = missCount != null ? missCount : 0L;
double hitRate = (double) hitCount / (hitCount + missCount);
hitRates.put(cacheName, hitRate);
}
return hitRates;
}
}
结语:双引擎架构的最佳实践
MySQL+Redis的双引擎架构为Java业务系统提供了可靠的数据持久化和高性能的缓存能力。通过本文的代码示例,我们展示了:
- 基础配置:如何正确配置Spring Boot集成MySQL和Redis
- 协同策略:缓存穿透、雪崩、热点数据的解决方案
- 一致保障:事务一致性和分布式锁的实现
- 性能优化:批量操作和管道技术的应用
- 运维监控:缓存命中率的统计方法
在实际项目中,应根据业务特点选择合适的缓存策略,并通过监控不断优化缓存配置。记住:没有银弹,最适合业务场景的方案才是最好的方案。