SpringBoot 整合 Redis

1. 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

2. 配置

#redis单机配置
spring.data.redis.url=redis://127.0.0.1:6379
spring.data.redis.port=6379
#spring.redis.password=root

#redis高可用集群配置
#哨兵监控主redis节点名称,必选 与sentinei.conf的mymaster名称相同
spring.data.redis.sentinel.master=mymaster
#哨兵节点
spring.data.redis.sentinel.nodes=127.0.0.1:26380,127.0.0.1:26381,127.0.0.1:26382

#redis分布式配置
spring.data.redis.cluster.nodes=127.0.0.1:6380,127.0.0.1:6381,127.0.0.1:6382,127.0.0.1:6383,127.0.0.1:6384,127.0.0.1:6385
#缓存策略
spring.cache.type=redis
spring.cache.cache-names=myRedis

3. 实体类必须要序列化

@Entity
@Table(name = "development")
@Data
public class Development implements Serializable{

    @Id
    private Integer id;

    private String devName;

    private String devDesc;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)		// 反序列化
    @JsonSerialize(using = LocalDateTimeSerializer.class)		// 序列化
    private LocalDateTime createTime;
}

4.启用缓存注解@EnableCaching

@SpringBootApplication
@MapperScan("com.example.demo17.mapper")
@EnableCaching
public class Demo17Application {

    public static void main(String[] args) {
        SpringApplication.run(Demo17Application.class, args);
    }

}

5.使用缓存注解,value属性必须指定

    /**
     * 查询方法上要添加@Cacheable 注解 ,@Cacheable标记的方法在执行后Spring Cache将缓存其返回结果
     */
    @Override
    @Cacheable(value = "pc", key = "'product_'+#name")
    public List<Product> findProductsByName(String name) {
        return dao.selectProductsByName(name);
    }
    @Override
    @Cacheable(value = "pc", key = "'product_all'")
    public List<Product> findAllProducts() {
        return dao.selectAllProducts();
    }

    
    /**
     * 对数据进行写操作的方法上添加@CacheEvict 注解 ,用@CacheEvict标记的方法会在方法执行前或者执行后移除Spring Cache中的某些元素 
     */
    @Override
    @CacheEvict(value = "pc", allEntries = true)   // 只要该方法被执行,立即清除pc缓存中的所有数据
    public void saveProduct(Product product) {
        dao.insertProduct(product);
    }

6. 手工操作 Redis

@Api(value = "秒杀接口",tags = "采用分段锁1分钟内秒杀1000个商品")
@RestController
public class SingleRedisForSecKillController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedissonClient redissonClient;
    //锁的前缀
    private static String LOCK_KEY_PREFIX;
    //锁段数量
    private static int SEGMENT_NUM ;
    //锁数组
