SpringBoot使用Redis

时间:2024-03-31 07:13:14

1.Spring是如何集成Redis的?

Spring Data Redis

引入依赖

 <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>

2.高级封装 

3.Redis配置

#Redis服务器地址
spring.redis.host=192.168.11.84
#Redis连接端口
spring.redis.port=6379
#Redis数据库索引(默认为0)
spring.redis.database=0
#Redis服务器的连接密码默认为空
spring.redis.password=
#连接超时时间
spring.redis.timeout=30000
#连接池最大的连接数(使用负值表示没有限制)默认为8
spring.redis.lettuce.pool.max-active=8
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=1
#连接池中最大空闲等待时间,3s没有活干的时候直接驱逐该链接
spring.redis.lettuce.pool.time-between-eviction-runs=3000
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=-1

4.StringRedisTemplate

String

@SpringBootTest
class StringTests {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Resource(name="redisTemplate")
    RedisTemplate<String,Student> redisTemplate;

    @Test
    public void test3() {
        Student student = Student.builder().id(1).name("海洋").build();
        redisTemplate.opsForValue().set("student.1",student);
//        stringRedisTemplate.opsForValue().set("Student."+student.getId(), JSONUtil.toJsonStr(student));
        Student student1 = redisTemplate.opsForValue().get("student.1");
        System.out.println(student1);
    }
    @Test  //设置过期时间
    public void test(){
//        stringRedisTemplate.opsForValue().set("海洋","帅呆了",30, TimeUnit.SECONDS);
        stringRedisTemplate.opsForValue().set("海洋","帅呆了",Duration.ofSeconds(30));
        String s = stringRedisTemplate.opsForValue().get("海洋");
        System.out.println(s);
    }
    @Test  //setnx(锁的竞争)
    public void test1() {
        Boolean haiyang = stringRedisTemplate.opsForValue().setIfAbsent("haiyang", "88");
    }
    @Test
    public void test2() {
        Long haiyang = stringRedisTemplate.opsForValue().increment("haiyang");
        Long haiyang1 = stringRedisTemplate.opsForValue().increment("haiyang", 20);
        Long haiyang2 = stringRedisTemplate.opsForValue().decrement("haiyang", 50);
    }

    @Test
    public void test4() {
        stringRedisTemplate.opsForValue().append("haiyang","酷");
    }
}

Hash

package com.by;

import com.by.model.Product;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Set;

@SpringBootTest
class HashTests {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Resource(name="redisTemplate")
    RedisTemplate<String, Product> redisTemplate;

    @Test
    public void test(){
        redisTemplate.opsForHash().put("手机","name","小米");
        redisTemplate.opsForHash().put("手机","age","6个月");
        Product product = Product.builder().id(1).name("手机").build();
        redisTemplate.opsForHash().put("手机","小米品牌手机",product);
    }
    @Test
    public void test1(){
        Object o = redisTemplate.opsForHash().get("手机", "name");
        System.out.println(o);
    }
    @Test
    public void test2(){
        Boolean aBoolean = redisTemplate.opsForHash().hasKey("手机", "name");
        Map<Object, Object> entries = redisTemplate.opsForHash().entries("手机");//key和value同时获取
    }
    @Test
    public void test3(){
        Set<Object> objects = redisTemplate.opsForHash().keys("手机");
    }
    @Test
    public void test4(){
        List<Object> values = redisTemplate.opsForHash().values("手机");
    }
    @Test
    public void test5(){
        Product product1 = Product.builder().id(1).name("小米手机").build();
        Product product2 = Product.builder().id(1).name("华为手机").build();
        redisTemplate.opsForHash().put("黑名单",String.valueOf(1),product1);
        redisTemplate.opsForHash().put("黑名单",String.valueOf(2),product2);
    }


}

List

@SpringBootTest
class ListTests {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Resource(name="redisTemplate")
    RedisTemplate<String, Product> redisTemplate;

