redis入门第十课:实战之缓存

1.为什么需要缓存?

我们知道redis缓存是存储key-value的内存数据库,内存访问数据更快捷。而在web项目中,对于读多写少的高并发场景,我们会经常使用缓存来进行优化。redis缓存的优点:
1:读写性能极高 , redis读对的速度是11万次/s , 写的速度是8.1万次/s.
2:redis 支持多种数据类型,redis存储的是 key–value格式的数据,其中key是字符串,但是value就有多种数据类型了:String , list ,map , set ,有序集合(sorted set)

2.redis缓存架构

项目中redis的架构如下所示:


1556439840(1).png

3.redis缓存遇到的问题

3.1 缓存和数据库写入一致性问题

当写入数据时,会遇到一个问题:是先将数据更新到缓存,还是将数据更新到数据库。必须要知道的是,更新缓存和更新数据库不会是原子性的,如果在更新缓存成功后,未更新数据库,会导致数据异常。如果更新数据库,但是未更新缓存,在从获取数据时,数据也是异常的。这两种操作都会导致数据不一致的问题。所以,我们得根据系统的需求来评估,是先更新缓存还是更新数据库。
由于我们的系统读大于写,在这种场景下,只需要以数据库为主,先写数据库,再写缓存就好了。

3.2 缓存击穿问题

指查询一个不存在的key,而这个key扛着大并发,这时由于缓存没有就需要到数据库中查找,则此时数据库将面临大量的请求压力。

  • 将查询不到的key添加一个标志空值的value,比如:我们场景为查询一个标的的信息,如果这个标的信息不存在,我们设置一个空字符串返回。代码如下:
//无论数据是否为空,都推一个key到redis,防止数据为null时不走缓存
        List<String> jsonList = new ArrayList<String>();
        jsonList.add("");
  • 把所有存在的key都存到另外一个存储的Set集合里,查询的时候将不符合规则的key进行过滤。
    还有最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
    基本原理及要点:对于原理来说很简单,位数组+k个独立hash函数。将hash函数对应的值的位数组置1,查找时如果发现所有hash函数对应位都是1说明存在,很明显这个过程并不保证查找的结果是100%正确的。
  • 这些Key可能不是永远不存在,所以需要根据业务场景来设置过期时间。

3.3 缓存雪崩问题

缓存雪崩,是指在某一个时间段,缓存集中过期失效。这时客户端获取key时,无法获取到,只能从数据库中获取,那么短暂的时间内,大量请求都积压到了数据库,造成数据库雪崩。

  • 这个没有完美解决办法,但可以分析用户行为,尽量让失效时间点均匀分布
  • 批量缓存的对象是一个结果集,条目有10万条,缓存时间基础为 60602(sec),现在需要同时进行缓存。我的做法是默认生成一个随机数,如random(范围 0 - 1000),过期时间则设置为( 60602 + random ) 。
  • 做二级缓存,或者双缓存策略。

4. 缓存搭建

我们的web项目中采用的是哨兵。一主两从三哨兵。


图片.png

