Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > MySQL频繁更新热点数据

MySQL频繁更新热点数据高并发场景下的具体解决方案

作者:学亮编程手记

本文详细介绍了MySQL高频更新热点数据在高并发场景下的解决方案,包括应用层缓存、数据分片、队列削峰、数据库层面优化(如CAS、调整事务隔离级别)、读写分离、请求合并、参数调优、分布式计数器等方法,需要的朋友可以参考下

热点数据问题确实是高并发场景下的典型瓶颈。以下是针对热点数据问题的具体解决方案:

1. 应用层缓存 + 批量更新

使用 Redis 计数器

// 应用层累加,定期批量更新到数据库
public class HotSpotCounter {
    private Jedis jedis;
    
    public void increment(String key) {
        // 在Redis中累加
        jedis.incr(key);
    }
    
    // 定时任务,批量同步到数据库
    @Scheduled(fixedRate = 5000) // 每5秒同步一次
    public void syncToDatabase() {
        Set<String> keys = jedis.keys("counter:*");
        for (String key : keys) {
            Long value = Long.parseLong(jedis.get(key));
            String entityId = key.substring(8); // 去掉"counter:"前缀
            
            // 批量更新数据库
            updateDatabase(entityId, value);
            
            // 清空Redis计数器
            jedis.set(key, "0");
        }
    }
}

2. 数据分片(水平拆分)

将单行热点数据拆分为多行

-- 原始热点表
CREATE TABLE page_views (
    page_id INT PRIMARY KEY,
    view_count BIGINT
);

-- 拆分为多行
CREATE TABLE page_views_sharded (
    page_id INT,
    shard_id INT,  -- 分片ID (0-15)
    view_count BIGINT,
    PRIMARY KEY (page_id, shard_id)
);

-- 更新时分散到不同分片
UPDATE page_views_sharded 
SET view_count = view_count + 1 
WHERE page_id = 1001 AND shard_id = RAND() * 16;

-- 查询时汇总
SELECT SUM(view_count) FROM page_views_sharded WHERE page_id = 1001;

3. 队列削峰填谷

使用消息队列缓冲写请求

@Component
public class ViewCountService {
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    
    public void recordView(int pageId) {
        // 发送到消息队列,异步处理
        kafkaTemplate.send("page-view-topic", String.valueOf(pageId));
    }
}

// 消费者,批量处理
@KafkaListener(topics = "page-view-topic")
public void batchUpdateViews(List<String> pageIds) {
    Map<Integer, Long> countMap = pageIds.stream()
        .collect(Collectors.groupingBy(Integer::parseInt, Collectors.counting()));
    
    // 批量更新数据库
    for (Map.Entry<Integer, Long> entry : countMap.entrySet()) {
        updatePageView(entry.getKey(), entry.getValue());
    }
}

4. 数据库层面的优化

使用 CAS (Compare-And-Set) 更新

-- 基于当前值的更新,减少锁竞争
UPDATE products 
SET stock = stock - 1 
WHERE id = 1001 AND stock > 0;

-- 或者使用版本号
UPDATE products 
SET stock = stock - 1, version = version + 1 
WHERE id = 1001 AND version = @current_version;

调整事务隔离级别(临时方案)

-- 对于计数类操作,使用READ-COMMITTED + 短事务
SET SESSION transaction_isolation = 'READ-COMMITTED';
BEGIN;
UPDATE counters SET value = value + 1 WHERE name = 'page_views';
COMMIT;

5. 读写分离

写主库,读从库

-- 写操作指向主库
UPDATE hot_table SET count = count + 1 WHERE id = 1; -- 主库

-- 读操作指向从库  
SELECT count FROM hot_table WHERE id = 1; -- 从库

6. 应用层合并请求

请求合并窗口

public class RequestMerger {
    private Map<Integer, AtomicLong> counterMap = new ConcurrentHashMap<>();
    private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    
    public RequestMerger() {
        // 每100ms批量处理一次
        scheduler.scheduleAtFixedRate(this::flush, 100, 100, TimeUnit.MILLISECONDS);
    }
    
    public void increment(int id) {
        counterMap.computeIfAbsent(id, k -> new AtomicLong(0)).incrementAndGet();
    }
    
    private void flush() {
        Map<Integer, Long> snapshot = new HashMap<>();
        counterMap.forEach((id, atomic) -> {
            long value = atomic.getAndSet(0);
            if (value > 0) {
                snapshot.put(id, value);
            }
        });
        
        // 批量更新数据库
        batchUpdate(snapshot);
    }
}

7. 数据库参数调优

优化 InnoDB 参数

-- 增加锁相关内存
SET GLOBAL innodb_buffer_pool_size = 8G;  -- 根据内存调整
SET GLOBAL innodb_log_file_size = 2G;     -- 增大日志文件
SET GLOBAL innodb_lock_wait_timeout = 10; -- 减少锁等待时间

-- 调整线程并发数
SET GLOBAL innodb_thread_concurrency = 0; -- 0表示不限制

8. 架构层面的解决方案

使用分布式计数器

// 使用 Redis Cluster 分散热点
public class DistributedCounter {
    public void increment(String key) {
        // 使用CRC32分片到不同的Redis节点
        int slot = CRC32.hash(key) % 16384;
        String redisNode = getNodeBySlot(slot);
        redisTemplate(redisNode).opsForValue().increment(key);
    }
}

实际案例:电商库存热点

问题场景

-- 热点商品库存更新,秒杀时大量并发
UPDATE products SET stock = stock - 1 WHERE id = 1001 AND stock > 0;

解决方案

-- 1. 库存分片
CREATE TABLE product_stock_shard (
    product_id INT,
    shard_id TINYINT, -- 0-9 10个分片
    stock INT,
    PRIMARY KEY (product_id, shard_id)
);

-- 2. 更新时随机选择分片
UPDATE product_stock_shard 
SET stock = stock - 1 
WHERE product_id = 1001 AND shard_id = FLOOR(RAND() * 10) AND stock > 0;

-- 3. 检查是否成功,如果失败重试其他分片

监控和诊断

监控热点行锁

-- 查看行锁等待
SELECT * FROM information_schema.INNODB_LOCKS 
WHERE lock_table = 'your_hot_table';

-- 查看锁等待关系
SELECT * FROM information_schema.INNODB_LOCK_WAITS;

监控数据库状态

-- 查看InnoDB状态
SHOW ENGINE INNODB STATUS;

-- 查看当前运行的事务
SELECT * FROM information_schema.INNODB_TRX 
ORDER BY trx_started DESC 
LIMIT 10;

选择策略的建议

关键原则:将串行更新改为并行更新,将实时更新改为批量更新,将单点压力分散到多个节点。

以上就是MySQL频繁更新热点数据高并发场景下的具体解决方案的详细内容,更多关于MySQL频繁更新热点数据的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文