//    private static ArrayList<RLock> locks;

    @ApiOperation(value = "redis采用分段锁-1分钟内秒杀1000个商品-初始化")
    @PostMapping("/redisson/init/seckill")
    public void initSecKillBySegmentationLock(@RequestBody RequestMessage<GoodsSecKillRequest> requestMessage) {
        /**
         * 使用redisson可重入锁+分段式锁;解决高并发问题:每秒上千订单的高并发场景下如何完成分布式锁的性能优化
         * 1、这种方式适用于一套代码在多个服务器部署,但redis只能有一个,不然可能会出现锁丢失问题
         * 思想:分段式锁
         */
        GoodsSecKillRequest inDto = requestMessage.getRoot();
        int goodsSum = inDto.getGoodsSum();
        String goods = inDto.getGoods();
        //锁数量
        SEGMENT_NUM = (int) Math.ceil(goodsSum/50.0);
        //锁前缀
        LOCK_KEY_PREFIX = goods+"lock";
        //初始化商品
        for (int i = 0; i < SEGMENT_NUM; i++) {
            if (i < SEGMENT_NUM-1){
                stringRedisTemplate.opsForValue().set(goods+i,"50");
            }else {
                stringRedisTemplate.opsForValue().set(goods+i,String.valueOf(goodsSum-50*i));
            }
        }
    }

    @ApiOperation(value = "redis采用分段锁-1分钟内秒杀1000个商品")
    @PostMapping("/redisson/seckill")
    public String secKillBySegmentationLock(@RequestBody RequestMessage<GoodsSecKillRequest> requestMessage) {
        /**
         * 使用redisson可重入锁+分段式锁;解决高并发问题:每秒上千订单的高并发场景下如何完成分布式锁的性能优化
         * 1、这种方式适用于一套代码在多个服务器部署,但redis只能有一个,不然可能会出现锁丢失问题
         * 思想:分段式锁
         */
        GoodsSecKillRequest inDto = requestMessage.getRoot();
        String userId = inDto.getUserId();
        String goods = inDto.getGoods();
        //对订单id取模运算(0-19)
        int segment = Math.abs(userId.hashCode() % SEGMENT_NUM);
        //获取对应锁
        RLock redissonLock = redissonClient.getLock(LOCK_KEY_PREFIX+segment);
        try {
            //tryLock() 方法的作用是尝试获取锁,如果锁已经被其他线程持有,则立即返回
            //boolean tryLock = redissonLock.tryLock();
            //waitTime:等待获取锁的最大时间。如果在此时间内无法获取到锁,则返回失败
            //leaseTime :成功获取锁后的持有时间。在此时间内,锁将保持有效状态,超过该时间后锁将自动释放
//            boolean tryLock = redissonLock.tryLock(600, 100, TimeUnit.SECONDS);
            boolean tryLock = redissonLock.tryLock();
            if(tryLock){
                //方式一:opsForValue()
                String stock = stringRedisTemplate.opsForValue().get(goods+segment);
                int amount = stock == null ? 0 : Integer.parseInt(stock);
                if (amount > 0) {
                    //方式一:opsForValue()
                    stringRedisTemplate.opsForValue().set(goods+segment, String.valueOf(--amount));
                    return "库存剩余" + amount + "台";
                }
                return "库存不足";
            }else {
                return "没有抢到锁";
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //解锁
            redissonLock.unlock();
        }
    }

}

📢:RedisTemplate的script操作

tryLock(Lists.newArraryList("key"),"value","30");
boolean tryLock(List<String> keys,Object... value){
    String script = """
            if (redis.call('setnx', KEYS[1], ARGV[1]) < 1) then
                return 0;
            end;
            redis.call('expire', KEYS[1], tonumber(ARGV[2]));
            return 1;
            
            """;
    // 创建一个Lua脚本对象,script:脚本 Long.class返回值类型
    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script,Long.class);
    // 使用RedisTemplate执行Lua脚本
    Long execute = stringRedisTemplate.execute(redisScript, keys, value);
    if (execute > 0){
        // 启动守护线程
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
        scheduler.scheduleAtFixedRate(()->{
            if (Objects.equals(stringRedisTemplate.opsForValue().get(keys.get(0)), value[0])) {
                stringRedisTemplate.expire(keys.get(0), 10,TimeUnit.SECONDS);  // 续期锁
            }else {
                System.out.println(Thread.currentThread().getName() + "停止守护线程");
                scheduler.shutdown();  // 停止守护线程
            }
        },5,5,TimeUnit.SECONDS);
    }
    return execute > 0;
}

📢:RedisTemplate的BitMap操作

//为给定 key(users) 的BitMap 数据的 offset(100066) 位置设置值为 1
redisTemplate.opsForValue().setBit("users",100066,true);
//对 key 所储存的 BitMap 字符串值,获取指定 offset 偏移量上的位值 bitValue
System.out.println(redisTemplate.opsForValue().getBit("users", 100101));
System.out.println(redisTemplate.opsForValue().getBit("users", 100102));
//统计给定字符串中被设置为 1 的 bit 位的数量
Long execute = redisTemplate.execute((RedisCallback<Long>) res -> res.bitCount("users".getBytes(StandardCharsets.UTF_8)));
//返回 key 指定的 BitMap 中第一个值为指定值 bit(非 0 即 1) 的二进制位的位置
Long offset = redisTemplate.execute((RedisCallback<Long>) res -> res.bitPos("users".getBytes(StandardCharsets.UTF_8), true));
System.out.println(execute);
System.out.println(offset);
输出:
true
true
5
100001

