SpringBoot Redis

本文介绍Spring Boot结合Redis的开发方式,包括依赖配置、常用操作、对象存储、分布式锁实现及缓存一致性处理。

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的分布式锁。

为了确保分布式锁可用,少要确保锁的实现同时满足以下四个条件:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  4. 加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值