服务常见架构
redis作为缓存时,其key可能会由于过期、lru、lfu算法而被清理。高并发地访问恰好被清理掉的某个key对应的数据,就会导致高并发访问数据库,这就是缓存的击穿
解决缓存击穿的客户端代码
如果第一个进程在获取到锁后,挂掉,会导致锁永远无法释放,因此可以为锁设置过期时间
假设过期时间1s,如果确实第一个客户端获取锁后,访问后段数据库需要10s,此时锁被自动释放,后续的客户端进程再次击穿缓存,访问到后端数据库。因此可以考虑为获取到锁的客户端启动一个守护进程,守护进程去监控客户端状态,如果发现客户端并没有死,只是访问数据库时间较长,可以延长锁的过期时间
同一个客户端用户,同时发出了多个请求,分布式环境下,多台机器上部署的多个service进行了并发操作,导致插入了冗余数据
修改逻辑为service需要先去抢锁,抢到锁的service,才去数据库操作,这个锁就是分布式锁
分布式锁的实现
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)
}
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中,而不是将字符串本身直接转为二进制码
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())));
操作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.bootgroupId>
<artifactId>spring-boot-starter-jsonartifactId>
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());
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();
}
}