思路
每一个key都有一个附属key1,附属key1可以是key加特定前缀组成,key对应value为真正的缓存数据,附属key1对应的value不重要,可以是随便一个值,附属key1的作用主要是维护缓存更新时间并保证只有一个线程到数据源拉取数据更新缓存
附属key1的过期时间设置为缓存刷新时间,比如30s,key的过期时间设置 缓存刷新时间 + 数据源修复预期时间(比如2天)
每次请求数据时,使用setnx(将 key 的值设为 value ,当且仅当 key 不存在)设置附属key1,返回结果为1:设置成功,代表附属key1过期,需要刷新数据,从数据源获取数据更新缓存,若返回结果为0:设置失败,代表附属key1未过期,不需要刷新数据,从缓存key中获取数据
由于redis是单线程,setnx操作相当与互斥锁,在并发情况下只有一个线程能获取到锁,杜绝了大量并发击穿缓存请求到数据库的问题
流程图
代码演示
package com.liutf.util; import redis.clients.jedis.Jedis; /** * redis工具 **/ public class ReidsUtil { private static final String HOST = "192.168.11.23"; private static final int PORT = 6379; /** * 附属key前缀 */ private static final String PREFIX = "prefix:"; /** * 数据源修复预期时间 */ private static final int FIX_TIME = 2 * 26 * 60 * 60; /** * 缓存时间过期时PREKEY_TIME的缓存时间 */ private static final int PREKEY_TIME_COMMON = 30; private static Jedis jedis = null; static { jedis = new Jedis(HOST, PORT); } public static String get(String key) { /** * 组装设置附属key */ String prefixKey = PREFIX + key; Long setnxResult = jedis.setnx(prefixKey, "1"); /** * 附属key过期返回null,从数据源获取数据 * 附属key未过期,从key中获取数据 */ if (setnxResult == 1) { jedis.expire(prefixKey, PREKEY_TIME_COMMON); return null; } else { return jedis.get(key); } } public static boolean set(String key, String value) { /** * 组装设置附属key */ String prefixKey = PREFIX + key; jedis.setnx(prefixKey, "1"); jedis.expire(prefixKey, PREKEY_TIME_COMMON); jedis.set(key, value); jedis.expire(key, PREKEY_TIME_COMMON + FIX_TIME); return true; } }
public String get(key) { String value = redis.get(key); if (value == null) { //代表缓存值过期 //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功 value = db.get(key); redis.set(key, value, expire_secs); redis.del(key_mutex); } else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可 sleep(50); get(key); //重试 } } else { return value; }
缺点分析
- 每次请求数据,就需要先操作附属key,再设置附属key过期时间,请求量在原有的两倍多
- 并发情况下,附属key过期,抢到锁的线程从数据源获取数据,再更新缓存,其他未获取锁的线程获取老数据返回