第七章-springboot整合redis
分类: springboot 专栏: springboot3.0新教材 标签: 整合redis
2024-01-08 15:33:49 3228浏览
redis简介
Redis是一款基于key-value的内存数据存储系统,有以下一些优点: 速度快:Redis非常快,每秒可执行大约110000次的设置(SET)操作,每秒大约可执行81000次的读取/获取(GET)操作。
支持丰富的数据类型:Redis支持五种数据类型: string (字符串) , hash(哈希) , list(列表) , set(集合)及zset(sorted set:有序集合)。
操作具有原子性:所有Redis操作都是原子操作,这确保如果两个客户端并发访问,Redis服务器能接收更新的值。
应用广泛:可用于缓存,消息队列(Redis本地支持发布/订阅),应用程序中的任何短期数据如web应用程序中的会话(Session)、网页计数、排行榜等。
redis的安装与运行
安装windows版本的Redis即可,另外可以下载Redis图形化客户端工具,方便直观查看数据库信息,这里下载的是RedisDesktopManager。
启动redis:双击redis-server.exe就行
疑问:那如果我要设置密码启动呢?
修改redis.windows.conf里的配置
然后启动的时候,是在redis安装目录下,cmd里输入redis-server.exe redis.windows.conf回车
redis的常用命令
a. 通用命令
select:选择数据库,Redis默认有16个数据库,编号是0-15,默认当前数据库是0,如果要选择其他编号的数据库则用“select 编号”的命令。
keys:查看键,后面接键的名称或包含通配符*的字符串,如果要查看所有的键,则用 keys *。
exists key:是否存个某个键,key代表键的名称。
del key:删除某个键,key代表键的名称。
flushall:删除所有的键
flushdb:删除当前数据库中的所有Key
b. 值为String类型的有关命令
set key value:添加一个键值对到Redis数据库中,如果键存在,则修改键的值。
mset key1 value1 key2 value…:一次设置多个键值对。
get key:通过键查找值。
mget key1 key2 key3…:一次读取多个键的值。
append key value:追加value值到键原有值上,如果没有这个键,则新建。
c. 值为 List类型的有关命令
lpush key value:添加一个值到列表的头部(左侧添加)。
rpush key value:添加一个值到列表的尾部(右侧添加)。
lrange key start stop:查看列表中索引从start到stop之间的值。
lpop key:返回并删除列表中的头部的值。
rpop key: 返回并删除列表中的尾部的值。
springboot访问redis
Spring Boot访问Redis的技术称为Spring Data Redis,Spring Boot提供了RedisAutoConfiguration自动配置类,该自动配置类检测到spring-boot-start-data-redis依赖包被使用时生效,只需要在Spring Boot中导入spring-boot-start-data-redis依赖包就可以使用Spring Data Redis。Spring Data Redis默认创建了RedisTemplate和StringRedisTemplate两个bean,用户可以从Spring容器中注入并使用它们进行Redis的常用操作。此外用户还可以使用RedisRepository来访问Redis数据库。RedisTemplate提供了操作Redis中的多种数据类型的API,StringRedisTemplate只针对键值都是字符串类型的数据进行操作。RedisTemplate默认使用JdkSerializationRedisSerializer进行序列化,StringRedisTemplate默认使用StringRedisSerializer进行序列化。
RedisAutoConfiguration自动配置类创建RedisTemplate和StringRedisTemplate实例(bean)时使用了RedisProperties类提供的属性,包括Redis服务器地址、端口号、密码等等属性,这些属性很多都有默认值,比如服务器地址默认为localhost、端口号默认为6379,同时该类上面有@ConfigurationProperties(prefix = "spring.redis")注解, 表示可以加载Spring Boot的主配置文件application.properties中前缀为spring.redis的配置信息,这样用户既可以不在application.properties做任何有关Redis的配置,也可以在application.properties中使用spring.redis前缀重新配置Redis。查看RedisProperties源码,可以看到Spring Boot有关Redis的默认配置如下,如果要重新配置,拷贝这些代码到application.properties中并适当修改即可。代码如下:
redis template操作string类型
a. 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
b. 配置application.properties
Spring Boot自动配置了Redis,默认配置可以满足本项目要求,所以这里application.properties不需要任何配置。
c. 创建实体类。注意实体类Book必须实现序列化接口。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book implements Serializable {
private Integer id;
private String name;
private Double price;
private String category;
private Integer pnum;
private String imgurl;
private String description;
private String author;
private Integer sales;
}
d. 写单元测试类
然后开始直接写单元测试类(完成以下功能)
//存储字符串类型的key-value
//通过键获取字符串类型的值
//删除某个键值对
//保存一本书
//通过id号查找一本书
//保存多本书 list
//查找所有书
//从多本book集合中找到某个id的书
//根据id删除book集合中的某本书
Book book = new Book(1, "三国演义", 100.00, "文学", 100, "101.png", "四大名著", "罗贯中", 55);
new Book(1,"C语言程序设计",50.0,"计算机",100,"101.jpg","","zhangsan",50),
new Book(2,"java语言程序设计",60.0,"计算机",100,"102.jpg","","zhangsan",50),
new Book(3,"python语言程序设计",70.0,"计算机",100,"103.jpg","","zhangsan",50)
@SpringBootTest
public class RedisTemplateTest {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
RedisTemplate redisTemplate;
//存储字符串类型的key-value
@Test
void testKeyV(){
stringRedisTemplate.opsForValue().set("username","xiaojie");
}
//通过键获取字符串类型的值
@Test
void getValue(){
String username = stringRedisTemplate.opsForValue().get("username");
System.out.println(username);
}
//删除某个键值对
@Test
void delKey(){
stringRedisTemplate.delete("username");
}
//保存一本书
@Test
void saveBook(){
Book book = new Book(1, "三国演义", 100.00, "文学", 100, "101.png", "四大名著", "罗贯中", 55);
redisTemplate.opsForValue().set(1,book);
}
//通过id号查找一本书
@Test
void getBook(){
Book book = (Book) redisTemplate.opsForValue().get(1);
System.out.println(book);
}
//保存多本书 list
@Test
void saveBooks(){
List<Book> books= Arrays.asList(
new Book(1,"C语言程序设计",50.0,"计算机",100,"101.jpg","","zhangsan",50),
new Book(2,"java语言程序设计",60.0,"计算机",100,"102.jpg","","zhangsan",50),
new Book(3,"python语言程序设计",70.0,"计算机",100,"103.jpg","","zhangsan",50)
);
redisTemplate.opsForValue().set("books",books);
}
//查找所有书
@Test
void searchBooks(){
System.out.println(redisTemplate.opsForValue().get("books"));
}
//从多本book集合中找到某个id的书
@Test
void getBookById(){
Integer id =1;
List<Book> books =(List<Book>) redisTemplate.opsForValue().get("books");
Book book = books.get(id - 1);
System.out.println(book);
}
//根据id删除book集合中的某本书
@Test
void delBook(){
Integer id =1;
List<Book> books =(List<Book>) redisTemplate.opsForValue().get("books");
books.remove(id-1);
redisTemplate.opsForValue().set("books",books);
}
}
e. 解决序列化问题
使用StringRedisTemplate操作的数据没什么问题,但RedisTemplate操作的数据为乱码,无法直接查看,这是因为RedisTemplate默认的序列化为JdkSerializationRedisSerializer,这样对象数据就会使用Java对象流保存为序列化后的字符串(无法直接查看)。对于这种情况,可以通过在配置类中重新创建RedisTemplate的bean,使用Jackson2JsonRedisSerialize替换默认序列化,使对象转换成JSON格式的字符串再进行保存。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate( RedisConnectionFactory redisConnectionFactory) {
RedisTemplate template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
//objectMapper, 必须加,解决查询缓存转换异常的问题
Jackson2JsonRedisSerializer ser = new Jackson2JsonRedisSerializer(objectMapper,Object.class);
template.setDefaultSerializer(ser);
return template;
}
}
redis template操作redis各种数据类型
RedisTemplate提供了以下5种方法分别操作Redis的5种类型的数据。
(1) opsForValue()方法:操作字符串。
(2) opsForHash()方法:操作散列。
(3) opsForList()方法:操作列表。
(4) opsForSet()方法:操作集合。
(5) opsForZSet()方法:操作有序集合。
1. opsForValue()方法
void set(K key,V value)方法:用于存储键值对,键和值都是字符串,其中值可以是序列化后的对象
void set(K key,V value,long timeout,timeUnit unit)方法:存储键值对的同时规定了失效时间,timeout代表失效时间,unit代表时间单位,可以取以下值:
(1) TimeUnit.DAYS:日
(2) TimeUnit.HOURS:时
(3) TimeUnit.MINUTES:分
(4) TimeUnit.SECONDS:秒
(5) TimeUnit.MILLISECONDS:毫秒
a. 测试案例1:set
@SpringBootTest
class RedisdemoApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void test1() throws InterruptedException { //测试失效时间
redisTemplate.opsForValue().set("test1", "Test Timeout 5 seconds", 5, TimeUnit.SECONDS);
System.out.println("第0秒取值:" + redisTemplate.opsForValue().get("test1"));
Thread.sleep(4000);
System.out.println("第4秒取值:" + redisTemplate.opsForValue().get("test1"));
Thread.sleep(2000);
System.out.println("第6秒取值:" + redisTemplate.opsForValue().get("test1"));
}
第0秒取值:Test Timeout 5 seconds
第4秒取值:Test Timeout 5 seconds
第6秒取值:null
可见一旦超过时间,就会被redis删掉
b. 测试案例2:getAndSet
V getAndSet(K key,V value)方法:先获取并返回key的旧值,再设置为新值。
@Test
void test2() throws InterruptedException { //测试getAndSet方法
redisTemplate.opsForValue().set("test2", "Test getAndSet1");
System.out.println(redisTemplate.opsForValue().getAndSet("test2", "Test getAndSet2"));
System.out.println(redisTemplate.opsForValue().get("test2"));
}
Test getAndSet1
Test getAndSet2
c. 案例3:append
Integer append(K key,String value)方法:如果key不存在则创建,相当于set方法,如果key存在则追加字符串到未尾。
@Test
void test3() { //测试append方法
redisTemplate.setValueSerializer(new StringRedisSerializer());//设置字符串序列化
redisTemplate.opsForValue().append("test3", "test3"); //首次追加相当于set方法
System.out.println(redisTemplate.opsForValue().get("test3"));
redisTemplate.opsForValue().append("test3", " OK!");//追加内容到未尾
System.out.println(redisTemplate.opsForValue().get("test3"));
}
test3
test3 OK!
2. opsForHash()方法
opsForHash()方法又提供操作Hash类型的值的方法如下:void put(H h, HK hk,HV hv)方法:用于存储指定键中的字段与值。Map<HK, HV> entries(H h)方法:获取指定键的所有字段与值。
分别存储书的名称与价格信息
@Test
void testHash1() { //测试put方法,存入一个键值
redisTemplate.opsForHash().put("book1", "bookname", "三国演义");
redisTemplate.opsForHash().put("book1", "price", "69.0");
System.out.println(redisTemplate.opsForHash().entries("book1")); //读取整个hash类型(键与值)
}
{bookname=三国演义, price=69.0}
a. putAll
void putAll(H h, java.util.Map<? extends HK, ? extends HV> map)方法:用Map 封装多个字段与值,一次性存储多个字段与值。
@Test
void testHash2() { //测试Hash类型,putAll方法,将整个hashmap存入
Map<String, Object> map = new HashMap();
map.put("bookname", "西游记");
map.put("category", "文学");
redisTemplate.opsForHash().putAll("book2", map);
System.out.println(redisTemplate.opsForHash().entries("book2")); //读取整个hash类型
}
b. keys
Set<HK> keys(H h)方法:获取指定键的字段的集合。
@Test
void testHash3() { //测试keys方法。获取所有键的集合
redisTemplate.opsForHash().put("book3", "bookname", "红楼梦");
redisTemplate.opsForHash().put("book3", "price", "89.0");
System.out.println(redisTemplate.opsForHash().keys("book3")); //读取所有键的集合
}
[bookname, price]
c. values
List<HV> values(H h)方法:获取指定键的值的集合。
@Test
void testHash4() { //测试values方法。获取所有值的集合
redisTemplate.opsForHash().put("book4", "bookname", "封神榜");
redisTemplate.opsForHash().put("book4", "price", "79.0");
System.out.println(redisTemplate.opsForHash().values("book4")); //读取所有值的集合
}
[封神榜, 79.0]
d. size、hasKey、delete
Long size(H h)方法:返回指定键中字段的数量。
Boolean hasKey(H h, Object o)方法:查找指定键中是否包含某个字段。
Long delete(H h, Object... objects)方法:删除指定键中的某个字段。
@Test
void testHash5() { //测试delete方法。删除某个键的某个字段,hasKey方法,size方法
redisTemplate.opsForHash().put("book5", "bookname", "封神榜");
redisTemplate.opsForHash().put("book5", "price", "89.0");
System.out.println(redisTemplate.opsForHash().entries("book5"));
System.out.println(redisTemplate.opsForHash().size("book5"));//字段个数
System.out.println(redisTemplate.opsForHash().hasKey("book5", "price")); //判断字段是否存在
redisTemplate.opsForHash().delete("book5", "price"); //删除一个字段
System.out.println(redisTemplate.opsForHash().hasKey("book5", "price"));//再次判断
System.out.println(redisTemplate.opsForHash().entries("book5"));
}
{bookname=封神榜, price=89.0}
2
true
false
{bookname=封神榜}
3. opsForList()方法
a. leftPush:从左侧添加一个元素到列表
Long leftPush(K k, V v):从左侧添加一个元素到列表。
Long leftPushAll(K k, V... vs):从左侧一次添加多个元素到列表。
Long size(K k):获取集合中的元素个数。
java.util.List<V> range(K k,long l, long l1):返回某个列表指定索引区间的元素。索引从0算起,-1表示最右侧第一个元素。
@Test
void TestList1() {//测试leftPush方法,leftPushAll方法,range方法,size方法
redisTemplate.opsForList().leftPush("mylist1", "a");
redisTemplate.opsForList().leftPush("mylist1", "b");
redisTemplate.opsForList().leftPush("mylist1", "c");
System.out.println(redisTemplate.opsForList().size("mylist1"));
System.out.println(redisTemplate.opsForList().range("mylist1", 0, -1));
redisTemplate.opsForList().leftPushAll("mylist1", "1","2");//添加多个元素
String[] str = {"3", "4", "5"};
redisTemplate.opsForList().leftPushAll("mylist1", str);//使用数组添加多个元素
System.out.println(redisTemplate.opsForList().size("mylist1"));
System.out.println(redisTemplate.opsForList().range("mylist1", 0, -1));
}
3
[c, b, a]
8
[5, 4, 3, 2, 1, c, b, a]
b. rightPushAll:从右侧添加多个元素到列表
Long rightPushAll(K k, V... vs):从右侧一次添加多个元素到列表。
@Test
void TestList2() {//测试rightPush方法,rightPushAll方法
redisTemplate.opsForList().rightPush("mylist2", "a");
redisTemplate.opsForList().rightPush("mylist2", "b");
redisTemplate.opsForList().rightPush("mylist2", "c");
System.out.println(redisTemplate.opsForList().size("mylist2"));
System.out.println(redisTemplate.opsForList().range("mylist2", 0, -1));
redisTemplate.opsForList().rightPushAll("mylist2", "1","2");//一次添加多个元素
String[] str = {"3", "4", "5"};
redisTemplate.opsForList().rightPushAll("mylist2", str);//使用数组一次添加多个元素
System.out.println(redisTemplate.opsForList().size("mylist2"));
System.out.println(redisTemplate.opsForList().range("mylist2", 0, -1));
}
3
[a, b, c]
8
[a, b, c, 1, 2, 3, 4, 5]
c. remove:删除集合中指定数量个元素
【例7-11】 Long remove(K k, long l, Object o):删除集合中指定数量个元素,如果数量为0,则该元素全部从集合删除,如果数量为正数,则从左到右删除,如果为负数,则从右到左删除。
@Test
void testList3() { //测试remove
String[] str = {"10", "20", "30", "10", "20", "30", "10", "20", "30"};
redisTemplate.opsForList().rightPushAll("mylist3", str);
System.out.println(redisTemplate.opsForList().range("mylist3", 0, -1));
redisTemplate.opsForList().remove("mylist3", 0, "10");//全部删除
System.out.println(redisTemplate.opsForList().range("mylist3", 0, -1));
redisTemplate.opsForList().remove("mylist3", 2, "20");//从左到右删
System.out.println(redisTemplate.opsForList().range("mylist3", 0, -1));
redisTemplate.opsForList().remove("mylist3", -2, "30");//从右到左删
System.out.println(redisTemplate.opsForList().range("mylist3", 0, -1));
}
[10, 20, 30, 10, 20, 30, 10, 20, 30]
[20, 30, 20, 30, 20, 30]
[30, 30, 20, 30]
[30, 20]
d. leftPop和rightPop:删除并返回左侧/右侧第一个元素
V leftPop(K k):删除并返回左侧第一个元素。
V rightPop(K k):删除并返回右侧第一个元素。
@Test
void testList4() {//测试leftPop,rightPop方法
String[] str = {"a", "b", "c","d","e"};
redisTemplate.opsForList().rightPushAll("mylist4", str);
System.out.println(redisTemplate.opsForList().range("mylist4", 0, -1));
System.out.println(redisTemplate.opsForList().leftPop("mylist4"));//从左侧删
System.out.println(redisTemplate.opsForList().range("mylist4", 0, -1));
System.out.println(redisTemplate.opsForList().rightPop("mylist4"));//从右侧删
System.out.println(redisTemplate.opsForList().range("mylist4", 0, -1));
}
[a, b, c, d, e]
a
[b, c, d, e]
e
[b, c, d]
e. index:获取指定索引处的元素值 set:为指定索引处的元素设置值,原有值将被覆盖
V index(K k, long l):获取指定索引处的元素值-
void set(K k, long l, V v):为指定索引处的元素设置值,原有值将被覆盖。
@Test
void testList5(){ //测试set和index方法
String[] str = {"a", "b", "c"};
redisTemplate.opsForList().rightPushAll("mylist5", str);
System.out.println(redisTemplate.opsForList().range("mylist5", 0, -1));
System.out.println(redisTemplate.opsForList().index("mylist5",1));//获取索引1处的值
redisTemplate.opsForList().set("mylist5", 1,"d");//将索引1处的值改为b
System.out.println(redisTemplate.opsForList().range("mylist5", 0, -1));
System.out.println(redisTemplate.opsForList().index("mylist5",1));
}
[a, b, c]
b
[a, d, c]
d
4. opsForSet()方法
a. add size members remove pop
Long add(K k, V... vs)方法:添加数据(元素)到集合。
Long size(K k)方法:返回集合的长度,即集合中元素的个数。
java.util.Set<V> members(K k):返回集合中的所有元素。
Long remove(K k, Object... objects):删除元素。
V pop(K k):随机删除。
@Test
void testSet1(){ //测试添加 删除,size,members方法
redisTemplate.opsForSet().add("myset1","a");
redisTemplate.opsForSet().add("myset1","b","c");
String[] str={"1","2"};
redisTemplate.opsForSet().add("myset1",str);
System.out.println("size:"+redisTemplate.opsForSet().size("myset1"));
System.out.println(redisTemplate.opsForSet().members("myset1"));//查看所有元素
System.out.println(redisTemplate.opsForSet().remove("myset1","a"));//删除一个元素
System.out.println(redisTemplate.opsForSet().members("myset1"));
System.out.println(redisTemplate.opsForSet().remove("myset1",str));//删除多个元素
System.out.println(redisTemplate.opsForSet().members("myset1"));
redisTemplate.opsForSet().add("myset1",str);//重新添加
System.out.println(redisTemplate.opsForSet().members("myset1")); //再次查看
System.out.println(redisTemplate.opsForSet().pop("myset1"));//随机删除
System.out.println(redisTemplate.opsForSet().members("myset1"));
}
size:5
[b, a, 1, c, 2]
1
[1, c, 2, b]
2
[b, c]
[2, b, 1, c]
2
[b, 1, c]
b. move:移动元素到另一个集合
Boolean move(K k, V v,K k1)方法:移动元素到另一个集合。
@Test
void testSet2(){ //测试移动
String[] str1={"1","2","3"};
String[] str2={"a","b","c"};
System.out.println(redisTemplate.opsForSet().add("myset2",str1));
System.out.println(redisTemplate.opsForSet().add("myset3",str2));
System.out.println(redisTemplate.opsForSet().members("myset2"));
System.out.println(redisTemplate.opsForSet().members("myset3"));
redisTemplate.opsForSet().move("myset2","1","myset3");
System.out.println(redisTemplate.opsForSet().members("myset2"));
System.out.println(redisTemplate.opsForSet().members("myset3"));
}
[3, 1, 2]
[c, a, b]
[3, 2]
[c, a, 1, b]
c. scan:遍历集合中所有元素
Cursor<V> scan(K k, ScanOptions scanOptions):遍历集合中所有元素。
@Test
void testSet3(){
String[] str={"a","b","c"};
redisTemplate.opsForSet().add("myset4",str);
Cursor<Object> cursor=redisTemplate.opsForSet().scan("myset4", ScanOptions.NONE);
while(cursor.hasNext()){
System.out.println(cursor.next());
}
}
5. opsForZSet()方法
Boolean add(K k, V v, double v1):添加元素。
Long size(K k):返回集合中元素的个数。
Set<V> range(K k, long l, long l1):返回指定索引范围的元素。
Long rank(K k, Object o):返回某个元素的索引。
@Test
void testZset1(){
redisTemplate.opsForZSet().add("zset1","f1",80);
redisTemplate.opsForZSet().add("zset1","f2",70);
redisTemplate.opsForZSet().add("zset1","f3",90);
System.out.println("size:"+redisTemplate.opsForZSet().size("zset1"));
System.out.println(redisTemplate.opsForZSet().range("zset1",0,-1));
System.out.println(redisTemplate.opsForZSet().rank("zset1","f1"));
}
size:3
[f2, f1, f3]
1
a. score:返回某个元素的分数
Double score(K k, Object o):返回某个元素的分数。
Set<V> rangeByScore(K k, double v, double vl):返回指定分数范围的元素。
Long count(K k, double v, double vl):返回指定分数范围的元素的个数。
@Test
void testZset2(){
redisTemplate.opsForZSet().add("zset2","f1",80);
redisTemplate.opsForZSet().add("zset2","f2",70);
redisTemplate.opsForZSet().add("zset2","f3",90);
redisTemplate.opsForZSet().add("zset2","f4",60);
System.out.println(redisTemplate.opsForZSet().score("zset2","f1"));
System.out.println(redisTemplate.opsForZSet().range("zset2",0,-1));
System.out.println(redisTemplate.opsForZSet().rangeByScore("zset2",70,90));
System.out.println(redisTemplate.opsForZSet().count("zset2",70,90));
}
80.0
[f4, f2, f1, f3]
[f2, f1, f3]
3
b. remove:删除指定元素
@Test
void testZset3(){
redisTemplate.opsForZSet().add("zset3","f1",40);
redisTemplate.opsForZSet().add("zset3","f2",50);
redisTemplate.opsForZSet().add("zset3","f3",60);
redisTemplate.opsForZSet().add("zset3","f4",70);
redisTemplate.opsForZSet().add("zset3","f5",80);
redisTemplate.opsForZSet().add("zset3","f6",90);
redisTemplate.opsForZSet().add("zset3","f7",100);
System.out.println(redisTemplate.opsForZSet().range("zset3",0,-1));
redisTemplate.opsForZSet().remove("zset3","f1");
System.out.println(redisTemplate.opsForZSet().range("zset3",0,-1));
redisTemplate.opsForZSet().removeRange("zset3",0,1);
System.out.println(redisTemplate.opsForZSet().range("zset3",0,-1));
redisTemplate.opsForZSet().removeRangeByScore("zset3",70,80);
System.out.println(redisTemplate.opsForZSet().range("zset3",0,-1));
}
[f1, f2, f3, f4, f5, f6, f7]
[f2, f3, f4, f5, f6, f7]
[f4, f5, f6, f7]
[f6, f7]
如果是降序排列呢?
redisTemplate.opsForZSet().reverseRange("zset3",0,-1)
redis实现分布式session共享
如果服务端做了负载均衡,服务端同时部署了多个服务器,当用户在服务器1上进行了登录,然后下次访问的是服务器2,则有可能不能识别用户的登录状态,这时就要通过共享Session来解决,其思路是将Session放在多个服务器之外的共同的Redis中,让多个服务器共享。
a. 依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<!-- <version>2.5.0</version>-->
</dependency>
b. 启动类添加@EnableRedisHttpSession注解。
c. 创建控制器
d. 多环境配置
删除application.properties文件,
创建application-dev.properties文件表示开发环境,配置如下:
server.port=8081
创建application-prod.properties文件表示生产环境,配置如下:
server.port=8082
e. 运行测试
先打包项目,然后切换到jar包所在目录的命令提示符,运行两个服务器端程序,模拟两个不同的服务器,分别输入两条命令如下:
服务器安装jdk17
wget https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.tar.gz
配置环境变量
export JAVA_HOME=/path/to/jdk17
export PATH=$PATH:$JAVA_HOME/bin
使其生效
source /etc/profile
nohup java -jar redisSession-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev &
nohup java -jar redisSession-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod &
#&表示让项目在后台运行
java -jar xxx.jar &
#nohup表示当窗口(xshell)关闭时服务不挂起,继续在后台运行
nohup java -jar xxx.jar &
nohup java -jar -Dserver.port=8088 xxx.jar &
也可以换个思路解决
复制一个出来,相当于集群,两个项目源码是一模一样的,只是激活的profiles不一样(一个是dev的,一个是prop的)
f. 配置Nginx
浏览器访问localhost:80 时,并多次刷新,发现将轮流实际访问127.0.0.1:8081和127.0.0.1:8082,实现了反向代理和负载均衡的功能
新闻阅读与点赞次数实战
实战说明:首页是新闻列表,点击任一标题,可以进入该新闻详情页面,详情页面显示该条新闻阅读次数,点赞次数,初次进入阅读次数为1,点赞数为0,提供一个按钮可以点赞,点赞完后点赞数立即更新,刷新页面,阅读次数和点赞次数都刷新。阅读和点赞次数都保存到redis内存数据库中,速度快,但每隔一分钟都将最新数据持久化保存到mysql数据库中进行持久化。
考虑用redis自增自减
自增(increment):使用opsForHash().increment(key, hashKey, delta)方法来对指定的hash字段进行自增操作。其中,key表示存储在Redis中的键名称; hashKey表示要自增的字段名称; delta表示每次自增的值。如果该字段不存在或者为空,则会将其初始化为0后再进行自增操作。
a. 添加依赖
<!--quartz相关依赖 -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
</dependency>
b. 创建NewsController控制器
主要提供访问首页的功能,查看新闻详情的功能,查看详情时把该新闻id存储在redis中包含阅读次数和点赞次数的hash类型数据读取出来,阅读次数还要加1,存入model域供前端展示,并更新到redis。还有点赞功能,从redis中提取数据,然后点赞次数加1
@Controller
public class NewsController {
@Autowired
RedisTemplate redisTemplate;
//更新浏览量
@GetMapping("/detail/{id}")
public String see(@PathVariable Integer id, Model model){
//阅读量自增1
redisTemplate.opsForHash().increment("news_" + id, "read", 1);
//查询出数据放到model中,方便前端页面展示点赞量和阅读量
Map map = redisTemplate.opsForHash().entries("news_" + id);
model.addAllAttributes(map);
return "news"+id;
}
//点赞功能
@RequestMapping("/addZan/{id}/{opType}")
@ResponseBody
public Long addZan(@PathVariable Integer id ,@PathVariable Integer opType){
Long countZan;
if (opType==1) {//点赞
countZan = redisTemplate.opsForHash().increment("news_" + id, "countZan", 1);
}else{//取消点赞
countZan=redisTemplate.opsForHash().increment("news_"+id,"countZan",-1);
}
return countZan;
}
}
c. 定时任务
Redis中的数据每隔一分钟要保存到数据库用到Spring的定时任务,
创建NewJob类,创建SaveNewsData方法,在方法前面添加@Scheduled注解实现每隔一分钟执行该方法功能,此外启动类上面要加上@EnableScheduling注解,表示开启定时任务
@Slf4j
@Component
public class NewsJob {
@Autowired
private RedisTemplate redisTemplate;
@Scheduled(cron = "0 0/1 * * * ? ")
public void saveNew(){
Set<String> keys = redisTemplate.keys("news_*");
for(String key:keys){
Map map=redisTemplate.opsForHash().entries(key);
String id=key.substring(5);//获取key中包含的id号,key是news_+id的格式
//然后就可调用业务层将这些数据update到数据库,自行实现。
log.info("编号:"+id+",阅读次数:"+map.get("read")+",点赞次数:"+map.get("countZan"));
}
log.info("以上数据已保存到数据库");
}
}
补充:cron表达式(七子表达式)
d. 创建视图
前端页面
<h1>新闻列表</h1>
<ul>
<li>
<a th:href="@{/detail/1}">新闻1</a>
</li>
<li>
<a th:href="@{/detail/2}">新闻2</a>
</li>
</ul>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>warmer-redis</title>
<script type="text/javascript" src="/js/jquery-1.12.4.js"></script>
<script>
function zan(id,opType){
$.ajax({
url:"/addZan/"+id+"/"+opType,
type:"get",
success:function(data){
$("#countZan").text(data);
}
});
}
</script>
</head>
<body>
<h1 style="text-align: center">新闻1</h1>
阅读数<font color="red" th:text="${read}" ></font> 点赞数<font id="countZan" color="red" th:text="${countZan==null?0:countZan}"></font>
<input type="button" onclick="zan(1,1)" value="点赞"/>
<input type="button" onclick="zan(1,0)" value="取消点赞"/>
</body>
</html>
e. 测试
浏览新闻2,结果类似。此外观察控制台,每隔一分钟会提示最新数据保存到数据库
使用RedisRepository访问redis
Spring Data推出了操作Redis的RedisRespository方式,提供类似Spring Data JPA一样的接口,使得操作Redis变得非常简单。只要在接口中继承CrudRepository接口,就能跟Spring Data JPA一样,其本的增删改操作不需要定义任何方法。
a. 加@RedisHash注解
注意必须要加@RedisHash注解,用于指定操作实体类对象在Redis数据库中的存储空间,此处表示针对Book实体类的数据操作都存储在Redis数据库中名为books的存储空间下
@Data
@AllArgsConstructor
@NoArgsConstructor
//@RedisHash(value = "bookinfo",timeToLive = 60)
@RedisHash(value = "bookinfo")
public class BookInfo {
@Id
private int id;
@Indexed
private String name;
private String category;
private String author;
private double price;
}
b. dao层
@Repository
public interface BookInfoDao extends CrudRepository<BookInfo,Integer> {
Iterable<BookInfo> findByName(String name);
}
c. 测试
@SpringBootTest
public class RedisRepository {
@Autowired
BookInfoDao bookInfoDao;
@Test //添加图书
void testSaveBook() {
BookInfo book1=new BookInfo(1,"西游记","文学","吴承恩",88);
bookInfoDao.save(book1);
BookInfo book2=new BookInfo(2,"三国演义","文学","罗贯中",78);
bookInfoDao.save(book2);
BookInfo book3=new BookInfo(3,"水浒传","文学","施耐俺",68);
bookInfoDao.save(book3);
BookInfo book4=new BookInfo(4,"三国志","文学","佚名",78);
bookInfoDao.save(book4);
System.out.println("添加成功!");
}
@Test //查询所有图书
void testFindAllBooks() {
Iterable<BookInfo> iterable=bookInfoDao.findAll();
Iterator<BookInfo> it=iterable.iterator();
while(it.hasNext()){
BookInfo book= it.next();
System.out.println(book);
}
}
@Test //根据Id号图书
void testFindBookById() {
BookInfo book=bookInfoDao.findById(1).get();
System.out.println(book);
}
@Test //根据Name查询图书
void testFindBooksByName() {
Iterable<BookInfo> iterable=bookInfoDao.findByName("三国志");
Iterator<BookInfo> it=iterable.iterator();
while(it.hasNext()){
BookInfo book= it.next();
System.out.println(book);
}
}
@Test //修改图书
void testUpdateBook() {
BookInfo book=bookInfoDao.findById(1).get();
book.setPrice(98);
bookInfoDao.save(book);
System.out.println("修改成功!");
}
@Test //删除图书
void testDeleteBook() {
bookInfoDao.deleteById(2);
System.out.println("删除成功!");
}
}
Linux服务器里redis安装和启动
1、解压安装包
2、进入到安装目录
3、make编译或者 make MALLOC=libc
如果报错的话,检查一下是否安装gcc
4、修改redis的配置文件redis.conf,
- 确保宿主机能连接到虚拟主机里的redis
本机redis客户端连接虚拟主机里的Redis,需要修改下redis的配置文件,bind 默认是127.0.0.1要将其改为0.0.0.0
- 修改成密码启动,
- 设置以后台的形式运行
5、启动redis
进入redis安装目录的src下输入./redis-server ../redis.conf启动redis
搭建redis集群
什么是redis集群呢?
Redis3.0加入了Redis的集群模式,实现了数据的分布式存储,对数据进行分片,将不同的数据存储在不同的master节点上面,从而解决了海量数据的存储问题。 Redis集群采用去中心化的思想,没有中心节点的说法,对于客户端来说,整个集群可以看成一个整体,可以连接任意一个节点进行操作,就像操作单一Redis实例一样,不需要任何代理中间件,当客户端操作的key没有分配到该node上时,Redis会返回转向指令,指向正确的node。
首先在Linux中搭建好Redis集群,
redis的三种集群方式
redis有三种集群方式:主从复制,哨兵模式和集群
1.主从复制
- 主从复制原理:
●从服务器连接主服务器,发送SYNC命令;
●主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
●主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期问继续记录被执行的写命令;
●从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
●主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令; .
●从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令; (从服务器初始化完成)
●主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令(从服务器初始化完成后的操作)
- 主从复制优缺点:
优点:
●为了分载Master的读操作压力, Slave服务器可以为客户端提供只读操作的服务,写服务仍然必须由Master来完成
●Slave同样可以接受其它Slaves的连接和同步请求 ,这样可以有效的分载Master的同步压力。
●支持主从复制,主机会自动将数据同步到从机,可以进行读写分离
● Master Server是以非阻塞的方式为Slaves提供服务。所以在Master-Slave同步期间,客户端仍然可以提交查询或修改请求。
●Slave Server同样是以非阻塞的方式完成数据同步。在同步期间,如果有客户端提交查询请求,Redis则返回同步之前的数据
缺点:
●Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写清求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
●主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题 ,降低了系统的可用性。
●Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。
2.哨兵模式
当主服务器中断服务后,可以将一个从服务器升级为主服务器,以便继续提供服务,但是这个过程需要人工手动来操作。为此, Redis2.8中提供了哨兵工具来实现自动化的系统监控和故障恢复功能。
哨兵的作用就是监控Redis系统的运行状况。它的功能包括以下两个。
(1)监控主服务器和从服务器是否正常运行,
(2)主服务器出现故障时自动将从服务器转换为主服务器。
- 哨兵模式的优缺点
优点:
●哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。
●主从可以自动切换,系统更健壮,可用性更高。
缺点:
●Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。
3.Redis-Cluster集群
redis的哨兵模式基本已经可以实现高可用,读写分离, 但是在这种模式下每台redis服务器都存储相同的数据,很浪费内存,所以在redis3.0上加入了cluster模式,实现的redis的分布式存储,也就是说每台redis节点上存储不同的内容。
Redis-Cluster采用无中心结构,它的特点如下:
●所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.
●节点的fail是通过集群中超过半数的节点检测失效时才生效。
●客户端与redis节点直连,不需要中间代理层,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
工作方式:
在redis的每一个节点上, 都有这么两个东西, 一个是插槽(slot) ,它的的取值范围是:0-16383, 还有一个就是cluster ,可以理解为是-个集群管理的播件。当我们的存取的key到达的时候, redis会根据crc16的算法得出一个结果, 然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。
为了保证高可用, redis-cluster体群引入了主从模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点。当其它主节点ping一个主节点A时,如果半数以上的主节点与A通信超时,那么认为主节点A宕机了。如果主节点A和它的从节点A1都宕机了,那么该集群就无法再提供服务了。
开始搭建-redis版本5.0.9
在redis的安装目录下创建一个存放集群的文件夹,比如我建的是rediscluster
mkdir -p rediscluster
接着进入到rediscluster这个文件夹中创建7000——7005这六个文件夹,用来模拟集群中6个节点
mkdir -p 7000 7001 7002 7003 7004 7005
然后,把redis安装目录下的redis.conf分别复制进刚创建的6个文件夹中。(建议用最原始的redis.conf,而不是改动过的,因为要改的地方比较多,所以干脆直接在redis.conf里追加)
修改每个节点里的redis.conf,参考下面的,直接复制粘贴进去后,改改端口就行,比如有的是7000,有的端口是7001,还有那个bind的ip,改成你自己的
bind 192.168.56.15
protected-mode no
port 7000
daemonize yes
pidfile /var/run/redis_7000.pid
dir /opt/software/redis-5.0.9/rediscluster/7000/
appendonly yes
cluster-enabled yes
cluster-config-file nodes-7000.conf
cluster-node-timeout 5000
masterauth 123456
requirepass 123456
接着就是启动6个redis服务器
./redis-server ../rediscluster/7000/redis.conf
接着启动 Redis Cluster 集群
./redis-cli --cluster create 192.168.56.15:7000 192.168.56.15:7001 192.168.56.15:7002 192.168.56.15:7003 192.168.56.15:7004 192.168.56.15:7005 --cluster-replicas 1 -a 123456
然后就是验证集群的高可用性,
可以用以下命令让7000端口的redis宕机,看其他redis节点还能不能正常访问,正常存值取值。
./redis-cli -h 192.168.56.15 -a 123456 -c -p 7000 shutdown
查看集群中所有节点的情况,
cluster nodes
刚开始6个节点都是启动状态,7000,7001,7002是从机,7003,7004,7005是主机
干掉了从机 7000
干掉主机7005
7002变成了主机
然后干掉7001
再干到7002
然后就不行了
springboot访问Redis集群
data-redis依赖啥的要有。然后在application.yml里配置redis相关
# Redis连接信息
#spring.data.redis.host=192.168.56.15
spring.data.redis.database=0
spring.data.redis.cluster.nodes=192.168.56.15:7000,192.168.56.15:7001,192.168.56.15:7002,192.168.56.15:7003,192.168.56.15:7004,192.168.56.15:7005
spring.data.redis.password=123456
# Redis连接池设置
spring.data.redis.pool.max-active=8 # 最大活动连接数
spring.data.redis.pool.max-idle=8 # 最大空闲连接数
spring.data.redis.pool.min-idle=0 # 最小空闲连接数
spring.data.redis.timeout=3000
单元测试
@SpringBootTest
public class RedisCluster {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
void test(){
stringRedisTemplate.opsForValue().set("redis_cluster","hello");
System.out.println(stringRedisTemplate.opsForValue().get("redis_cluster"));
}
}
然后把6个redis服务器中宕机几个,看看还能不能正常存数据取数据
分布式锁
普通业务减库存操作
@RestController
public class StockController {
@Autowired
StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deduct_stock")
public String deductStock(){
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock >0){
stock--;
stringRedisTemplate.opsForValue().set("stock",stock+"");
System.out.println("扣减成功,剩余库存:"+stock);
}else{
System.out.println("扣减失败,库存不足");
}
return "end";
}
}
存在超卖问题-单体项目解决
高并发场景下库存出现超卖情况,那么采用同步加锁的方式
@RequestMapping("/deduct_stock")
public String deductStock(){
synchronized (this){
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock >0){
stock--;
stringRedisTemplate.opsForValue().set("stock",stock+"");
System.out.println("扣减成功,剩余库存:"+stock);
}else{
System.out.println("扣减失败,库存不足");
}
}
return "end";
}
分布式集群项目解决
如果是分布式项目,集群项目的话,sync同步是JVM进程级别锁,在集群环境下,每一个tomcat就是一个JVM进程,此时将不起作用
使用redis实现分布式锁,setnx命令,springboot中使用setIfAbsent命令,返回boolean类型,如果是true则表示set值成功
在Redis中,`setnx`是一个命令,用于设置一个键值对,但只有在键不存在时才会设置成功。如果键已经存在,则不会进行任何操作。
具体来说,`setnx`命令的语法如下:
setnx key value
其中,`key`是要设置的键的名称,`value`是要设置的值。如果键不存在,则会将`key`和`value`关联起来,并返回1。如果键已经存在,则不会进行任何操作,并返回0。
`setnx`命令通常用于实现分布式锁。在使用`setnx`命令实现分布式锁时,多个客户端可以同时尝试获取锁,只有一个客户端能够获取到锁,并且在获取到锁的过程中,其他客户端无法获取到锁。这种方式可以确保在分布式系统中,只有一个客户端可以访问共享资源,从而避免了竞态条件和死锁等问题。
当第一个进程进来接口,先set一把锁,下一个进程进来后,执行同样的命令那么会返回false。就直接返回错误!但依然存在问题!
@RequestMapping("/deduct_stock")
public String deductStock(){
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lockKey", "jf3q");
if (!result) {
return "error";
}
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock >0){
stock--;
stringRedisTemplate.opsForValue().set("stock",stock+"");
System.out.println("扣减成功,剩余库存:"+stock);
}else{
System.out.println("扣减失败,库存不足");
}
return "end";
}
解决第一个问题,当代码正常执行完了之后就要释放锁!但是还有问题!
@RequestMapping("/deduct_stock")
public String deductStock(){
String lockKey="product_001";
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "jf3q");
if (!result) {
return "error";
}
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock >0){
stock--;
stringRedisTemplate.opsForValue().set("stock",stock+"");
System.out.println("扣减成功,剩余库存:"+stock);
}else{
System.out.println("扣减失败,库存不足");
}
//释放锁
stringRedisTemplate.delete(lockKey);
return "end";
}
解决第二个问题,加锁后抛了异常造成锁释放的代码没有执行,就会出现死锁使用finally模块来释放锁。但还是有问题!
@RequestMapping("/deduct_stock")
public String deductStock(){
String lockKey="product_001";
try {
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "jf3q");
if (!result) {
return "error";
}
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock >0){
stock--;
stringRedisTemplate.opsForValue().set("stock",stock+"");
System.out.println("扣减成功,剩余库存:"+stock);
}else{
System.out.println("扣减失败,库存不足");
}
} finally {
//释放锁
stringRedisTemplate.delete(lockKey);
}
return "end";
}
解决第三个问题,如果在执行减库存的业务逻辑时,服务器宕机了,或者kill-9杀了服务器进程,finally模块还是不会执行,依然会造成死锁,那么给这个锁加一个自动过期时间,比如10秒钟之后自动释放!但还是有问题!
解决第四个问题,如果在24,25行代码执行中间出现了异常,那么失效时间设置没有成功,则采用合并命令,让他们同时执行要不都成功,要不都失败!但还是有问题!
第5个问题,高并发场景下,可能会出现当前线程未执行完毕,但锁已经失效,然后当线程执行完了之后删除锁的时候是删除的下一个线程加的锁。
解决第五个问题,给锁的value值设置一个UUID随机数,删除锁之前判断一下,是自己的锁,才去删除!但还是有问题,如果业务没有执行完,锁就超时失效了,怎么办?
解决第六个问题,当主线程设置锁成功后,马上开启分线程,让分线程写定时任务去查询当前锁是否存在,如果存在的话,给他再蓄力10S过期失效时间,直到他被删除为止。就证明程序执行完毕。一般定时任务为过期时间的3分之1
最终解决方案:使用redisson客户端添加redisson依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.1</version>
</dependency>
初始化redisson连接
@Bean
public Redisson redisson(){
Config config=new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0).setPassword("123456");
return (Redisson)Redisson.create(config);
}
redisson客户端,lock方法上锁之后,其他的进程进来后会阻塞,不会往下继续执行,当主线程执行完了之后才会继续往下执行。并且会自动延迟锁的时间。
@Autowired
Redisson redisson;
@RequestMapping("/deduct_stock")
public String deductStock(){
String lockKey="product_001";
String clientId = UUID.randomUUID().toString();
RLock redissonLock = redisson.getLock(lockKey);
try {
// Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId,10, TimeUnit.SECONDS);
// if (!result) {
// return "error";
// }
redissonLock.lock(30,TimeUnit.SECONDS);
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock >0){
stock--;
stringRedisTemplate.opsForValue().set("stock",stock+"");
System.out.println("扣减成功,剩余库存:"+stock);
}else{
System.out.println("扣减失败,库存不足");
}
} finally {
redissonLock.unlock();
// if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
// //释放锁
// stringRedisTemplate.delete(lockKey);
// }
}
return "end";
}
redisson逻辑结构图,如何提高分布式锁性能以及redisson架构中redis集群主节点宕机了,但锁还没来得及同步到从节点,从节点就升为主节点后没有锁了!(分段存储商品库存,redlock和zookeeper)
好博客就要一起分享哦!分享海报