1 面试常见问题
1.1 击穿
-
服务常见架构
- 通常请求需要查询数据,先尝试从redis中查询,如果能取到直接返回
- 如果取不到,改为从数据库中取,取到数据后更新缓存并返回结果
- 如果数据库中也取不到,直接返回空结果
-
redis作为缓存时,其key可能会由于过期、lru、lfu算法而被清理。高并发地访问恰好被清理掉的某个key对应的数据,就会导致高并发访问数据库,这就是缓存的击穿
-
解决缓存击穿的客户端代码
- get k1:先尝试从缓存中查找k1
- setnx k1:如果缓存中没有k1,设置k1的值,且该命令会为k1上锁,且redis是单进程单实例,因此一定会有一个客户端先获取到k1的锁
- 上锁成功:访问数据库,将结果更新缓存
- 上锁失败:后续的客户端会上锁失败,上锁失败后sleep一段时间,再重复整个流程。先get k1,此时能从缓存获取到k1,就不会再访问数据库
-
如果第一个进程在获取到锁后,挂掉,会导致锁永远无法释放,因此可以为锁设置过期时间
-
假设过期时间1s,如果确实第一个客户端获取锁后,访问后段数据库需要10s,此时锁被自动释放,后续的客户端进程再次击穿缓存,访问到后端数据库。因此可以考虑为获取到锁的客户端启动一个守护进程,守护进程去监控客户端状态,如果发现客户端并没有死,只是访问数据库时间较长,可以延长锁的过期时间
1.2 穿透
- 可以使用布隆过滤器
- client包含布隆算法和数据
- 客户端持有算法,redis存放数据
- redis集成布隆(redis bloom)
- 布隆过滤器无法删除
- 可以使用布谷鸟这种可以删除的过滤器替代
- 从数据库删除后,在redis中存放一个value为空的key,客户端发现该产品的value为空,就知道根本没有该产品信息
1.3 雪崩
- 大量的key同时失效,从而间接导致高并发到达数据库,称为雪崩
- 解决方案
- key必须在某时间点过期(例如0点必须过期)
- 参考击穿的解决方案
- 业务层增加判断,当0点时,服务器上进行延时,比如先sleep一会再继续访问
- key的过期与时点无关:将过期时间设置为随机
- key必须在某时间点过期(例如0点必须过期)
1.4 分布式锁
-
同一个客户端用户,同时发出了多个请求,分布式环境下,多台机器上部署的多个service进行了并发操作,导致插入了冗余数据
-
修改逻辑为service需要先去抢锁,抢到锁的service,才去数据库操作,这个锁就是分布式锁
-
分布式锁的实现
- Redis
- Zookeeper
- Memcached
- Chubby
-
redis实现分布式锁
//1. 加锁 //a. 不使用setnx(lock_sale_商品ID,1)+expire(lock_sale_商品ID, 30)是为了防止加锁后,还未设置过期时间这段时间内进程挂掉,导致的死锁 //b. 如果线程A执行了30s没执行完,但此时由于key到期,线程B进入,而此时A处理完成,del时,可能误将B的锁删除,因此需要将value设置为线程id,且当当前线程id和加锁时的线程id相同,才允许删除 //c. 但无法解决B可能会和A访问同一段代码的问题 //d. 可以为A开启一个守护线程,守护线程从A进行了29s后开始执行,之后每20s执行一次,如果发现A未处理完成,使用expire命令为A的锁延长20s,当A结束后,关闭其守护进程。如果节点1忽然断电,由于线程A和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了 String threadId = Thread.currentThread().getId() set(key,threadId ,30,NX) //2. 解锁 if(threadId .equals(redisClient.get(key))){ del(key) }
2 API
- spring官网–Projects–Spring Data–Spring Data Redis–LEARN–Reference Doc.
- java中可以使用lettuce、Jedis、Redisson等开源api访问redis,spring中也集成了它们,但现在默认使用lettuce对redis进行连接
- Jedis和lettuce的简单使用:可以参考各自github中的文档,都是需要先通过maven引入,然后输入ip和port来创建Jedis或lettuce实例,最后使用具体方法发送命令给redis
- Jedis线程不安全,多线程使用同一个Jedis实例会有问题,通常采用一个称为JedisPool的连接池,为每个线程分配一个连接,但这样会导致大量socket连接在服务端上,成本较高
- RedisConnectionFactory:用于建立与Redis的连接
- RedisTemplate:用于真正发送指令给redis,由RedisConnectionFactory创建,可以使用RedisTemplate这种高阶的API来操作redis,也可以通过RedisTemplate来获取一个RedisConnection对象,从而调用lettuce、Jedis、Redisson等API的原生方法(低阶别API)
- Serializers:由于Redis是二进制安全的,该类可以用于指定如何将数据转为二进制码,比如是直接通过java序列化转为二进制码,还是先转为一个jason格式的字符串,再转为二进制码。如果写和读使用的二进制转换的机制不同(例如写入使用java序列化,读取使用python的逻辑),可能会导致客户端无法正确解析读取到的二进制码
2.1 使用spring boot连接redis
-
File–New–Project–Spring Initializr–com.msb.spring.redis–NoSQL–Spring Data Redis(Access+Driver)
-
application.properties
spring.redis.host=172.128.246.128 spring.redis.port=6379
-
TestRedis
package com.msb.spring.redis.demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; @Component public class TestRedis { //1. spring boot会自动根据application.properties中配置的内容,创建RedisConnectionFactory,并通过RedisConnectionFactory创建RedisTemplate //2. 之后根据Autowired注释,将容器中的RedisTemplate对象自动注入到redisTemplate中 @Autowired RedisTemplate redisTemplate; public void testRedis(){ //3. 表示获取redis的string类型的命令 redisTemplate.opsForValue().set("hello","china"); System.out.println(redisTemplate.opsForValue().get("hello")); } }
-
DemoApplication
package com.msb.spring.redis.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { //1. 用于初始化spring容器 ConfigurableApplicationContext ctx = SpringApplication.run(DemoApplication.class, args); TestRedis redis = ctx.getBean(TestRedis.class); redis.testRedis(); } }
-
redis服务默认的安全策略是禁止远端访问,且绑定必须通过127.0.0.1访问,即telnet 192.168.246.128 6379不通。需要通过修改配置文件
protected-mode no
开启远端访问,#bind 127.0.0.1
取消ip绑定。也可以通过本地客户端连接后执行config set protected-mode no
命令临时开启远端访问。config get *
可以查看当前redis服务的所有配置参数 -
执行DemoApplication后,发现执行成功,正确打印china,但使用redis-cli直接连接redis服务时,发现存放的key和value并不是hello和china,而是一堆乱码。这是因为在使用redisTemplate将key和value转为二进制码时,默认使用的是java的序列化机制,也就是将一个String对象序列到了redis中,而不是将字符串本身直接转为二进制码
2.2 不使用java序列化
-
TestRedis
//StringRedisTemplate要求key和value都必须为string类型,同时会将String对象,当作字符串转为二进制码,这样存放到redis中的key和value就不再是乱码 @Autowired StringRedisTemplate stringRedisTemplate; public void testRedis(){ stringRedisTemplate.opsForValue().set("hello","china"); System.out.println(stringRedisTemplate.opsForValue().get("hello")); }
-
使用低阶API操作redis
//redis中存放的key和value也是正常的 RedisConnection connection = redisTemplate.getConnectionFactory().getConnection(); connection.set("hello02".getBytes(),"mashibing".getBytes()); System.out.println(new String(connection.get("hello02".getBytes())));
2.3 hash操作
-
操作hash
HashOperations<String, Object, Object> hash = stringRedisTemplate.opsForHash(); hash.put("sean","name","zhouzhilei"); //由于使用了stringRedisTemplate,因此无法使用12这个整型作为value,必须使用string类型作为value hash.put("sean","age","12"); //打印:{name=zhouzhilei, age=12} System.out.println(hash.entries("sean"));
-
使用Jackson2HashMapper将java对象转为Map类型,从而存放到redis的hash类型变量中
-
引入pom依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-json</artifactId> </dependency>
-
Person
package com.msb.spring.redis.demo; public class Person { private String name; private Integer age; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } }
-
TestRedis
Person p = new Person(); p.setName("zhangsan"); p.setAge(16); //1. 选择true时,会采用扁平化的方式转为jason //2. 不采用扁平化时,jason内容为 //{"firstname" : "Jon", "lastname" : "Snow", "address" : { "city" : "Castle Black", "country" : "The North" }, "date" : "1561543964015", "localDateTime" : "2018-01-02T12:13:14"} //3. 采用扁平化处理时,jason内容为 //{"firstname" : "Jon", "lastname" : "Snow", "address.city" : "Castle Black", "address.country" : "The North", "date" : "1561543964015", "localDateTime" : "2018-01-02T12:13:14"} //public class Person { // String firstname; // String lastname; // Address address; // Date date; // LocalDateTime localDateTime; //} // //public class Address { // String city; // String country; //} Person p = new Person(); p.setName("zhangsan"); p.setAge(16); Jackson2HashMapper jm = new Jackson2HashMapper(objectMapper, false); redisTemplate.opsForHash().putAll("sean01",jm.toHash(p)); Map map = redisTemplate.opsForHash().entries("sean01"); Person per = objectMapper.convertValue(map,Person.class); System.out.println(per.getName());
-
-
由于使用了RedisTemplate,所以会对Key和Value进行java序列化,导致存入redis的字节码只能用java程序读取。但如果改为使用StringRedisTemplate,由于Person中存在Integer类型的age,Person转为的Map中的value为Integer类型,会导致放入redis失败
-
可以为RedisTemplate或StringRedisTemplate设置hash中value的序列化器,StringRedisTemplate默认使用的就是StringRedisSerializer,因此可以将String对象以字符串的方式转为二进制码
-
MyTemplate
package com.msb.spring.redis.demo; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; @Configuration public class MyTemplate { //1. 之前spring管理了一个StringRedisTemplate类型的bean,id为stringRedisTemplate,现在加上一个id为ooxx的StringRedisTemplate类型的bean,下面StringRedisTemplate类型的变量使用新的bean进行注入 @Bean public StringRedisTemplate ooxx(RedisConnectionFactory fc){ StringRedisTemplate tp = new StringRedisTemplate(fc); //2. 可以分别为key、value、hash中的key、hash中的value设置序列化器,由于stringRedisTemplate已经规定了key、value、hash中的key都以字符串的形式转为二进制码,因此只需要对hash的value进行特殊设置即可 tp.setHashValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class)); return tp; } }
-
TestRedis
@Autowired @Qualifier("ooxx") StringRedisTemplate stringRedisTemplate; ... Person p = new Person(); p.setName("zhangsan"); p.setAge(16); Jackson2HashMapper jm = new Jackson2HashMapper(objectMapper, false); stringRedisTemplate.opsForHash().putAll("sean01",jm.toHash(p)); Map map = stringRedisTemplate.opsForHash().entries("sean01"); Person per = objectMapper.convertValue(map,Person.class); System.out.println(per.getName());
2.4 实现聊天室
RedisConnection cc = stringRedisTemplate.getConnectionFactory().getConnection();
// 订阅频道,此时使用redis-cli连接一个客户端,执行publish ooxx "xiaohong: Hello",订阅中就会收到该消息,收到后触发onMessage,就会打印消息
cc.subscribe(new MessageListener() {
@Override
public void onMessage(Message message, byte[] pattern) {
byte[] body = message.getBody();
System.out.println(new String(body));
}
}, "ooxx".getBytes());
while(true){
// 向频道发布消息
stringRedisTemplate.convertAndSend("ooxx","我: hello from wo zi ji ");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}