    @Test
    public void test(){
        Product oppo1 = Product.builder().id(1).name("OPPO").build();
        Product oppo2 = Product.builder().id(2).name("OPPOX").build();
        redisTemplate.opsForList().leftPushAll("OPPO手机",oppo1,oppo2);
    }
    @Test
    public void test1(){
        Product oppo3 = Product.builder().id(3).name("OPPOB").build();
        redisTemplate.opsForList().leftPush("OPPO手机",oppo3);
    }

    @Test
    public void test2(){
      redisTemplate.opsForList().leftPop("OPPO手机");
    }
    @Test
    public void test3(){
        redisTemplate.opsForList().rightPop("OPPO手机");
    }
    @Test
    public void test4(){
        Product product = redisTemplate.opsForList().index("OPPO手机", 0);
        System.out.println(product);
    }
    @Test
    public void test5(){
        Long size = redisTemplate.opsForList().size("OPPO手机");
        System.out.println(size);
    }
    @Test
    void test6() {
        // 如果一些原生命令,spring 没有给我们封装,redisTemplate.execute(new RedisCallback)
        while (true){
            System.out.println("开始一轮监听");
            List<byte[]> rawResults = redisTemplate.execute(new RedisCallback<List<byte[]>>() {
                @Override
                public List<byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
                    return connection.bRPop(10,"OPPO手机".getBytes());
                }
            });
            if(ObjUtil.isNotEmpty(rawResults)){
                byte[] rawKey = rawResults.get(0);
                byte[] rawValue = rawResults.get(1);
                Product product = (Product)redisTemplate.getValueSerializer().deserialize(rawValue);
                System.out.println(product);
            }
        }
    }
}

Set

package com.by;

import cn.hutool.core.util.ObjUtil;
import com.by.model.Product;
import com.by.model.Student;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.StringRedisTemplate;

import javax.annotation.Resource;
import java.util.List;
import java.util.Set;


@SpringBootTest
class SetTests {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Resource(name="redisTemplate")
    SetOperations<String, Student> setOperations;

    @Test//取差集
    void test(){
        stringRedisTemplate.opsForSet().add("海洋","语文","数学","英语","品德");
        stringRedisTemplate.opsForSet().add("洋洋","语文","数学","英语","美术");
        Set<String> difference = stringRedisTemplate.opsForSet().difference("海洋", "甜甜");
    }
    @Test//取交集
    void test1(){
        stringRedisTemplate.opsForSet().add("海洋","语文","数学","英语","品德");
        stringRedisTemplate.opsForSet().add("洋洋","语文","数学","英语","美术");
        Set<String> intersect = stringRedisTemplate.opsForSet().intersect("海洋", "甜甜");
    }
    @Test//取交集
    void test2(){
        stringRedisTemplate.opsForSet().add("海洋","语文","数学","英语","品德");
        stringRedisTemplate.opsForSet().add("洋洋","语文","数学","英语","美术");
        Set<String> union = stringRedisTemplate.opsForSet().union("海洋", "甜甜");
    }

}

Zset

@SpringBootTest
class ZSetTests {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Resource(name="redisTemplate")
    SetOperations<String, Student> setOperations;
    @Test
    void test(){
        stringRedisTemplate.opsForZSet().add("海洋","语文",80);
        stringRedisTemplate.opsForZSet().add("海洋","英语",60);
        stringRedisTemplate.opsForZSet().add("海洋","数学",70);
        Long aLong = stringRedisTemplate.opsForZSet().size("海洋");
        Long aLong1 = stringRedisTemplate.opsForZSet().removeRangeByScore("海洋", 60, 100);

    }
    @Test
    void test1(){
        stringRedisTemplate.opsForZSet().add("海洋","语文",80);
        stringRedisTemplate.opsForZSet().add("海洋","英语",60);
        stringRedisTemplate.opsForZSet().add("海洋","数学",70);
        Set<String> range1 = stringRedisTemplate.opsForZSet().range("海洋", 0,-1);
        Set<ZSetOperations.TypedTuple<String>> tuples = stringRedisTemplate.opsForZSet().rangeByScoreWithScores("海洋", 60, 100);
        Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores("海洋", 50, 100);//正序排列
    }

