关于亿级流量网站架构一书缓存机制的探讨

在京东的亿级流量网站架构一书,175页介绍缓存有这样一段话

仅就这段代码来看,在高并发情况下,实际上并不能阻止大量线程调用loadSync函数

当然这个书里的代码是作者的简写,这里探讨只是针对书中这段代码,实际生成代码应该有考虑这个问题,另外loadSync函数的逻辑看不到,也可能有考虑到到这个问题。

这中情况应该使用双锁,另外firstCreateNewEntry也应该是定义为volatile类型。还有如果是分布式缓存,针对远程客户端的回源请求应该要设置一个时限,比如30秒内只受理一个回源请求。

下面给出本人项目中使用的缓存加载机制。本人项目根据机构,应用,数据库类型三个字段进行了分库。因此缓存最粗粒度也是这个级别的。

高并发治理关键点

  • 初始获取锁对象时使用双锁机制。
  • 使用获取到的锁对象同步代码。
  • 记录上次重刷时间的 lastReloadDate来自ConcurrentHashMap对象,对象在jvm的堆中,所以无需volatile类型就能保证线程间的可见性。
  • 最终加载数据时没有使用双锁,因为本项目是使用分布式缓存,都是由远程客户端发起的回源请求,双锁只能保证本地缓存高并发时刻多线程不会同时进入,而不能防止远程回源。因为远程调用时陆陆续续到达的,这里假设30秒缓存能加载完成,一旦加载完成就不会有客户端要求回源。该机制保障了30秒内的回源请求只会触发一次缓存加载。(客户端发现无缓存,发起回源请求,由于网络延迟,请求还未到达缓存加载服务器,这时即使缓存已经加载完成了,如果不防范,这些在路上的回源请求也会被受理)
/**
 * 作者: 林森
 * 日期: 2017年1月5日
 * CopyRight @lins
 */
package com.yunkang.ykcachemanage.provider.service;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.ColumnMapRowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;

import com.alibaba.dubbo.config.annotation.Service;
import com.yunkang.ykcachemanage.keys.CacheKeys.cache_keys;
import com.yunkang.ykcachemanage.keys.CacheKeys;
import com.yunkang.ykcachemanage.keys.DictCacheLoader;
import com.yunkang.yktechcom.cache.DictCacheHelper;
import com.yunkang.yktechcom.jdbc.JDBCRouteHelper;

/**
 *
 * 项目名称:ykcachemanage-provider
 *
 * 类描述:
 *
 * 从数据库获取数据并缓存到redis中 为防止多个线程并发重刷redis可能导致无限循环加载问题,该服务建议只开一一个,同时刷新方法提供同步保护
 * 另外限制同一个字典十秒内只能刷新一次
 * 该服务一般用于重置或初始化redis,初始化后redis与mysql的同步由业务系统的在维护字典时,手工调用缓存更新api保证.
 *
 *
 * 创建人:林森
 *
 * 创建时间:2017年1月5日 修改人: 修改时间: 修改备注:
 *
 * @version
 *
 */
@Service
public class DictCacheLoaderProvider implements DictCacheLoader {

    @Autowired
    JDBCRouteHelper jdbcRouteHelper;
    @Autowired
    DictCacheHelper dictCacheHelper;

    //针对不同的缓存集合,使用不同的锁
    Map<String, Object> mapLock = new ConcurrentHashMap<>();

    //记录上次重刷缓存的时间,用于防止恶意重刷。
    static Map<String, Date> lastReload = new ConcurrentHashMap<>();

    @Override
    public void reloadCache(String idFieldName, cache_keys setKey, Map<String, Object> splitFields,
            Map<String, Object> filter, Map<String, String> dbParam) {
        String[] splitFieldValue = splitFields.values().toArray(new String[0]);
        String lockKey = CacheKeys.getPrefix(splitFieldValue) + setKey;

        if (mapLock.get(lockKey) == null) {
            synchronized (this) {
                if (mapLock.get(lockKey) == null) {
                    mapLock.put(lockKey, new Object());
                }
            }
        }
        Object lock = mapLock.get(lockKey);
        synchronized (lock) {
            Date lastReloadDate = lastReload.get(lockKey);
            if (lastReload.get(lockKey) == null || (new Date().getTime() - lastReloadDate.getTime()) > 30 * 1000) {// 30秒内不重刷
                dictCacheHelper.removeAll(CacheKeys.getPrefix(splitFieldValue) + setKey);
                this.reloadCacheFromDB(idFieldName, setKey, splitFields, filter,dbParam);
                lastReload.put(lockKey, new Date());
            } else {
                // ===rejectreload redis from db " + setKey + Thread.currentThread());
            }
        }
    }

    static String sqlGetDoctor = "select * from doctors where status=‘1‘";
    static String sqlDefault = "select * from %s where 1=1 ";

