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 {@AutowiredStringRedisTemplate stringRedisTemplate;@Resource(name="redisTemplate")RedisTemplate<String,Student> redisTemplate;@Testpublic 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");}@Testpublic void test2() {Long haiyang = stringRedisTemplate.opsForValue().increment("haiyang");Long haiyang1 = stringRedisTemplate.opsForValue().increment("haiyang", 20);Long haiyang2 = stringRedisTemplate.opsForValue().decrement("haiyang", 50);}@Testpublic 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 {@AutowiredStringRedisTemplate stringRedisTemplate;@Resource(name="redisTemplate")RedisTemplate<String, Product> redisTemplate;@Testpublic void test(){redisTemplate.opsForHash().put("手机","name","小米");redisTemplate.opsForHash().put("手机","age","6个月");Product product = Product.builder().id(1).name("手机").build();redisTemplate.opsForHash().put("手机","小米品牌手机",product);}@Testpublic void test1(){Object o = redisTemplate.opsForHash().get("手机", "name");System.out.println(o);}@Testpublic void test2(){Boolean aBoolean = redisTemplate.opsForHash().hasKey("手机", "name");Map<Object, Object> entries = redisTemplate.opsForHash().entries("手机");//key和value同时获取}@Testpublic void test3(){Set<Object> objects = redisTemplate.opsForHash().keys("手机");}@Testpublic void test4(){List<Object> values = redisTemplate.opsForHash().values("手机");}@Testpublic 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 {@AutowiredStringRedisTemplate stringRedisTemplate;@Resource(name="redisTemplate")RedisTemplate<String, Product> redisTemplate;@Testpublic 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);}@Testpublic void test1(){Product oppo3 = Product.builder().id(3).name("OPPOB").build();redisTemplate.opsForList().leftPush("OPPO手机",oppo3);}@Testpublic void test2(){redisTemplate.opsForList().leftPop("OPPO手机");}@Testpublic void test3(){redisTemplate.opsForList().rightPop("OPPO手机");}@Testpublic void test4(){Product product = redisTemplate.opsForList().index("OPPO手机", 0);System.out.println(product);}@Testpublic void test5(){Long size = redisTemplate.opsForList().size("OPPO手机");System.out.println(size);}@Testvoid test6() {// 如果一些原生命令,spring 没有给我们封装,redisTemplate.execute(new RedisCallback)while (true){System.out.println("开始一轮监听");List<byte[]> rawResults = redisTemplate.execute(new RedisCallback<List<byte[]>>() {@Overridepublic 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 {@AutowiredStringRedisTemplate 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 {@AutowiredStringRedisTemplate stringRedisTemplate;@Resource(name="redisTemplate")SetOperations<String, Student> setOperations;@Testvoid 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);}@Testvoid 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 {@AutowiredStringRedisTemplate 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天的偏移量为1b = stringRedisTemplate.opsForValue().setBit(key, 30, true);b = stringRedisTemplate.opsForValue().setBit(key, 56, true);RedisCallback<Long> redisCallback = new RedisCallback<Long>() {@Overridepublic 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的beanpublic 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//就是把类型的全类名写入JSONmapper.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 {@AutowiredStringRedisTemplate stringRedisTemplate;@Resource(name="redisTemplate")ValueOperations<String, String> valueOperations;@Testvoid 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语句具有原子性,在处理订单模块具有重要作用
-
参数说明:
KEYS[]
数组用于在脚本中引用传入的键名。ARGV[]
数组用于传递额外的参数(非键名)给脚本。redis.call()
函数在脚本内部调用Redis命令。
@SpringBootTest
class LuaTests {@AutowiredStringRedisTemplate stringRedisTemplate;@Resource(name="redisTemplate")RedisTemplate<String, Student> redisTemplate;@Testvoid 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");}@Testvoid 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.1sb.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 " ); //满足条件,返回-1sb.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 = {} " );//所有不满足的商品都存到tablesb.append( " local values = redis.call('mget', unpack(KEYS)) " );//查询所有的key所含有的value,[product.1,product.2]=>product.1,product.2sb.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进行演示
@Testvoid 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,才会造成这种原因。
@Testvoid 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具有的弊端为,使商品多买,出现负数。