SpringBoot Redis
两种应用开发方式:SpringBoot cache提供的注解和Spring data提供的RedisTemplate。
RedisTemplate的用法
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
配置
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.lettuce.pool.max-active=10
spring.redis.lettuce.pool.min-idle=2
- 注意lettuce和jedis的区别:文档1
户端中大量api进行了归类封装,将同一类型操作封装为operation接口
ValueOperations:简单K-V操作,在具体的应用中使用较多,如果需要存储对象,则通过使用jackson/ali的fastjon工具将对象转换为 json字符串进行存储
SetOperations:set类型数据操作
ZSetOperations:zset类型数据操作
HashOperations:针对map类型的数据操作,实际上官方推荐用于操作对象
ListOperations:针对list类型的数据操作
@SpringBootTest
class Redis03ApplicationTests {
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Test
void contextLoads() {
//基本操作
redisTemplate.opsForValue().set("num","123");
//存储数据,并设置timeout时长,存活周期为100小时,超时回收策略为 定期+惰性
redisTemplate.opsForValue().set("num1","zhangsan",100, TimeUnit.HOURS);
redisTemplate.opsForValue().set("num2","lisi", Duration.ofHours(100));
String res=redisTemplate.opsForValue().get("num");
System.out.println(res);
//如果num1不存在则存储num1值为cccc,如果num1已经存在,则不会执行操作【默认替换,例如set方法】。操作失败返回false
boolean bb=redisTemplate.opsForValue().setIfAbsent("num1","cccc");
System.out.println(bb);// 因为num1已经存在所以返回false,并不执行任何操作
res=redisTemplate.opsForValue().get("num1");
System.out.println("num1:"+res);
//如果存在则执行替换,否则不指定任何操作
bb=redisTemplate.opsForValue().setIfPresent("num1","ddddd");
System.out.println(bb);
res=redisTemplate.opsForValue().get("num1");
System.out.println("num1:"+res);
//获取旧有数据,同时替换新数据。如果没有旧有数据则返回null---CAS模型
res=redisTemplate.opsForValue().getAndSet("num11","999999");
System.out.println("old value:"+res);
res=redisTemplate.opsForValue().get("num11");
System.out.println("new Value:"+res);
//如果存储的旧有数据为整型,则执行加1操作.如果没有旧有数据,则相当于旧有数据为0
//如果所存储的旧有数据不能转换为整型,则RedisSystemException
redisTemplate.opsForValue().increment("num11");
res=redisTemplate.opsForValue().get("num11");
System.out.println("increment Value:"+res);
//decrement执行减法操作
}
}
使用ValueOptions存储对象
自定义类
@Data
public class User implements Serializable {
private Long id;
private String username;
private String password;
}
具体操作
@Autowired
private ObjectMapper objectMapper;
@Test
void testObject()throws Exception{
User user=new User(99L,"huangmao","666666");
//调用ObjectMapper可以将对象转换为JSON字符串
redisTemplate.opsForValue().set("users::1", objectMapper.writeValueAsString(user));
//按照key查询数据,查询完成后可以通过ObjectMapper将json字符串转换为指定类型的对象
String res=redisTemplate.opsForValue().get("users::1");
User utmp=objectMapper.readValue(res,User.class);
System.out.println(utmp);
}
redisTemplate中的方法
虽然提供了一些基本的操作方法,但是增删数据还得依赖具体的Operation,所以一般不直接使用。一般开发中引入工具类,自行封装对应的操作
@Component
public class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
//判断是否存在key,参数key键,返回true 存在 false不存在
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
//删除缓存,参数key可以传一个值
public void del(String key) {
if (key != null) {
redisTemplate.delete(key);
}
}
//从redis中获取值,其中key键
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
//普通缓存放入,key键,value值,返回值true成功 false失败
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
//普通缓存放入并设置时间
//time是生存周期,单位为s
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
//递增,delta要增加几(大于0)
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
//递减,delta 要减少几(大于0)
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
}
添加配置类,主要用于提供RedisTemplate<String,Object>
RedisTemplate默认使用的序列化机制是JdkSerializationRedisSerializer,但实际开发中,往往会以json的形式来保存数据。那么可以通过两种方式来实现这种要求,第一就是将保存的对象通过一些方法先转换成JsonString的形式,然后再通过redis保存;第二种方式,就是系统直接提高支持的Jackson2JsonRedisSerializer。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
//使用的序列化器组件为jackson2提供,系统默认是
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
//配置所需要识别的属性
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//不识别final属性
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
测试
@SpringBootTest
public class MyTests {
@Autowired
private RedisUtil redisUtil;
@Test
void testCreateObject(){
User user=new User(199L,"刘可可","666666");
boolean bb=redisUtil.set("users::"+user.getId(),user);
System.out.println(bb);
}
@Test
void testLoadObject(){
User user=(User) redisUtil.get("users::199");
System.out.println(user);
}
}
分布式锁的实现
在业务开发中,为了保证在多线程下处理共享数据的安全性,需要保证同一时刻只有一个线程能处理共享数据。
分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。
为了确保分布式锁可用,少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
- 加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。当且仅当 key 不存在,将 key 的值设为 value。若给定的 key 已经存在,则 SETNX 不做任何动作。
使用SETNX完成同步锁的流程及事项如下:
- 127.0.0.1:6379> set key value [EX seconds] [PX milliseconds] [NX|XX]
- 127.0.0.1:6379> setnx key value
- 127.0.0.1:6379> expire key seconds
- 使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功
- 为了防止获取锁后程序出现异常,导致其他线程/进程调用SETNX命令总是返回0而进入死锁状态,需要为该key设置一个“合理”的过期时间
- 释放锁,使用DEL命令将锁数据删除
Redis 官方站提出了一种权威的基于Redis实现分布式锁的方式名叫Redlock,此种方式比原先的单节点的方法更安全。它可以保证以下特性:
- 安全特性:互斥访问,即永远只有一个 client 能拿到锁
- 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
- 容错性:只要大部分 Redis 节点存活就可以正常提供服务
代码参考实现
@Component
@Slf4j
public class RedisLock {
@Autowired
StringRedisTemplate redisTemplate;
//加锁,value为当前时间 + 超时时间
public boolean lock(String key, String value){
if (redisTemplate.opsForValue().setIfAbsent(key, value)){
return true;
}
//解决死锁,且当多个线程同时来时,只会让一个线程拿到锁
String currentValue = redisTemplate.opsForValue().get(key);
//如果过期
if (!StringUtils.isEmpty(currentValue) &&
Long.parseLong(currentValue) < System.currentTimeMillis()){
//获取上一个锁的时间
String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
if (StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)){
return true;
}
}
return false;
}
//解锁
public void unlock(String key, String value){
try {
String currentValue = redisTemplate.opsForValue().get(key);
if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)){
redisTemplate.opsForValue().getOperations().delete(key);
}
}catch (Exception e){
log.error("【redis锁】解锁失败, {}", e);
}
}
}
#####数据库和缓存的双写处理
一致性
一致性就是数据保持一致,在分布式系统中,可以理解为多个节点中数据的值是一致的。
- 强一致性:这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大
- 弱一致性:这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态
- 最终一致性:最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型
三个经典的缓存模式
缓存可以提升性能、缓解数据库压力,但是使用缓存也会导致数据不一致性的问题。一般如何使用缓存呢?有三种经典的缓存使用模式:
- Cache-Aside Pattern
- Read-Through/Write-through
- Write-behind
Cache-Aside Pattern即旁路缓存模式,它的提出是为了尽可能地解决缓存与数据库的数据不一致问题。
Read/Write-Through模式中,服务端把缓存作为主要数据存储。应用程序跟数据库缓存交互,都是通过抽象缓存层完成的。
Write-behind 跟Read-Through/Write-Through有相似的地方,都是由Cache Provider来负责缓存和数据库的读写。它们又有个很大的不同:Read/Write-Through是同步更新缓存和数据的,Write-Behind则是只更新缓存,不直接更新数据库,通过批量异步的方式来更新数据库。
可以采用延时双删的方式保证一致性
##分布式session管理
spring-session
本文介绍Spring Boot结合Redis的开发方式,包括依赖配置、常用操作、对象存储、分布式锁实现及缓存一致性处理。
1031

被折叠的 条评论
为什么被折叠?