    @Test //模拟新闻点击量,排名
    void test2(){
        String key ="product.hot";
        ArrayList<Integer> productId = CollUtil.newArrayList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);//使用hutool中的工具,获取先的数组
        ExecutorService executorService = Executors.newFixedThreadPool(100);//jdk自带的线程池
        for (int i = 1; i <=100; i++) {
            executorService.submit(()->{
                int c = RandomUtil.randomInt(1, 11);//RandomUtil.randomInt 获得指定范围内的随机数,例如我们想产生一个[10, 100)的随机数
                System.out.println("访问了"+c);
                stringRedisTemplate.opsForZSet().incrementScore(key,String.valueOf(c),1);//每次访问,数据加一
            });
        }
        //因为是异步的,避免冲突
        ThreadUtil.safeSleep(5000);
        Set<String> strings = stringRedisTemplate.opsForZSet().reverseRange(key, 0, -1);
    }


}

BitMap

@SpringBootTest
class BitMapTests {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Resource(name="redisTemplate")
    RedisTemplate<String, Student> redisTemplate;
    private String key = "sing.2024.haiyang";

    @Test //签到
    void test(){
        Boolean b = stringRedisTemplate.opsForValue().setBit(key, 10, true);//设置某一天的偏移量,表示第10天的偏移量为1
         b = stringRedisTemplate.opsForValue().setBit(key, 30, true);
         b = stringRedisTemplate.opsForValue().setBit(key, 56, true);

        RedisCallback<Long> redisCallback = new RedisCallback<Long>() {
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return connection.bitCount(key.getBytes());
            }
        };
        Long execute = redisTemplate.execute(redisCallback);
    }
    @Test //车展,统计总的人数和这三天都来的人数
    void test1() {
        String key1 = "2024.3.28";
        String key2 = "2024.3.29";
        String key3 = "2024.3.30";
        int yangyang = 10, tiantian = 20, tangtang = 40;
        //第一天
        stringRedisTemplate.opsForValue().setBit(key1, yangyang, true);
        stringRedisTemplate.opsForValue().setBit(key1, tangtang, true);
        //第二天
        stringRedisTemplate.opsForValue().setBit(key2, yangyang, true);
        stringRedisTemplate.opsForValue().setBit(key2, tiantian, true);
        stringRedisTemplate.opsForValue().setBit(key2, tangtang, true);
        //第三天
        stringRedisTemplate.opsForValue().setBit(key3, yangyang, true);
        stringRedisTemplate.opsForValue().setBit(key3, tangtang, true);
        //Q1统计一共来了多少人
        String q1 = "q1";
        RedisCallback<Long> redisCallback = connection -> {
            return connection.bitOp(RedisStringCommands.BitOperation.OR, q1.getBytes(), key1.getBytes(), key2.getBytes(), key3.getBytes());
        };
        //将三天的key合并值放到q1的key中
        Long aLong = redisTemplate.execute(redisCallback);

        RedisCallback<Long> redisCallback2 = connection -> {
            return connection.bitCount(q1.getBytes());
        };
        //求q1里面1的总和
        Long execute = redisTemplate.execute(redisCallback2);
        //Q2:统计每天都来的人数
         String q2="q2";
        redisCallback = connection -> {
            return connection.bitOp(RedisStringCommands.BitOperation.AND, q2.getBytes(), key1.getBytes(), key2.getBytes(), key3.getBytes());
        };
        //将三天的key合并值放到q1的key中
        Long aLong1 = redisTemplate.execute(redisCallback);

        redisCallback2 = connection -> {
            return connection.bitCount(q2.getBytes());
        };
        //求q1里面1的总和
        execute = redisTemplate.execute(redisCallback2);
    }

}

 

5.RedisTemplate