RedisTemplate的批量操作

Pipeline是为了减少Redis命令多次网络传输而设计出来的客户端命令缓存功能,能够将多个Redis命令打包一次性发送给服务端,从而将多次命令的多次网络耗时压缩为1次。多条命令的响应也会打包一次性返回给客户端。

假设客户端和Redis集群位于同机房或者相同局域网,一次网络IO的耗时也在大概数百微秒到几十毫秒之间,而一次内存IO的耗时则在纳秒级别。因此Redis的性能瓶颈是网络IO而非内存IO。

如果客户端要发送10条Redis命令,在不使用Pipeline的情况下,总耗时=10次网络时间+10次命令执行时间;

在使用Pipeline的情况下,总耗时=1次网络时间+10次命令执行时间;

//批量新增
stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            StringRedisConnection stringRedisConnection = (StringRedisConnection) connection;
            redisCheckCacheDTOS.forEach(cacheKey -> stringRedisConnection.hSet(cacheKey.getKey(), cacheKey.getCacheKey(), cacheKey.getValue()));
            return null;
        });
//批量删除
stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            StringRedisConnection stringRedisConnection = (StringRedisConnection) connection;
            redisCheckCacheDTOS.forEach(cacheKey -> stringRedisConnection.hDel(cacheKey.getKey(), cacheKey.getCacheKey()));
            return null;
        });

定制化redisTemplate(可选)

import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.nio.charset.StandardCharsets;

@Configuration
public class RedisConfig {
    @Bean("redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory) {
        //创建模版
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        //设置连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 使用 FastJsonRedisSerializer 来序列化和反序列化redis 的 value的值
        FastJsonRedisSerializer<Object> serializer = new FastJsonRedisSerializer<>(Object.class);
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setCharset(StandardCharsets.UTF_8);
        serializer.setFastJsonConfig(fastJsonConfig);

        // key 的 String 序列化采用 StringRedisSerializer
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);

        // value 的值序列化采用 fastJsonRedisSerializer
        redisTemplate.setValueSerializer(serializer);
        redisTemplate.setHashValueSerializer(serializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

定制化缓存管理(可选)

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

    /*
     * 引入 redis 来实现缓存,此时我们便使用 RedisCacheManager 来管理缓存的生命周期、缓存的存储和检索等。
     * 我们在使用 RedisCacheManager 来操作 redis 时,底层操作默认使用的是 RedisTemplate,
     * 而 redisTemplate 是 redisAutoConfiguration 在项目启动时帮我们自动注册的组件,它默认使用的是 JDK 序列化机制。
     * 所以在 redis 存储时,会出现类似乱码的情况出现,所以我们需要来自己配置 redisCacheManager。
     * 注:尤其是当使用@Cacheable @CachePut @CacheEvict注解时redisCacheManager将其序列化成对应格式
     * */
@Configuration
public class CacheConfig {
    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        //这一句必须要,作用是序列化时将对象全类名一起保存下来
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        genericJackson2JsonRedisSerializer.serialize(om);
        //配置序列化(解决乱码的问题)
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                //设置 key 过期时间
                .entryTtl(Duration.ofSeconds(300))
                //设置key序列化规则
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                //设置value序列化规则
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer))
                .disableCachingNullValues();

        RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

定制化 LettuceConnectionFactory(可选)

    @Bean
    public LettuceConnectionFactory lettuceConnectionFactory() {
 
        RedisConfiguration redisConfiguration = new RedisStandaloneConfiguration(
                hostName, port
        );
        // 设置选用的数据库号码
        ((RedisStandaloneConfiguration) redisConfiguration).setDatabase(databaseId);
        // 设置 redis 数据库密码
        ((RedisStandaloneConfiguration) redisConfiguration).setPassword(password);
        // 连接池配置
        GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>();
        poolConfig.setMaxIdle(maxIdle);
        poolConfig.setMinIdle(minIdle);
        poolConfig.setMaxTotal(maxActive);
        poolConfig.setMaxWaitMillis(maxWaitMillis);
        LettucePoolingClientConfiguration lettucePoolingClientConfiguration
                = LettucePoolingClientConfiguration.builder()
                .poolConfig(poolConfig)
                .commandTimeout(Duration.ofMillis(timeout))
                .build();
        // 根据配置和客户端配置创建连接
        LettuceConnectionFactory factory = new LettuceConnectionFactory(redisConfiguration, lettucePoolingClientConfiguration);
        return factory;
    }

附录:四种缓存注解

(1) @Cacheable

@Cacheable可以标记在一个方法上,也可以标记在一个类上。