5. 实际运用

Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持。Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。因此项目中选择了jedis作为java客户端。依赖如下:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>${redis.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>${spring.redis.version}</version>
</dependency>

redis相关操作工具类

@Component
public class RedisUtil {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private HashOperations<String, String, String> hashOps;

    private ListOperations<String, String> listOps;

    // private SetOperations<String, String> setOps;

    private ValueOperations<String, String> valueOps;

    private ZSetOperations<String, String> zsetOps;


    @PostConstruct
    public void setUp() {
        hashOps = stringRedisTemplate.opsForHash();
        listOps = stringRedisTemplate.opsForList();
        // setOps = stringRedisTemplate.opsForSet();
        valueOps = stringRedisTemplate.opsForValue();
        zsetOps = stringRedisTemplate.opsForZSet();
    }

    /**
     * key是否存在
     * 
     * @param key
     * @return
     */
    public boolean hasKey(String key) {
        return stringRedisTemplate.hasKey(key);
    };

    /**
     * 
     * @param key
     * @param hashKey
     * @return
     */
    public boolean hasHashKey(String key, String hashKey) {
        return hashOps.hasKey(key, hashKey);
    };

    /**
     * 删除key
     * 
     * @param keys
     */
    public void deleteKey(String... keys) {
        stringRedisTemplate.delete(Arrays.asList(keys));
    }

    /**
     * 通过key删除数据
     * 
     * @param key
     */
    public void deleteByKey(String key) {
        stringRedisTemplate.delete(key);
    }

    /**
     * key设置过期时间
     * 
     * @param key
     * @param timeout
     * @param timeUnit
     */
    public void expire(String key, Long timeout, TimeUnit timeUnit) {
        stringRedisTemplate.expire(key, timeout, timeUnit);
    }

    /**
     * 类似keys *
     * 
     * @param pattern
     * @return
     */
    public Collection<String> getKeys(String pattern) {
        return stringRedisTemplate.keys(pattern);
    }

    public static boolean isEnableRedisCache = true;

    /**
     * 是否已缓存key
     * 
     * @param key
     * @return
     */
    public boolean isCached(String key) {
        return isEnableRedisCache && hasKey(key);
    };

    public boolean isCachedHashKey(String key, String hashKey) {
        return isEnableRedisCache && hasHashKey(key, hashKey);
    };

    public final static int EXRP_MINUTE = 60; // 一分钟
    public final static int EXRP_HOUR = 60 * 60; // 一小时
    public final static int EXRP_DAY = 60 * 60 * 24; // 一天
    public final static int EXRP_MONTH = 60 * 60 * 24 * 30; // 一个月
    public final static int EXPR_YEAR = 12 * 60 * 60 * 24 * 30;// 一年

    // 缓存有效期,单位秒
    public static Map<String, Integer> expireMap = new HashMap<String, Integer>();
    static {
        // key过期设置
        expireMap.put("User", 30 * EXRP_MINUTE);// 30分钟过期

    }

    /**
     * 缓存单条记录
     * 
     * @param <T>
     * @param id
     * @param o
     */
    public void setSingleObjectInCache(String key, Object obj) {
        if (!isEnableRedisCache) {
            return;
        }

        String singleJson = FastJosnUtils.toJson(obj);
        if (expireMap.containsKey(obj.getClass().getSimpleName())) {
            valueOps.set(key, singleJson,
                    expireMap.get(obj.getClass().getSimpleName()),
                    TimeUnit.SECONDS);
        } else {
            valueOps.set(key, singleJson, expireMap.get("default"),
                    TimeUnit.SECONDS);
        }
    }

    /**
     * 缓存单条记录,并设置过期时间
     * 
     * @param key
     * @param obj
     * @param time
     * @param unit
     */
    public void setSingleObjectInCache(String key, Object obj, Integer time,
            TimeUnit unit) {
        if (!isEnableRedisCache) {
            return;
        }

        String singleJson = FastJosnUtils.toJson(obj);
        valueOps.set(key, singleJson, time, unit);
    }

    /**
     * 根据key获取
     * 
     * @param key
     * @param clazz
     * @return
     */
    public <T> T getByKeyFromCache(String key,Class<T> clazz) {
        if (!isEnableRedisCache) {
            return null;
        }

        String jsonStr = valueOps.get(key);
        return jsonStr == null ? null : (T)  FastJosnUtils.toObject(jsonStr, clazz);
    }

    /**
     * key的剩余有效期
     * 
     * @param key
     * @return
     */
    public long ttl(String key) {
        return valueOps.getOperations().getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 缓存一个集合
     * 
     * @param key
     *            键
     * @param collecation
     *            要缓存的集合
     * @param elementClass
     *            集合中类的类型
     */
    public <T> void setCollectionInCache(String key, Collection<T> collecation,
            Class<?> elementClass) {
        if (!isEnableRedisCache) {
            return;
        }

        // 无论数据是否为空,都推一个key到redis,防止数据为null时不走缓存
        List<String> jsonList = new ArrayList<String>();
        jsonList.add("");

        List<T> list = (List<T>) collecation;
        for (int i = list.size() - 1; i >= 0; i--) {
            jsonList.add(FastJosnUtils.toJson(list.get(i)));
        }

        listOps.getOperations().delete(key);
        listOps.leftPushAll(key, jsonList);

        if (expireMap.containsKey(elementClass.getSimpleName())) {
            listOps.getOperations().expire(key,
                    expireMap.get(elementClass.getSimpleName()),
                    TimeUnit.SECONDS);
        }
    }

    /**
     * 从缓存中取出列表,支持分页
     * 
     * @param <E>
     * @param <E>
     * @param id
     * @param obj
     * @return 若没有启用缓存或缓存中没有相应的key,返回null 若缓存中存在key,但value为null,返回空集合对象 example
     *         : new ArrayList()
     */
    @SuppressWarnings("unchecked")
    public <T extends Collection<E>, E> T getCollectionInCache(String key,
            Class<T> collectionClass, Class<E> elementClass, int offset,
            int size) {
        if (!isEnableRedisCache) {
            return null;
        }

        if (!hasKey(key)) {
            return null;
        }

        T conllection = (T) new ArrayList<E>();

        List<String> collecationJson = listOps
                .range(key, offset, offset + size);
        if (collecationJson == null || collecationJson.isEmpty()) {
            return conllection;
        }

        for (int i = 0; i < collecationJson.size() - 1; i++) {
            conllection.add((E) FastJosnUtils.toJson(collecationJson.get(i)));
        }

        return conllection;
    }

    /**
     * 缓存一个Map
     * 
     * @param <K>
     * @param key
     *            键
     * @param collecation
     *            要缓存的集合
     * @param elementClass
     *            集合中类的类型
     */
    public <K, V> void setMapInCache(String key, Map<K, V> map,
            Class<?> elementClass) {
        if (!isEnableRedisCache) {
            return;
        }

        Map<String, String> jsonMap = new ConcurrentHashMap<String, String>();
        for (Map.Entry<K, V> entry : map.entrySet()) {
            jsonMap.put((String) entry.getKey(), FastJosnUtils.toJson(entry.getValue()));
        }

        hashOps.getOperations().delete(key);
        hashOps.putAll(key, jsonMap);
    }

    /**
     * 添加元素到有序集合
     * 
     * @param key
     * @param obj
     * @param score
     */
    public void zSet(String key, Object obj, double score) {
        if (!isEnableRedisCache) {
            return;
        }

        zsetOps.add(key, FastJosnUtils.toJson(obj),score);
    }

    /**
     * 获取有序集合中指定key-value的分数
     * @param key
     * @param obj
     * @return
     */
    public Double  getScore(String key,Object obj) {
        return zsetOps.score(key, obj);
    }

    /**
     * 从有序集合中删除元素
     * @param key
     * @param values
     * @return
     */
    public Long remove(String key, Object... values){
        return null;
    }

}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容