- Jedis 基本使用
- Jedis 连接 Redis 服务器
- Jedis 使用 pipeline
- Jedis 发布订阅
- Jedis 使用 bitmap
- Jedis 使用 HyperLogLog
- Jedis 使用 GEO
- Jedis 使用哨兵
- Jedis 使用 Redis 连接池
- 实战应用 - 使用 Redis 缓存的用户登陆
- 缓存常见问题
- 缓存穿透
- 雪崩
- 热点 key
Jedis 基本使用Java 从 0 到架构师目录:【Java从0到架构师】学习记录
参考:Redis 笔记之 Java 操作 Redis(Jedis)
引入依赖:
Jedis 连接 Redis 服务器redis.clients jedis 3.1.0
public class BasicDemo {
private Jedis jedis = null; //客户端连接对象
@Before
public void init() throws Exception {
jedis = new Jedis("192.168.52.128", 6379);
}
@After
public void close() throws Exception {
if (jedis != null) {
jedis.close();
}
}
@Test
public void test1() throws Exception {
String val = jedis.set("hello", "java");
System.out.println("val = " + val); // val = OK
}
}
Jedis 使用 pipeline
public class PipeLineDemo {
private Jedis jedis = null; //客户端连接对象
@Before
public void init() throws Exception {
jedis = new Jedis("192.168.52.128", 6379);
}
@After
public void close() throws Exception {
if (jedis != null) {
jedis.close();
}
}
// 模拟发送写入10w个数据
@Test
public void testBasic() throws Exception {
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
jedis.set("base:key" + i, "value");
}
long end = System.currentTimeMillis();
// 10w->51s
System.out.println("耗时时长: " + (end - start));
}
// 模拟发送写入10w个数据 pipeline
@Test
public void testPipeline() throws Exception {
long start = System.currentTimeMillis();
// 创建一个管道
Pipeline pipelined = jedis.pipelined();
for (int i = 0; i < 100000; i++) {
// 把需要执行的命令放管道中
pipelined.set("pipe2:key" + i, "value");
}
// 同步执行命令
pipelined.sync();
long end = System.currentTimeMillis();
// 10w -> 0.9s
// 100w -> 6.2s
System.out.println("耗时时长: " + (end - start));
}
}
Jedis 发布订阅
PublisherDemo.java:
public class PublisherDemo {
// 客户端连接对象
private Jedis jedis = null;
@Before
public void init() throws Exception {
jedis = new Jedis("192.168.52.128", 6379);
}
@After
public void close() throws Exception {
if (jedis != null) {
jedis.close();
}
}
// 生成消息
@Test
public void produce() throws Exception {
for (int i = 0; i < 100; i++) {
jedis.publish("9527", "hello" + i);
TimeUnit.MICROSECONDS.sleep(10);
}
}
}
Subscriber.java:
public class Subscriber {
// 客户端连接对象
private Jedis jedis = null;
@Before
public void init() throws Exception {
jedis = new Jedis("192.168.52.128", 6379);
}
@After
public void close() throws Exception {
if (jedis != null) {
jedis.close();
}
}
// 订阅channel 消费消息
@Test
public void produce() throws Exception {
JedisPubSub jedisPubSub = new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
System.out.println("channel = " + channel);//哪个频道
System.out.println("message = " + message);// 消息内容
}
};
jedis.subscribe(jedisPubSub, "9527");
}
}
Jedis 使用 bitmap
// 记录每天用户的登录数量
public class UserLoginDemo {
private Jedis jedis = null; //客户端连接对象
@Before
public void init() throws Exception {
jedis = new Jedis("192.168.52.128", 6379);
}
@After
public void close() throws Exception {
if (jedis != null) {
jedis.close();
}
}
@Test
public void initData() throws Exception {
// 初始化用户登录数据
jedis.setbit("user:login:20191206", 1, "1");
jedis.setbit("user:login:20191206", 3, "1");
jedis.setbit("user:login:20191206", 5, "1");
jedis.setbit("user:login:20191206", 5, "1");
jedis.setbit("user:login:20191206", 8, "1");
jedis.setbit("user:login:20191207", 11, "1");
jedis.setbit("user:login:20191207", 3, "1");
jedis.setbit("user:login:20191207", 5, "1");
jedis.setbit("user:login:20191207", 18, "1");
jedis.setbit("user:login:20191208", 21, "1");
jedis.setbit("user:login:20191208", 13, "1");
jedis.setbit("user:login:20191208", 5, "1");
jedis.setbit("user:login:20191208", 28, "1");
}
@Test
public void resultData() throws Exception {
// 1 20191206 有多少用户登录
System.out.println(jedis.bitcount("user:login:20191206")); // 4
// 2 最近三天 20191206 20191207 20191208 有多少用户登录
jedis.bitop(BitOP.OR, "user:login:last31",
"user:login:20191206", "user:login:20191207", "user:login:20191208");
System.out.println(jedis.bitcount("user:login:last31")); // 9
// 3 统计连续登录三天的用户
jedis.bitop(BitOP.AND, "user:login:last32",
"user:login:20191206", "user:login:20191207", "user:login:20191208");
System.out.println(jedis.bitcount("user:login:last32")); // 1 --> 5
System.out.println("=========");
System.out.println(jedis.bitpos("user:login:last32", true));
}
// 比较一下用set 和用bitmap直接的一个存储量的差别
@Test
public void testSet() throws Exception {
Pipeline pipelined = jedis.pipelined();
for (int i = 0; i < 1000000; i++) {
pipelined.sadd("set:user:login:20191206", "user:" + i);
}
pipelined.sync();
// 11888895 b
}
// 比较一下用set 和用bitmap直接的一个存储量的差别
@Test
public void testBitMap() throws Exception {
Pipeline pipelined = jedis.pipelined();
for (int i = 0; i < 10000000; i++) {
pipelined.setbit("bit:user:login:20191207", i, true);
}
pipelined.sync();
// 100w --> 1436 b
// 1000w --> 14219 b
}
}
Jedis 使用 HyperLogLog
public class HyperLogLogDemo {
// 客户端连接对象
private Jedis jedis = null;
@Before
public void init() throws Exception {
jedis = new Jedis("192.168.52.128", 6379);
}
@After
public void close() throws Exception {
if (jedis != null) {
jedis.close();
}
}
@Test
public void initData() throws Exception {
// 初始化用户登录数据
jedis.pfadd("pf:login:20191206", "1");
jedis.pfadd("pf:login:20191206", "3");
jedis.pfadd("pf:login:20191206", "5");
jedis.pfadd("pf:login:20191206", "5");
jedis.pfadd("pf:login:20191206", "8");
}
@Test
public void testLog() throws Exception {
Pipeline pipelined = jedis.pipelined();
for (int i = 0; i < 10000000; i++) {
pipelined.pfadd("pf:user:login:20191207", "" + i);
}
pipelined.sync();
System.out.println(jedis.pfcount("pf:user:login:20191207"));
}
}
Jedis 使用 GEO
public class GeaDemo {
// 客户端连接对象
private Jedis jedis = null;
@Before
public void init() throws Exception {
jedis = new Jedis("192.168.52.128", 6379);
}
@After
public void close() throws Exception {
if (jedis != null) {
jedis.close();
}
}
@Test
public void initData() throws Exception {
HotelPosition h1 = new HotelPosition(113.23121, 23.117933, "如家酒店");
HotelPosition h2 = new HotelPosition(113.203641, 23.382214, "速8酒店");
HotelPosition h3 = new HotelPosition(113.361532, 23.128617, "7天连锁酒店");
HotelPosition h4 = new HotelPosition(113.258358, 23.162526, "广州曼克顿酒店");
jedis.geoadd("hotel", h1.getLng(), h1.getLat(), h1.getName());
jedis.geoadd("hotel", h2.getLng(), h2.getLat(), h2.getName());
jedis.geoadd("hotel", h3.getLng(), h3.getLat(), h3.getName());
jedis.geoadd("hotel", h4.getLng(), h4.getLat(), h4.getName());
}
@Test
public void handler() throws Exception {
// 离当前位置10km之内的酒店
// 当前的位置 113.324981,23.150597
List hotels = jedis.georadius("hotel", 113.324981, 23.150597, 10, GeoUnit.KM);
for (GeoRadiusResponse hotel : hotels) {
System.out.println(new String(hotel.getMember()));
}
//查看距离信息
System.out.println(jedis.geodist("hotel", "如家酒店", "7天连锁酒店", GeoUnit.KM));
}
}
Jedis 使用哨兵
public class SentinelDemo {
private JedisSentinelPool pool = null;
@Before
public void init() throws Exception {
// redis服务集群的名字
String masterName = "mymaster";
// 哨兵集群的名字
Set sentinels = new HashSet<>();
pool = new JedisSentinelPool(masterName, sentinels);
}
// 批量插入500w个数据
@Test
public void test1() throws Exception {
// 获取到连接客户端
Jedis jedis = pool.getResource();
Pipeline pipelined = jedis.pipelined();
for (int i = 0; i < 5000000; i++) {
pipelined.set("key" + i, "value" + i);
}
pipelined.sync();
}
// 客户端自动连接重试
@Test
public void testfailOver() throws Exception {
//获取到连接客户端
Jedis jedis = null;
try {
while (true) {
jedis = pool.getResource();
jedis.incr("count");
System.out.println("count=" + jedis.get("count"));
TimeUnit.SECONDS.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
Jedis 使用 Redis 连接池
public class JedisPoolDemo {
@Test
public void testPool() throws Exception {
// 连接池参数配置
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
// 最多连接数
config.setMaxTotal(10);
// 最多空闲数
config.setMaxIdle(5);
// 最小空闲数
config.setMinIdle(5);
// 最多等待时间
config.setMaxWaitMillis(2000);
String host = "192.168.52.128";
int port = 6379;
// jedis连接池对象配置
JedisPool jedisPool = new JedisPool(config, host, port);
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.set("a", "1");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedis != null) {
// 不会真正关闭连接, 把连接对象归还到连接池
jedis.close();
}
}
}
}
实战应用 - 使用 Redis 缓存的用户登陆
参考:SpringBoot整合Redis 之 StringRedisTemplate、RedisTemplate 基础
整体思路:
- 用户根据用户名和密码进行登录
- 优先在 redis 缓存中根据用户名查询用户信息
- 如果 redis 缓存中没有对应的用户信息,再从数据库查询用户信息,返回对应的数据,保存到 redis 缓存中
- 对于缓存到 redis 中的用户信息设置一个有效时间(7天)
引入 redis 依赖和连接池:
org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2
在配置文件中配置 redis 集群信息:
redis:
cluster:
nodes: 192.168.52.129:7000,192.168.52.129:7001,192.168.52.129:7002,192.168.52.129:7003,192.168.52.129:7004,192.168.52.129:7005
lettuce:
pool:
max-wait: 15000
max-idle: 10
max-active: 10
min-idle: 10
根据用户名查询的业务代码:
@Service
public class UserServiceImpl implements IUserService {
@Autowired
private UserMapper userMapper;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public User queryByName(String username) {
System.out.println("从数据库查询结果:" + username);
return userMapper.queryByName(username);
}
@Override
public User queryByNameByCache(String username) throws Exception {
System.out.println("从redis缓存中查询查询结果:" + username);
ValueOperations ops = redisTemplate.opsForValue();
// 直接从本地内存(比较短的时间1min)查询, 对于redis的压力会小
String value = ops.get("user:" + username);
ObjectMapper objectMapper = new ObjectMapper();
User user = null;
// 判断redis的返回结果
if (StringUtils.isEmpty(value)) {
// 查询数据库
user = queryByName(username);
if (user != null) { // 添加到缓存中
ops.set("user:" + username, objectMapper.writevalueAsString(user), 7, TimeUnit.DAYS);
} else {
ops.set("user:" + username, objectMapper.writevalueAsString(new User()), 1, TimeUnit.MINUTES);
}
} else {
user = objectMapper.readValue(value, User.class);
}
return user;
}
}
缓存常见问题
缓存穿透
缓存穿透:查询一个一定不存在的数据,由于缓存在不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次都需要到数据库中去查询,造成缓存穿透
解决方案:
- 单个 key 穿透:持久层查询不到就缓存空节点,查询时先判断缓存中是否 exists(key),如果有就直接返回空,没有则查询后返回(对于空节点设置一个很小的缓存过期时间)
- 多个 key 穿透:先判断 key 是否是一个存在的 key,如果 key 合法则查询数据库;如果 key 不合法,则不查询数据库
数据量不大,可以把 key 都存放到 redis 的 set 集合中;数据量非常大(上亿级别),可以考虑使用布隆过滤器
如果缓存集中在一个时间段内失效,发生大量的缓存穿透,所有的查询都落到了数据库上,造成数据库服务器雪崩
解决方案:
- 尽量让 key 的过期时间均匀分布
- 控制同一个 key 的线程数量(读取数据库的线程数量)–> 限流
常见的限流算法有:计数算法、滑动窗口、令牌桶、漏桶
某个 key 的访问频率非常频繁,导致 Redis 服务器的压力剧增,无法保证可以正常提供服务
比如说鹿晗和关晓彤的"官宣"直接导致微博的服务瘫痪
由于是 Redis 服务器的压力导致瘫痪,所以主要解决方案集中在:提高 Redis 的 qps、控制对 Redis 的访问
解决方案:
- 扩容,添加集群中的从服务器,提高读数据的能力
- 考虑使用本地缓存,把 redis 中的热点数据直接缓存在本地的服务器的内存中,减少对 redis 的访问