5.1乱码的问题

 JdkSerializationRedisSerializer  serializer = new JdkSerializationRedisSerializer();
        byte[] serialize = serializer.serialize("user#01");
        System.out.println(new String(serialize));

5.2自定义序列化工具(RedisTemplate)配置类

@Configuration
public class RedisConfig {
    @Bean //主动注册了一个名字为redisTemplate的bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer jackson = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        //启用默认类型推理,将类型信息作为属性写入json
        //就是把类型的全类名写入JSON
        mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(),ObjectMapper.DefaultTyping.NON_FINAL);
        jackson.setObjectMapper(mapper);
        template.setKeySerializer(RedisSerializer.string());
        template.setValueSerializer(jackson);
        template.setHashKeySerializer(RedisSerializer.string());
        template.setHashValueSerializer(jackson);
        return template;
    }

}

SetNX(分布式锁)

@SpringBootTest
class SetNXTests {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Resource(name="redisTemplate")
    ValueOperations<String, String> valueOperations;

    @Test
    void test(){
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 1; i <=5; i++) {
            executorService.execute(()->{
                //某一个工人
                String ioId="IO"+ RandomUtil.randomInt(1,1000);
                while (true){
                    Boolean b = valueOperations.setIfAbsent("lock.product.1", ioId + " : " + DateUtil.now());
                    if (b){
                        System.out.println(Thread.currentThread().getId()+"获得了分布式锁======");
                        //执行业务
                        ThreadUtil.safeSleep(3000);
                        //执行业务成功后
                        stringRedisTemplate.delete("lock.product.1");
                        System.out.println(Thread.currentThread().getId()+"释放了分布式锁++++++++");
                        break;
                    }else {
                        System.out.println(Thread.currentThread().getId()+"没有获得分布式锁-------------");
                        ThreadUtil.safeSleep(3000);
                    }
                }
            });
        }
        ThreadUtil.safeSleep(100000);
    }

LuaTest 

在Redis中使用lua脚本,主要是其能够使多条redis语句具有原子性,在处理订单模块具有重要作用

  1. 参数说明:

    • KEYS[]数组用于在脚本中引用传入的键名。
    • ARGV[]数组用于传递额外的参数(非键名)给脚本。
    • redis.call()函数在脚本内部调用Redis命令。
@SpringBootTest
class LuaTests {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Resource(name="redisTemplate")
    RedisTemplate<String, Student> redisTemplate;

    @Test
    void test(){
        String lua =  "return redis.call('set',KEYS[1],ARGV[1])";
        RedisScript<String> redisScript = RedisScript.of(lua, String.class);
        stringRedisTemplate.execute(redisScript, CollUtil.newArrayList("a"),"b100");
    }

    @Test
    void test1(){
        for (int i = 1; i <= 5; i++) {
            stringRedisTemplate.opsForValue().set("product."+i,String.valueOf(i));
        }
    }

