一、Redis介绍
Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些数据类型
都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
1.数据类型
其结构与普通的Key-Value结构不同,Redis的Key支持灵活的数据结构
- strings(字符串)
- lists(字符串列表)
- sets(字符串集合)
- sorted sets(有序字符串集合)
- hashes(哈希)等结构
正是这些灵活的数据结构,丰富了Redis的应用场景,能满足更多业务上的灵活存储需求。
2.持久化
通常,Redis将数据存储于内存中,或被配置为使用虚拟内存。通过两种方式可以实现数据持久化:使用截图的方式,将内存中的数据不断写入磁盘;或使用类似MySQL的日志方式,记录每次更新的日志。前者性能较高,但是可能会引起一定程度的数据丢失;后者相反。
3.主从同步
Redis支持将数据同步到多台从库上,这种特性对提高读取性能非常有益。
4.性能
相比需要依赖磁盘记录每个更新的数据库,基于内存的特性无疑给Redis带来了非常优秀的性能。读写操作之间有显著的性能差异。
5.适用场合
取最新的n个数据
排行榜
设置过期时间
计数器
缓存
二、Redis的安装
安装redis比较简单,这里简单说一下。
1.Linux环境下安装redis
首先先安装c编译器
yum -y install gcc
下载redis,先移动到安装目录,我这里在/usr/local
cd /usr/local
wget http://download.redis.io/releases/redis-5.0.4.tar.gz
解压并重命名
tar -xvf redis-5.0.4.tar.gz
mv redis-5.0.4 redis
进入redis目录,编译
cd redis
make
修改redis.conf
bind 127.0.0.1 将此行注释,去掉绑定本机IP,让其它机器访问
protected mode yes 修改此行,yes改为no
进入到src目录,启动redis服务器
./redis-server
再打开一个连接窗口,再次进入src目录后,启动redis客户端
./redis-cli
2.Windows环境下安装redis
windows安装redis就更简单了,网上下载redis的压缩包,解压到任意一个目录下,双击redis-server.exe即启动redis服务器。
三、各数据类型的基本操作
1.string
1)set
设置key对应的值为string类型的value。
127.0.0.1:6379> set name zhangsan
OK
127.0.0.1:6379> set age 20
OK
127.0.0.1:6379> set gender male
OK
2)get
获取key对应的string值,如果key不存在返回nil。
127.0.0.1:6379> get name
"zhangsan"
127.0.0.1:6379> get age
"20"
127.0.0.1:6379> get gender
"male"127.0.0.1:6379> get address
(nil)127.0.0.1:6379> set address wuhan
OK
127.0.0.1:6379> get address
"wuhan"
3)getset
设置key的值,并返回key的旧值。若key不存在,则返回nil
127.0.0.1:6379> get name
"zhangsan"
127.0.0.1:6379> getset name lisi
"zhangsan"
127.0.0.1:6379> get name
"lisi"127.0.0.1:6379> getset name1 zhangsan
(nil)
4)set ... EX
设置key-value的有效时间。单位为秒,过期后再次get后返回nil
127.0.0.1:6379> set name zhangsan EX 10
OK
127.0.0.1:6379> get name
"zhangsan"127.0.0.1:6379> get name
(nil)
5)set ... NX
设置key对应的值为string类型的value。如果key已经存在,则不保存。
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> set name zhangsan NX
OK
127.0.0.1:6379> get name
"zhangsan"127.0.0.1:6379> set name lisi NX
(nil)
127.0.0.1:6379> get name
"zhangsan"
6)del
删除Key-Value。
127.0.0.1:6379> get name
"zhangsan"
127.0.0.1:6379> del name
(integer) 1
127.0.0.1:6379> get name
(nil)
7)incr
对key的值做加加操作,并返回新的值。注意incr一个不是int的value会返回错误,incr一个不存在的key,则设置key为1。
127.0.0.1:6379> get age
"20"
127.0.0.1:6379> incr age
(integer) 21
127.0.0.1:6379> get age
"21"127.0.0.1:6379> get name
"zhangsan"
127.0.0.1:6379> incr name
(error) ERR value is not an integer or out of range127.0.0.1:6379> get age1
(nil)
127.0.0.1:6379> incr age1
(integer) 1
127.0.0.1:6379> get age1
"1"
8)decr
对key的值做的是减减操作,decr一个不存在key,则设置key为-1
127.0.0.1:6379> get age
"21"
127.0.0.1:6379> decr age
(integer) 20
127.0.0.1:6379> get age
"20"127.0.0.1:6379> get name
"zhangsan"
127.0.0.1:6379> decr name
(error) ERR value is not an integer or out of range127.0.0.1:6379> get age1
(nil)
127.0.0.1:6379> decr age1
(integer) -1
127.0.0.1:6379> get age1
"-1"
9)incrby
同incr类似,加指定值 ,key不存在时候会设置key,并认为原来的value是 0
127.0.0.1:6379> get age
"20"
127.0.0.1:6379> incrby age 5
(integer) 25
127.0.0.1:6379> get age
"25"127.0.0.1:6379> get age1
(nil)
127.0.0.1:6379> incrby age1 5
(integer) 5
127.0.0.1:6379> get age1
"5"
10)decrby
同decr,减指定值。
127.0.0.1:6379> get age
"25"
127.0.0.1:6379> decrby age 5
(integer) 20
127.0.0.1:6379> get age
"20"
127.0.0.1:6379> get age1
(nil)
127.0.0.1:6379> decrby age1 5
(integer) -5
127.0.0.1:6379> get age1
"-5"
2.hash
Redis hash是一个string类型的field和value的映射表。hash特别适合用于存储对象。相较于将对象的每个字段存成单个string类型。将一个对象存储在hash类型中会占用更少的内存,并且可以更方便的存取整个对象。省内存的原因是新建一个hash对象时开始是用zipmap(又称为small hash)来存储的。这个zipmap其实并不是hash table,但是zipmap相比正常的hash实现可以节省不少hash本身需要的一些元数据存储开销。
1)hset
设置hash field为指定值,如果key不存在,则先创建。
2)hget
获取指定的hash field。
3)hgetall
获取某个hash中全部的filed及value。
127.0.0.1:6379> hset student name zhangsan
(integer) 1
127.0.0.1:6379> hget student name
"zhangsan"
127.0.0.1:6379> hset student age 20 gender male address wuhan
(integer) 3
127.0.0.1:6379> hgetall student
1) "name"
2) "zhangsan"
3) "age"
4) "20"
5) "gender"
6) "male"
7) "address"
8) "wuhan"
4)hdel
删除指定的hash field。
127.0.0.1:6379> hdel student address
(integer) 1
127.0.0.1:6379> hgetall student
1) "name"
2) "zhangsan"
3) "age"
4) "20"
5) "gender"
6) "male"
3.list
Redis的list类型其实就是一个每个子元素都是string类型的双向链表。链表的最大长度是(2的32次方)。我们可以通过push,pop操作从链表的头部或者尾部添加删除元素。这使得list既可以用作栈,也可以用作队列。
1)lpush
在key对应list的头部添加字符串元素。
2)rpush
在key对应list的尾部添加字符串元素。
3)lrange
取得key对应list某些值,范围为起始下标与结束下表
127.0.0.1:6379> lpush names zhangsan
(integer) 1
127.0.0.1:6379> lpush names lisi
(integer) 2
127.0.0.1:6379> lpush names wangwu
(integer) 3
127.0.0.1:6379> lrange names 0 2
1) "wangwu"
2) "lisi"
3) "zhangsan"
127.0.0.1:6379> rpush names zhaoliu
(integer) 4
127.0.0.1:6379> rpush names sunqi
(integer) 5
127.0.0.1:6379> lrange names 0 4
1) "wangwu"
2) "lisi"
3) "zhangsan"
4) "zhaoliu"
5) "sunqi"
4)lpop
左出栈
5)rpop
右出栈
127.0.0.1:6379> lrange names 0 4
1) "wangwu"
2) "lisi"
3) "zhangsan"
4) "zhaoliu"
5) "sunqi"
127.0.0.1:6379> lpop names
"wangwu"
127.0.0.1:6379> lpop names
"lisi"
127.0.0.1:6379> lrange names 0 2
1) "zhangsan"
2) "zhaoliu"
3) "sunqi"
127.0.0.1:6379> rpop names
"sunqi"
127.0.0.1:6379> rpop names
"zhaoliu"
127.0.0.1:6379> lrange names 0 0
1) "zhangsan
4.set
Redis的set是string类型的无序集合。set元素最大可以包含(2的32次方)个元素。
set的是通过hash table实现的。hash table会随着添加或者删除自动的调整大小。关于set集合类型除了基本的添加删除操作,其他有用的操作还包含集合的取并集(union),交集(intersection),差集(difference)。
1)sadd
向名称为key的set中添加元素。
2)smembers
查看key中的所有元素
127.0.0.1:6379> sadd students zhangsan
(integer) 1
127.0.0.1:6379> sadd students lisi
(integer) 1
127.0.0.1:6379> sadd students wangwu
(integer) 1
127.0.0.1:6379> smembers students
1) "lisi"
2) "zhangsan"
3) "wangwu"
3)sismember
查询key中是否包含某元素。是返回1,否为0
127.0.0.1:6379> sismember students zhangsan
(integer) 1127.0.0.1:6379> sismember students sunqi
(integer) 0
4)sinter
返回所有给定key的交集。
5)sdiff
返回所有给定key与第一个key的差集。
6)sunion
返回所有给定key的并集。
127.0.0.1:6379> smembers students
1) "zhangsan"
2) "lisi"
3) "wangwu"
127.0.0.1:6379> sadd students1 zhangsan lisi zhaoliu sunqi
(integer) 4
127.0.0.1:6379> smembers students1
1) "zhaoliu"
2) "lisi"
3) "sunqi"
4) "zhangsan"127.0.0.1:6379> sinter students students1
1) "zhangsan"
2) "lisi"127.0.0.1:6379> sdiff students students1
1) "wangwu"
127.0.0.1:6379> sdiff students1 students
1) "zhaoliu"
2) "sunqi"127.0.0.1:6379> sunion students students1
1) "wangwu"
2) "zhaoliu"
3) "zhangsan"
4) "sunqi"
5) "lisi"
5.sorted set(zset)
和set一样sorted set也是string类型元素的集合,不同的是每个元素都会关联一个double类型的score。sorted set的实现是skip list和hash table的混合体。
很多时候,我们都将redis中的有序集合叫做zsets,这是因为在redis中,有序集合相关的操作指令都是以z开头的。
当元素被添加到集合中时,一个元素到score的映射被添加到hash table中,另一个score到元素的映射被添加到skip list,并按照score排序,所以就可以有序的获取集合中的元素。redis的skip list实现用的是双向链表,这样就可以逆序从尾部取元素。sorted set最经常的使用方式应该是作为索引来使用.我们可以把要排序的字段作为score存储,对象的id当元素存储。
1)zadd
向名称为key的zset中添加元素member,score用于排序。如果该元素已经存在,则根据score更新该元素的顺序。
2)zrange
读取数据,start和end是开始和结束位置
127.0.0.1:6379> zadd myzset 1 one
(integer) 1
127.0.0.1:6379> zadd myzset 2 two
(integer) 1
127.0.0.1:6379> zadd myzset 3 two
(integer) 0
127.0.0.1:6379> zrange myzset 0 -1 withscores
1) "one"
2) "1"
3) "two"
4) "3"
四、事务
redis
的事务是一个单独隔离的操作,它会将一系列指令按需排队并顺序执行,期间不会被其他客户端的指令插队。
1.三大指令:
1.mutil:开启事务
2.exec:执行事务
3.discard:取消事务
127.0.0.1:6379> set name1 zhangsan
QUEUED
127.0.0.1:6379> set name2 lisi
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
127.0.0.1:6379> get name1
"zhangsan"
127.0.0.1:6379> get name2
"lisi"
2.事务的错误与回滚
事物的错误与回滚分别有组队时错误和执行命令时错误两种情况
1)组队时错误
我们在组队时输入错误的指令,redis会之间将所有指令都会失效。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name1 zhangsan
QUEUED
127.0.0.1:6379> set name2 lisi
QUEUED
127.0.0.1:6379> set name3
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379> set name4 wangwu
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get name1
(nil)
127.0.0.1:6379> get name2
(nil)
127.0.0.1:6379> get name3
(nil)
127.0.0.1:6379> get name4
(nil)
2)执行时错误
他在按序处理所有指令,遇到错误就按正常流程处理继续执行下去。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name1 zhangsan
QUEUED
127.0.0.1:6379> incr name1
QUEUED
127.0.0.1:6379> set name2 lisi
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379> get name1
"zhangsan"
127.0.0.1:6379> get name2
"lisi"
3.redis的乐观锁
redis
就是通过CAS(check and set)
实现乐观锁的,通过watch
指令监听一个或者多个key值,当用户提交修改key值的事务时,会检查监听的key是否发生变化。若没有发生变化,则提交成功。
首先设置某个key的值,并监听
127.0.0.1:6379> set name zhangsan
OK
127.0.0.1:6379> watch name
OK
127.0.0.1:6379> multi
OK
再新建第二个客户端,同样监听并开启事务
127.0.0.1:6379> watch name
OK
127.0.0.1:6379> multi
OK
回到第一个客户端,修改后提交
127.0.0.1:6379> set name lisi
QUEUED
127.0.0.1:6379> exec
1) OK
到第二个客户端,再次修改后提交
127.0.0.1:6379> set name lisi
QUEUED
127.0.0.1:6379> exec
(nil)
可以看到,第二次提交失败。
五、Redis开发
我们通过springboot整合的Redis来实现使用缓存。使用缓存可以减少数据库的访问频率,提高性能。
适合放入缓存的数据可以是一些热点数据,修改频率较低,安全系数低的数据。
1.原理
2. 编程式缓存
1)首先导入Redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2)在配置文件中添加配置
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=0
spring.redis.jedis.pool.max-active=100
spring.redis.jedis.pool.max-wait=100ms
spring.redis.jedis.pool.max-idle=100
spring.redis.jedis.pool.min-idle=10
3)配置类
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { //创建Redis对象 RedisTemplate<String, Object> template = new RedisTemplate<String, Object>(); //指定连接工厂 template.setConnectionFactory(factory); // 配置值的序列化器 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); // key采用String的序列化方式 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); template.setKeySerializer(stringRedisSerializer); // hash的key也采用String的序列化方式 template.setHashKeySerializer(stringRedisSerializer); // value序列化方式采用jackson template.setValueSerializer(jackson2JsonRedisSerializer); // hash的value序列化方式采用jackson序列化器 template.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; } }
4)service层编写
/** * <p> * 服务实现类 * </p> * * @author Reuben * @since 2023-06-27 */ @Service public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> implements IStudentService { public static final String PREFIX = "Student-"; @Autowired private StudentMapper studentMapper; @Autowired private RedisTemplate<String, Object> redisTemplate; @Override public Student getStudentById(Long studentId) { //获得字符串操作对象 ValueOperations<String, Object> ops = redisTemplate.opsForValue(); //先查询Redis Student stu = (Student) ops.get(PREFIX + studentId); //如果Redis缓存存在数据,就直接返回 if (stu != null) { System.out.println("Redis查到,返回" + stu); return stu; } //如果Redis没有查到数据,就查询MySQL System.out.println("Redis未查到数据,开始查询数据库"); stu= studentMapper.selectById(studentId); if (stu != null) { System.out.println("MySQL查询到数据,返回" + stu); //保存到Redis ops.set(PREFIX + studentId, stu); return stu; } System.out.println("MySQL没查到数据,直接返回null"); return null; } }
3.注解式缓存
编程式缓存虽然自由度高,但是使用复杂,并且代码侵入性高。使用注解式缓存可以降低代码侵入性并且使用简便。
1)在启动类上加注解
//启动缓存
@EnableCaching
2)配置类
@Configuration
public class RedisConfig {@Bean
public RedisCacheConfiguration provideRedisCacheConfiguration(){
//加载默认配置
RedisCacheConfiguration conf = RedisCacheConfiguration.defaultCacheConfig();
//返回Jackson序列化器
return conf.serializeValuesWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
}
}
3)service层使用注解
/** * <p> * 服务实现类 * </p> * * @author Reuben * @since 2023-06-27 */ @Service public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> implements IStudentService { public static final String PREFIX = "Student-"; @Autowired private StudentMapper studentMapper; @Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private RBloomFilter<String> rBloomFilter; private final Object lock = new Object(); @Cacheable(cacheNames = "Student-Page", key = "T(String).valueOf(#page.current) + '-' + T(String).valueOf(#page.size) + '-' + T(String).valueOf(#studentName)") @Override public Page<Student> getStudentAllPage(Page<Student> page, String studentName) { return studentMapper.selectStudentAllPage(page, studentName); } /** * 声明式缓存 * @param studentId * @return */ @Cacheable(cacheNames = "Student", key = "T(String).valueOf(#studentId)") @Override public Student getStudentById(Long studentId) { return studentMapper.selectById(studentId); } @Caching(put = { @CachePut(cacheNames = "Student", key = "T(String).valueOf(#student.studentId)") }, evict = { @CacheEvict(cacheNames = "Student-Page", allEntries = true) }) @CachePut(cacheNames = "Student", key = "T(String).valueOf(#student.studentId)") @Override public Student addStudent(Student student){ studentMapper.insert(student); return student; } @Caching(put = { @CachePut(cacheNames = "Student", key = "T(String).valueOf(#student.studentId)") }, evict = { @CacheEvict(cacheNames = "Student-Page", allEntries = true) }) @Override public Student updateStudentById(Student student) { studentMapper.updateById(student); return student; } @Caching(evict = { @CacheEvict(cacheNames = "Student", key = "T(String).valueOf(#studentId)"), @CacheEvict(cacheNames = "Student-Page", allEntries = true) }) @Override public void removeStudentById(Long studentId) { studentMapper.deleteById(studentId); } }
-
@CacheConfig 使用在Service类上,可以配置缓存名称。
-
@Cacheable 使用在查询方法上,让方法优先查询缓存
-
@CachePut 使用在更新和添加方法上,数据库更新和插入数据后同时保存到缓存里
-
@CacheEvict 使用在删除方法上,数据库删除后同时删除缓存
-
@Caching 组合注解可以同时完成多个操作。