  • 当标记在一个方法上时表示该方法是支持缓存的

  • 当标记在一个类上时则表示该类所有的方法都是支持缓存的

作用:对于一个支持缓存的方法,Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法。 注意:当一个支持缓存的方法在对象内部被调用时是不会触发缓存功能的

用法:@Cacheable可以指定三个属性,value、key和condition

value属性指定Cache名称:value属性是必须指定的,其表示当前方法的返回值是会被缓存在哪个Cache上的(即缓存在那个分组中,若redis中不存在对应的分组就创建出该分组并将缓存数据保存其中),对应Cache的名称。其可以是一个Cache也可以是多个Cache,当需要指定多个Cache时其是一个数组。

//Cache是发生在cache1上的
@Cacheable(value = "cache1")
public User find(Integer id) {
    return null;
}

//Cache是发生在cache1和cache2上的
@Cacheable(value = {"cache1", "cache2"})
public User find(Integer id) {
    return null;
}

使用key属性自定义key:key属性是用来指定Spring缓存方法的返回结果时对应的key的。该属性支持SpringEL表达式。当我们没有指定该属性时,Spring将使用默认策略生成key。

//Cache是发生在cache1上的
@Cacheable(value = "cache1")
public User find(Integer id) {
    return null;
}

//Cache是发生在cache1和cache2上的
@Cacheable(value = {"cache1", "cache2"})
public User find(Integer id) {
    return null;
}

condition属性指定发生的条件:有的时候我们可能并不希望缓存一个方法所有的返回结果。通过condition属性可以实现这一功能。condition属性默认为空,表示将缓存所有的调用情形。其值是通过SpringEL表达式来指定的,当为true时表示进行缓存处理;

如下示例表示只有当user的id为偶数时才会进行缓存。

@Cacheable(value={"users"}, key="#user.id", condition="#user.id%2==0")
public User find(User user) {
    System.out.println("find user by user " + user);
    return user;
}

(2) @CacheEvict

@CacheEvict可以标记在一个方法上,也可以标记在一个类上。

  • 当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作

作用:用于清除缓存

用法:CacheEvict可以指定的属性有value、key、condition、allEntries和beforeInvocation

allEntries属性:allEntries是boolean类型,表示是否需要清除缓存中的所有元素。默认为false,表示不需要。当指定了allEntries为true时,Spring Cache将忽略指定的key。有的时候我们需要Cache一下清除所有的元素,这比一个一个清除元素更有效率。

 @CacheEvict(value="users", allEntries=true)
   public void delete(Integer id) {
      System.out.println("delete user by id: " + id);
   }

beforeInvocation属性:清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时不会触发清除操作。使用beforeInvocation可以改变触发清除操作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。

@CacheEvict(value="users", beforeInvocation=true)
   public void delete(Integer id) {
     System.out.println("delete user by id: " + id);
   }

(3) @CachePut

  • @CachePut也可以声明一个方法支持缓存功能。与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中(更新)。

  • @CachePut也可以标注在类上和方法上。使用@CachePut时我们可以指定的属性跟@Cacheable是一样的。

//每次都会执行方法,并将结果存入指定的缓存中
	@CachePut("users")
	public User find(Integer id) {
	   return null;
	}

(4) @Caching

@Caching注解可以让我们在一个方法或者类上同时指定多个Spring Cache相关的注解。其拥有三个属性:cacheable、put和evict,分别用于指定@Cacheable、@CachePut和@CacheEvict。

@Caching(cacheable = @Cacheable("users"), evict = { @CacheEvict("cache2"),
         @CacheEvict(value = "cache3", allEntries = true) })
   public User find(Integer id) {
      return null;
   }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值