    private void reloadCacheFromDB(String idFieldName, cache_keys setKey, Map<String, Object> splitFields,
            Map<String, Object> filter, Map<String, String> dbParam) {
        switch (setKey) {
        case KHLIS_DOCTORS:
            doLoadFromDB(setKey, idFieldName, splitFields, sqlGetDoctor, filter,  dbParam);
            break;
        default:
            doLoadFromDB(setKey, idFieldName, splitFields, getSqlFromSetKey(setKey, sqlDefault), filter, dbParam);
            break;
        }
    }

    // 使用setkey推测数据表的名字.
    private String getSqlFromSetKey(cache_keys setKey, String sql) {
        String tableName = setKey.toString().substring(setKey.toString().indexOf(‘_‘) + 1).toLowerCase();
        return String.format(sql, tableName);
    }

    /**
     * @param setKey 用于确定要加载的字典表
     * @param idFieldName 字典表的主键名称,多个冒号隔开
     * @param splitFields 字典表切割字段,将字典表切割为多个缓存.
     * @param filter 只有满足条件的才会加载到缓存
     * @param dbParam 对应数据路由表的用于分库的三个字段 org_id,app_id,dbs_type
     *    从数据库读取字典表的数据后更新缓存
     */
    private void doLoadFromDB(cache_keys setKey, String idFieldName, Map<String, Object> splitFields, String sql,
            Map<String, Object> filter, Map<String, String> dbParam) {
        String orgId =  dbParam.get("org_id");
        String appId =  dbParam.get("app_id");
        String dbsType =  dbParam.get("dbs_type");
        if (orgId == null || appId == null|| dbsType == null)
            throw new RuntimeException("org_id,app_id,dbs_type必须有,否则无法加载缓存");

        String[] splitFieldValues = splitFields.values().toArray(new String[0]);

        List<Map<String, Object>> po = new ArrayList<>();
        StringBuilder sb = new StringBuilder();
        sb.append(sql);
        for (Entry<String, Object> item : splitFields.entrySet()) {
            sb.append(String.format(" and %s=:%s ", item.getKey(), item.getKey()));
        }
        Map<String, Object> paramMap = new HashMap<String, Object>();

        if (filter != null) {
            for (Entry<String, Object> item : filter.entrySet()) {
                sb.append(String.format(" and %s=:%s ", item.getKey(), item.getKey()));
            }
            paramMap.putAll(filter);
        }
        paramMap.putAll(splitFields);
        MapSqlParameterSource paramSource = new MapSqlParameterSource(paramMap);
        NamedParameterJdbcTemplate jdbc = jdbcRouteHelper.getJDBCTemplate(orgId, appId,dbsType);
        TransactionTemplate trans = jdbcRouteHelper.getTransactionTemplate(orgId, appId,dbsType);
        po = (List<Map<String, Object>>) trans
                .execute(new TransactionCallback<List<Map<String, Object>>>() {
                    public List<Map<String, Object>> doInTransaction(TransactionStatus status) {
                        return jdbc.query(sb.toString(), paramSource,
                                new ColumnMapRowMapper());
                    }
                });

        Map<String, Object> mapValues = new HashMap<>();
        for (Map<String, Object> item : po) {
            if (idFieldName.contains(":")) {// 多字段主键
                String[] ids = idFieldName.split(":");
                String keys = "";
                for (String _id : ids) {
                    keys += item.get(_id).toString() + ":";
                }
                mapValues.put(keys, item);
            } else {
                mapValues.put(item.get(idFieldName).toString(), item);
            }

        }
        dictCacheHelper.setAll(CacheKeys.getPrefix(splitFieldValues) + setKey, mapValues);

    }

}
时间: 2024-12-25 18:24:47

关于亿级流量网站架构一书缓存机制的探讨的相关文章

观《亿级流量网站架构核心技术》一书有感

本文的架子参考张开套的<亿级流量网站架构核心技术>这本书分为四个部分:指导原则,高可用,高并发,实践案例.这篇文章说一说前三个部分,大部分内容都是我自己的思考,书只作为参考. 指导原则 高可用 事前 副本技术 隔离技术 配额技术 探知技术 预案 事发 监控和报警 事中 降级 回滚 failXXX系列 事后 高并发 提高处理速度 缓存 异步 增加处理人手 多线程 扩容 指导原则 书中所列举的,里有一些可能并不是原则,而是技巧.我理解的原则如下: 高并发原则: 无状态设计:因为有状态可能涉及锁操作

Nginx负载均衡与反向代理—《亿级流量网站架构核心技术》

当我们的应用单实例不能支撑用户请求时,此时就需要扩容,从一台服务器扩容到两台.几十台.几百台.然而,用户访问时是通过如http://www.XX.com的方式访问,在请求时,浏览器首先会查询DNS服务器获取对应的IP,然后通过此IP访问对应的服务.因此,一种方式是www.XX.com域名映射多个IP,但是,存在一个最简单的问题,假设某台服务器重启或者出现故障,DNS会有一定的缓存时间,故障后切换时间长,而且没有对后端服务进行心跳检查和失败重试的机制.因此,外网DNS应该用来实现用GSLB(全局负