    @Test  //一次扣减一个库存商品
    void test2(){
        StringBuilder sb = new StringBuilder();
        sb.append( " local key = KEYS[1] " );//你要扣减的key,例如:product.1
        sb.append( " local qty = ARGV[1] " );//你要剪得的数量
        sb.append( "local redis_qty = redis.call('get',key) " );//查询redis里面存储的数量
        sb.append( " if tonumber(redis_qty) >= tonumber(qty) then" ); //库存量与需求量进行对比,tonumber作用是转成数字
        sb.append( " redis.call('decrby',key,qty) " );   //对redis数据库进行减操作
        sb.append( " return -1 " ); //满足条件,返回-1
        sb.append( " else " );
        sb.append( "   return tonumber(redis_qty) " );//如果不满足,返回库存数量
        sb.append( "  end " );
        RedisScript<Long> redisScript = RedisScript.of(sb.toString(), Long.class);
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 1; i <= 5; i++) {
            executorService.execute(()->{
                int qty = RandomUtil.randomInt(1,6);
                Long aLong = stringRedisTemplate.execute(redisScript, CollUtil.newArrayList("product.5"), String.valueOf(qty));
                if (aLong == -1L) {
                    System.out.println(Thread.currentThread().getId() + " 扣减成功,扣减了-> "+ qty);
                } else {
                    System.out.println(Thread.currentThread().getId() + "扣减失败,需求量是:"+qty+",剩余库存量:"+aLong);
                }
            });
             ThreadUtil.safeSleep(3000);//因为线程是异步的,所以要睡眠一定时间
        }

    }

    @Test  //一次扣减多个库存
    void test3(){
        StringBuilder sb = new StringBuilder();
        sb.append( " local table = {} " );//所有不满足的商品都存到table
        sb.append( " local values =  redis.call('mget', unpack(KEYS)) " );//查询所有的key所含有的value,[product.1,product.2]=>product.1,product.2
        sb.append( " for i = 1, #KEYS   do  " );//循环,#KEYS代表KEYS的值
        sb.append( " if tonumber(ARGV[i]) > tonumber(values[i]) then " );//如果需求量大于库存量
        sb.append( " table[#table + 1] =  KEYS[i] .. '=' .. values[i] " );
        sb.append( " end " );
        sb.append( " end " );
        sb.append( " if #table > 0 then " );//如果有不满足的商品,返回该需求
        sb.append( " return table " );
        sb.append( " end " );
        sb.append( " for i=1 ,#KEYS do " );//如果满足,进行循环扣除
        sb.append( " redis.call('decrby',KEYS[i],ARGV[i]) " );
        sb.append( " end " );
        sb.append( " return{} " );
        RedisScript<List> redisScript = RedisScript.of(sb.toString(), List.class);
        List<StockProduct> stockProducts =new ArrayList<>();
        stockProducts.add(new StockProduct(5,1));
        stockProducts.add(new StockProduct(4,2));
        List<String> keys = stockProducts.stream().map(it -> "product." + it.getId()).collect(Collectors.toList());
        Object[] qtys = stockProducts.stream().map(it -> it.getQty()+"").toArray();
        List<String> list = stringRedisTemplate.execute(redisScript, keys, qtys);
        if (list.isEmpty()){
            System.out.println("库存冻结成功");
        }else {
            for(String key_qty : list){
                String[] split = key_qty.split("=");
                System.out.println(split[0]+"库存不足,剩余库存量"+split[1]);
            }
        }
        ThreadUtil.safeSleep(3000);
    }
}

为什么不使用decyby,decrby具有弊端,以下通过demo进行演示

 @Test
    void test(){
        String key ="product.1";
        valueOperations.set(key,5);
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 1; i <=5; i++) {
            executorService.execute(()->{
                Integer redis_qty = valueOperations.get(key);
                if (redis_qty>=1){
                    //满足条件,数量减一,但是可能被打断
                    valueOperations.set(key,redis_qty-1);
                    System.out.println("线程:"+Thread.currentThread().getId() +"执行成功,数量减一");
                }else {
                    System.out.println("线程:"+Thread.currentThread().getId() +"执行失败");
                }
            });
        }
        ThreadUtil.safeSleep(3000);
    }

可以看出,定义了product.1的数量为5,线程数为5,正常结果应该为0,但是结果缺为4,这是因为,线程的执行速度非常快,多条线程执行时,库里面显示的都是5,才会造成这种原因。

@Test
    void test2(){
        String key ="product.1";
        valueOperations.set(key,5);
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 1; i <=8; i++) {
            executorService.execute(()->{
                Integer redis_qty = valueOperations.get(key);
                if (redis_qty>=0){
                    //满足条件,数量减一,但是可能被打断
                    Long redis_qty2 = valueOperations.decrement(key);
                    System.out.println("线程:"+Thread.currentThread().getId() +"执行成功,数量减一");
                }else {
                    System.out.println("线程:"+Thread.currentThread().getId() +"执行失败");
                }
            });
        }
        ThreadUtil.safeSleep(3000);
    }

decrby具有的弊端为,使商品多买,出现负数。