《亿级流量网站架构核心技术》---高并发

1.应用级缓存 1.1.缓存简介: 让数据  更 接近  使用者: 目的 让访问速度更快: 工作机制:从缓存读取数据,如果没有,再从慢速设备读取实际数据  并 同步到缓存: eg:CPU读取数据:CPU--->L1/L2/L3--->内存--->磁盘: maven: 本地仓--->中央仓--->远程仓: 1.2.缓存命中率: 从缓存读取次数 / 总读取次数(缓存+磁盘): 非常重要的监控指标,监控此指标看缓存是否工作良好: 1.3.缓存回收策略: a,基于空间 b,基于容量

从100PV到1亿级PV网站架构演变

如果你对项目管理.系统架构有兴趣,请加微信订阅号"softjg",加入这个PM.架构师的大家庭 一个网站就像一个人,存在一个从小到大的过程.养一个网站和养一个人一样,不同时期需要不同的方法,不同的方法下有共同的原则.本文结合我自已14年网站人的经历记录一些架构演变中的体会. 1:积累是必不可少的 架构师不是一天练成的. 1999年,我作了一个个人主页,在学校内的虚拟空间,参加了一次主页大赛,几个DREAMWEAVER的页面,几个TABLE作布局,一个DB连接,几行PHP的代码嵌入在HT

从100PV到1亿级PV网站架构演变(转)

http://www.linuxde.net/2013/05/13581.html 一个网站就像一个人,存在一个从小到大的过程.养一个网站和养一个人一样,不同时期需要不同的方法,不同的方法下有共同的原则.本文结合我自已14年网站人的经历记录一些架构演变中的体会. 积累是必不可少的 架构师不是一天练成的. 1999年,我作了一个个人主页,在学校内的虚拟空间,参加了一次主页大赛,几个DREAMWEAVER的页面,几个TABLE作布局,一个DB连接,几行PHP的代码嵌入在HTML中,再用ftp传到服务

亿级流量场景下,大型缓存架构的虚拟机环境搭建

---内容持续更新--- 小型电商: 静态模板是固定的 数据库中的数据全量喧嚷到模板中,下次请求来了直接返回,速度也很快: 当数据上亿的时候,如果模板改定,把这些所有的数据在mysql中渲染进模板,非常耗时,不现实: 大型电商: 缓存数据生产服务: 不需要再进行全量重新渲染,直接将最新的html模板推送到nginx服务器,请求过来后直接在nginx本地进行渲染进模板中返回请求: redis的重要性: 虚拟机环境设置一: 虚拟机中安装CentOS 启动一个virtual box虚拟机管理软件 使用

亿级流量场景下,大型缓存架构设计实现【3】---- 实现高可用

1.Hystrix是什么? 在分布式系统中,每个服务都可能会调用很多其他服务,被调用的那些服务就是依赖服务,有的时候某些依赖服务出现故障也是很正常的. Hystrix可以让我们在分布式系统中对服务间的调用进行控制,加入一些调用延迟或者依赖故障的容错机制. Hystrix通过将依赖服务进行资源隔离,进而组织某个依赖服务出现故障的时候,这种故障在整个系统所有的依赖服务调用中进行蔓延,同时Hystrix还提供故障时的fallback降级机制 总而言之,Hystrix通过这些方法帮助我们提升分布式系统的

《亿级流量电商详情页系统实战:缓存架构+高可用服务架构+微服务架构》

视频教程:http://www.roncoo.com/course/view/af7d501667fe4a19a60e9467e6d2b3d9 升级说明: 该课程原本是123节课时,已于2017年7月份全部更新完毕.在原有123节课时的基础上,再免费新增70到80节左右的内容(注:课程大纲可能会做进一步优化,具体以最终更新为准),课程名将变更为<亿级流量电商详情页系统实战(第二版):缓存架构+高可用服务架构+微服务架构>简称第二版.本次免费新增内容将会从9月中旬开始更新,一直到10月底更新完毕

亿级流量电商详情页系统的大型高并发与高可用缓存架构实战

对于高并发的场景来说,比如电商类,o2o,门户,等等互联网类的项目,缓存技术是Java项目中最常见的一种应用技术.然而,行业里很多朋友对缓存技术的了解与掌握,仅仅停留在掌握redis/memcached等缓存技术的基础使用,最多了解一些集群相关的知识,大部分人都可以对缓存技术掌握到这个程度.然而,仅仅对缓存相关的技术掌握到这种程度,无论是对于开发复杂的高并发系统,或者是在往Java高级工程师.Java资深工程师.Java架构师这些高阶的职位发展的过程中,都是完全不够用的.技术成长出现瓶颈,在自己