负载均衡场景
解决session问题?
方案:
NoSQL数据库
NoSQL数据库,泛指非关系型数据库
以简单的key-value模式存储
适用场景
不适用的场景
需要事务支持
基于sql的结构化查询存储,处理复杂的关系,需要即席查
用不着sql的和用了sql也不行的情况,请考虑用NoSql
常用的NoSQL数据库
Memcached
很早
出现的NoSQL数据库
数据都在内存中,一般不持久化
支持简单的key-value模式,支持类型单一
一般是作为缓存数据库
辅助持久化的数据库
Redis
几乎覆盖了Memcached的绝大部分功能
数据都存在内存中,支持持久化
,主要用做备份恢复
除了支持简单的key-value模式,还支持多种数据结构的存储
,比如list、set、hash、zset
一般是作为缓存数据库
辅助持久化的数据库
MongoDB
高性能、开源、模式自由的文档型数据库
数据都存在内存中,如果内存不足,会把不常用的数据存到硬盘中
虽然是key-value 模式,但是对value(尤其是json
)提供了丰富的查询功能
支持二进制数据及大型对象
可以根据数据的特点代替RDBMS
,成为独立的数据库,或者配合RDBMS,存储特定的数据
REmote DIctionary Server(Redis) 是一个由 Salvatore Sanfilippo 写的 key-value 存储系统,是跨平台的非关系型数据库。
Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,并提供多种语言的 API。
Redis 通常被称为数据结构服务器,因为值(value)可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等类型。
开源
的key-value
存储系统string
(字符串)、list
(链表)、set
(集合)、 zset
(sorted set --有序集合)和hash
(哈希类型)原子性
的排序
缓存在内存
中周期性
的把更新的数据写入磁盘
或者把修改操作写入追加的记录文件master-slave(主从)
同步Redis
文档
单线程+多路IO复用
简单理解就是:一个服务端进程可以同时处理多个套接字描述符。
多路:多个客户端连接(连接就是套接字描述符)
复用:使用单进程就能够实现同时处理多个客户端的连接
以上是通过增加进程和线程的数量来并发处理多个套接字,免不了上下文切换的开销,而 IO 多路复用只需要一个进程就能够处理多个套接字,从而解决了上下文切换的问题。
其发展可以分 select->poll→epoll 三个阶段来描述。
五大数据类型
keys * #查看数据库中所有的key
exists (key) #查看key是否存在,返回值为key的数量
type (key) #查看key是什么类型
del (key) #删除key,返回删除个数
unlink (key) #根据value选择非阻塞删除,先通知删除该key,后续在删除内存中的key,异步执行
expire (key) (time) #设置key的过期时间,单位秒
ttl (key) #查看key过期时间,-1表示永不过期,-2表示已经过期
select (库序号) #切换库,redis有16个库,默认是0
dbsize #查看当前数据库key的数量,返回个数
flushdb #清除当前库
flushall #清除全部库
String类型是(二进制安全的
),可以包含任何数据
Srting类型是Redis最基本的数据类型,一个Redis中字符串的value最多可以是512M
set <key> <value> #设置一个key,value
get <key> #根据key获取value
append <key> <value> #在对应key的value后面追加数据,返回总长度
strlen <key> #获取值的长度
setnx <key> <value> #设置值,当键存在时不进行设置,键不存在才进行设置
incr <key> #将key中存储的值+1,返回增加后的值,只能对数字值进行操作,如果为空,新增值为1
decr <key> #将key中存储的值-1,返回减少后的值,只能对数字值进行操作,如果为空,新增值为-1
incrby/decrby <key> <步长> #将key中的值进行增减,长度为步长
增减操作都是原子性操作:
Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。
Redis的操作之所以是原子性的,是因为Redis是单线程的。
mset <key1> <value1> <key2> <value2> ... #同时对多对k-v进行赋值
mget <key1> <key2> <key3> ... #同时获取多个value
msetnx <key1> <value1> <key2> <value2> ... #同时设置多对值,当值存在时不进行设置,值不存在才进行设置
msetnx同时设置多对值时,原子性操作,要么都成功要么都不成功
#获取值的范围,类似Java中的substring,前后都为闭区间
getrange <key> <起始位置> <结束位置>
#根据起始位置,将key中的值覆盖为value
setrange <key> <起始位置> <value>
setex <key> <过期时间> <value> #设置k-v的同时设置过期时间,单位s
getset <key> <value> #设置新值的同时,获取旧值
String的底层结构为简单的动态字符串,内部结构类似与Java的ArrayList,才用预分配冗余空间的方式,来减少内存的频繁操作
内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间,需要注意的是字符串最大长度为512M
hash是一个键值对集合
hash是一个string类型的field和value的映射表,hash特别适用于存储对象,类似Java的Map
用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储
主要有以下两种存储方式:
第三种方式:
user为key,属性标签跟属性值是value
,存储方便,值的操作方便
命令
hset <key> <filed> <value> [filed value..] #是hash的键,是value的键,是值,可以批量设置,如果hash的key不存在则创建新的hash,如果key存在则创建失败
hget <key> <filed> #根据filed获取value
hmset <key> <filed> <value> [filed value ..] #可以批量设置hash,如果key存在,filed相同则覆盖对应的value,否则创建一个新的hash
hmget <key> <filed> [filed ..] #批量获取value,如果filed不存在返回nil,存在返回对应的value
hexists <key> <filed> #判断对应的key是否存在filed
hkeys <key> #查询对应的key的所有filed
hvals <key> #查询对应key的所有value
hincrby <key> <field> <increment> #为hash的key中的field的值增加或减少increment
hsetnx <key> <field> <value> #为对应key添加filed和value,只有filed不存在时才会成功
Hash类型对应的数据结构有两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziphash,否则使用hashtable
单键多值
redis列表是简单的字符串列表,按照插入顺序排序,可以插入一个元素到列表的头部(左边)或者尾部(右边)
列表类型内部使用双向链表
实现的,所以向列表两端添加元素的时间复杂度为O(1),获取越接近两端的元素速度越快。但是使用链表的代价是通过索引访问元素比较慢。
命令
命令 | 用法 | 描述 |
---|---|---|
lpush | lpush key value [value …] | (1)将一个或多个值插入到列表key的表头; (2)如果有多个value值,则从左到右的顺序依次插入表头; (3)如果key不存在,则会创建一个空列表,然后执行lpush操作;如果key存在,但不是列表类型,则返回错误。 |
lpushx | lpushx key value | (1)将value值插入到列表key的表头,当且仅当key存在且是一个列表; (2)如果key不存在时,lpushx命令什么都不会做。 |
lpop | lpop key | (1)移除并返回列表key的头元素。 |
lrange | lrange key start stop | (1)返回列表key中指定区间内的元素; (2)start大于列表最大下标是,返回空列表; (3)可使用负数下标,-1表示列表最后一个元素,以此类推。 |
lrem | lrem key count value | (1)count>0表示从头到尾搜索,移除与value相等的元素,数量为count; (2)count<0表示从尾到头搜索,移除与value相等的元素,数量为count; (3)count=0表示移除列表中所有与value相等的元素。 |
lset | lset key index value | (1)将列表key下标为index的元素值设置为value; (2)当index参数超出范围,或对一个空列表进行lset操作时,返回错误。 |
lindex | lindex key index | (1)返回列表key中下标为index的元素。 |
linsert | linsert key BEFORE|AFTER pivot value | (1)将值value插入列表key中,位于pivot前面或者后面; (2)当pivot不存在列表key中,或者key不存在时,不执行任何操作。 |
llen | len key | (1)返回列表key的长度,当key不存在时,返回0。 |
rpop | rpop key | (1)移除并返回列表key的尾元素。 |
rpoplpush | rpoplpush source destination | (1)将列表source中最后一个元素弹出并返回给客户端,并且将该元素插入到列表destincation的头部。 |
rpush | rpush key value [value …] | (1)将一个或多个值插入到列表key的尾部。 |
rpushx | rpushx key value | (1)将value值插入到列表key的表尾,当且仅当key存在且是一个列表; (2)如果key不存在时,lpushx命令什么都不会做。 |
总结:
Redis中的set类型是string类型的无序集合
。集合类型的常用操作是向集合中加入或删除元素、判断某个元素是否存在等,由于集合类型在Redis内部是使用值为空的散列表实现的,所以这些操作的时间复杂度都是O(1)。最方便的是多个集合类型键之间还可以进行并集、交集和差集运算。
命令
命令 | 用法 | 描述 |
---|---|---|
sadd | sadd key member [member …] | (1)将一个或多个member元素加入key中,已存在在集合中的member将被忽略; (2)如果key不存在,则创建一个只包含member元素的集合; (3)当key不是集合类型时,将返回一个错误。 |
scard | scard key | (1)返回key对应的集合中的元素数量。 |
sdiff | sdiff key [key …] | (1)返回所有key对应的集合的差集。 |
sdiffstore | sdiffstore destionation key [key …] | (1)返回所有key对应的集合的差集,并把该差集赋值给destionation; (2)如果destionation已经存在,则直接覆盖。 |
sinter | sinter key [key …] | (1)返回所有key对应的集合的交集; (2)不存在的key被视为空集。 |
sinterstore | sinter destionation key [key …] | (1)返回所有key对应的集合的交集,并把该交集赋值给destionation; (2)如果destionation已经存在,则直接覆盖。 |
sismember | sismember key member | (1)判断member元素是否是key的成员,0表示不是,1表示是。 |
smembers | smember key | (1)返回集合key中的所有成员; (2)不存在的key被视为空集。 |
srem | srem key member [member …] | (1)移除集合key中的一个或多个member元素,不存在的member将被忽略。 |
sunion | sunion key [key …] | (1)返回所有key对应的集合的并集; (2)不存在的key被视为空集。 |
sunionstore | sunionstore destionation key [key …] | (1)返回所有key对应的集合的并集,并把该并集赋值给destionation; (2)如果destionation已经存在,则直接覆盖。 |
zset类型也是string类型元素的集合,但是它是有序
的。
命令 | 用法 | 描述 |
---|---|---|
zadd | zadd key score member [score member …] | (1)将一个或多个member元素及其score值加入集合key中; (2)如果member已经是有序集合的元素,那么更新member对应的score并重新插入member保证member在正确的位置上; (3)score可以是整数也可以是双精度浮点数。 |
zcard | zcard key | (1)返回有序集的元素个数。 |
zcount | zcount key min max | (1)返回有序集key中,score值>=min且<=max的成员数量 |
zrange | zrange key start stop [withscores] | (1)返回有序集key中指定区间内的成员,成员位置按score从小到大排序; (2)如果score值相同,则按字典排序; (3)如果要使成员按score从大到小排序,则使用zrevrange命令。 |
zrank | zrank key number | (1)返回有序集key中成员member的排名,有序集合按score值从小到大排列; (2)zrevrank命令将按照score值从大到小排序。 |
zrem | zrem key member [member …] | (1)移除有序集key中的一个或多个元素,不存在的元素将被忽略; (2)当key存在但不是有序集时,返回错误。 |
zremrangebyrank | zremrangerank key start stop | (1)移除有序集key中指定排名区间内的所有元素。 |
zremrangebyscore | zremrangescore key min max | (1)移除有序集key中所有score值>=min且<=max之间的元素。 |
常用注解
根据方法的请求参数对其结果进行缓存
@Cacheable(value="user",key="#userName")
)@Cacheable(value="user")
或者 @Cacheable(value={"user1","use2"})
)@Cacheable(value = "user", key = "#id",condition = "#id < 10")
)根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用
根据条件对缓存进行清空
@CacheEvict(value = "user", key = "#id", allEntries = true)
)@CacheEvict(value = "user", key = "#id", beforeInvocation = true)
)依赖导入
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
配置文件
spring:
redis:
database: 0
#host: 192.168.190.134
host: 127.0.0.1
port: 6379
#password: root
timeout: 3000
jedis:
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
cache:
type: redis
开启缓存
//在启动类上面开启缓存
@EnableCaching
@SpringBootApplication
public class QueryDslApplication {
public static void main(String[] args) {
SpringApplication.run(QueryDslApplication.class, args);
}
}
使用缓存
@Override
//使用注解来使用缓存
@Cacheable(cacheNames = "emp", key = "#employee_id")
public EmployeeEntity findOne(Long employee_id) {
EmployeeEntity entity = employeeRepository.findOne(employee_id).orElse(null);
return entity;
}
三个注解的使用
package com.atguigu.cache.service;
import com.atguigu.cache.bean.Employee;
import com.atguigu.cache.mapper.EmployeeMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
@CacheConfig(cacheNames="emp"/*,cacheManager = "employeeCacheManager"*/) //抽取缓存的公共配置
@Service
public class EmployeeService {
@Autowired
EmployeeMapper employeeMapper;
/**
* 将方法的运行结果进行缓存;以后再要相同的数据,直接从缓存中获取,不用调用方法;
* CacheManager管理多个Cache组件的,对缓存的真正CRUD操作在Cache组件中,每一个缓存组件有自己唯一一个名字;
*
*
* 原理:
* 1、自动配置类;CacheAutoConfiguration
* 2、缓存的配置类
* org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration
* org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration
* org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration
* org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration
* org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration
* org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration
* org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration
* org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration
* org.springframework.boot.autoconfigure.cache.GuavaCacheConfiguration
* org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration【默认】
* org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration
* 3、哪个配置类默认生效:SimpleCacheConfiguration;
*
* 4、给容器中注册了一个CacheManager:ConcurrentMapCacheManager
* 5、可以获取和创建ConcurrentMapCache类型的缓存组件;他的作用将数据保存在ConcurrentMap中;
*
* 运行流程:
* @Cacheable:
* 1、方法运行之前,先去查询Cache(缓存组件),按照cacheNames指定的名字获取;
* (CacheManager先获取相应的缓存),第一次获取缓存如果没有Cache组件会自动创建。
* 2、去Cache中查找缓存的内容,使用一个key,默认就是方法的参数;
* key是按照某种策略生成的;默认是使用keyGenerator生成的,默认使用SimpleKeyGenerator生成key;
* SimpleKeyGenerator生成key的默认策略;
* 如果没有参数;key=new SimpleKey();
* 如果有一个参数:key=参数的值
* 如果有多个参数:key=new SimpleKey(params);
* 3、没有查到缓存就调用目标方法;
* 4、将目标方法返回的结果,放进缓存中
*
* @Cacheable标注的方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值作为key去查询缓存,
* 如果没有就运行方法并将结果放入缓存;以后再来调用就可以直接使用缓存中的数据;
*
* 核心:
* 1)、使用CacheManager【ConcurrentMapCacheManager】按照名字得到Cache【ConcurrentMapCache】组件
* 2)、key使用keyGenerator生成的,默认是SimpleKeyGenerator
*
*
* 几个属性:
* cacheNames/value:指定缓存组件的名字;将方法的返回结果放在哪个缓存中,是数组的方式,可以指定多个缓存;
*
* key:缓存数据使用的key;可以用它来指定。默认是使用方法参数的值 1-方法的返回值
* 编写SpEL; #i d;参数id的值 #a0 #p0 #root.args[0]
* getEmp[2]
*
* keyGenerator:key的生成器;可以自己指定key的生成器的组件id
* key/keyGenerator:二选一使用;
*
*
* cacheManager:指定缓存管理器;或者cacheResolver指定获取解析器
*
* condition:指定符合条件的情况下才缓存;
* ,condition = "#id>0"
* condition = "#a0>1":第一个参数的值》1的时候才进行缓存
*
* unless:否定缓存;当unless指定的条件为true,方法的返回值就不会被缓存;可以获取到结果进行判断
* unless = "#result == null"
* unless = "#a0==2":如果第一个参数的值是2,结果不缓存;
* sync:是否使用异步模式
* @param id
* @return
*
*/
@Cacheable(value = {"emp"}/*,keyGenerator = "myKeyGenerator",condition = "#a0>1",unless = "#a0==2"*/)
public Employee getEmp(Integer id){
System.out.println("查询"+id+"号员工");
Employee emp = employeeMapper.getEmpById(id);
return emp;
}
/**
* @CachePut:既调用方法,又更新缓存数据;同步更新缓存
* 修改了数据库的某个数据,同时更新缓存;
* 运行时机:
* 1、先调用目标方法
* 2、将目标方法的结果缓存起来
*
* 测试步骤:
* 1、查询1号员工;查到的结果会放在缓存中;
* key:1 value:lastName:张三
* 2、以后查询还是之前的结果
* 3、更新1号员工;【lastName:zhangsan;gender:0】
* 将方法的返回值也放进缓存了;
* key:传入的employee对象 值:返回的employee对象;
* 4、查询1号员工?
* 应该是更新后的员工;
* key = "#employee.id":使用传入的参数的员工id;
* key = "#result.id":使用返回后的id
* @Cacheable的key是不能用#result
* 为什么是没更新前的?【1号员工没有在缓存中更新】
*
*/
@CachePut(/*value = "emp",*/key = "#result.id")
public Employee updateEmp(Employee employee){
System.out.println("updateEmp:"+employee);
employeeMapper.updateEmp(employee);
return employee;
}
/**
* @CacheEvict:缓存清除
* key:指定要清除的数据
* allEntries = true:指定清除这个缓存中所有的数据
* beforeInvocation = false:缓存的清除是否在方法之前执行
* 默认代表缓存清除操作是在方法执行之后执行;如果出现异常缓存就不会清除
*
* beforeInvocation = true:
* 代表清除缓存操作是在方法运行之前执行,无论方法是否出现异常,缓存都清除
*
*
*/
@CacheEvict(value="emp",beforeInvocation = true/*key = "#id",*/)
public void deleteEmp(Integer id){
System.out.println("deleteEmp:"+id);
//employeeMapper.deleteEmpById(id);
int i = 10/0;
}
// @Caching 定义复杂的缓存规则
@Caching(
cacheable = {
@Cacheable(/*value="emp",*/key = "#lastName")
},
put = {
@CachePut(/*value="emp",*/key = "#result.id"),
@CachePut(/*value="emp",*/key = "#result.email")
}
)
public Employee getEmpByLastName(String lastName){
return employeeMapper.getEmpByLastName(lastName);
}
}
RedisAutoConfiguration
//操作对象,Cacheable默认实现的是redisTemplate,我们重写redisTemplate就可以实现自己的Cacheable
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
自定义KeyGenerator
使用redis中的setnx API,当key存在时不能设置value
配置环境
server:
port: 8080
spring:
redis:
host: 192.168.190.137
port: 6379
database: 0
timeout: 6000
password: root
cache:
type: redis
开启缓存
Github文档
官网
Redisson是Redis服务器上的分布式可伸缩Java数据结构----驻内存数据网格(In-Memory Data Grid,IMDG)。底层使用netty框架,并
提供了与java对象相对应的分布式对象、分布式集合、分布式锁和同步器、分布式服务等一系列的Redisson的分布式对象。
导入依赖
<dependency>
<groupId>org.redissongroupId>
<artifactId>redisson-spring-boot-starterartifactId>
<version>3.15.6version>
dependency>
配置redisson.yml
singleServerConfig:
idleConnectionTimeout: 10000
#pingTimeout: 1000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
#reconnectionTimeout: 3000
#failedAttempts: 3
password: root
subscriptionsPerConnection: 5
clientName: null
address: "redis://192.168.190.138:6379"
subscriptionConnectionMinimumIdleSize: 1
subscriptionConnectionPoolSize: 50
connectionMinimumIdleSize: 32
connectionPoolSize: 64
database: 0
#dnsMonitoring: false
dnsMonitoringInterval: 5000
threads: 0
nettyThreads: 0
codec: ! {}
transportMode : "NIO"
添加配置类
package com.du.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import java.io.IOException;
/**
* @author Du
*/
@Configuration
public class RedissonConfig {
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
//两种方式
//1、导入YAML文件
RedissonClient redisson = Redisson.create(
Config.fromYAML(new ClassPathResource("redisson-single.yml").getInputStream()));
return redisson;
//2、创建Config类,配置参数
Config config = new Config();
config.useSingleServer()
.setAddress("redis://192.168.190.140:6379")
.setDatabase(0)
.setPassword("root");
config.setLockWatchdogTimeout(15000);
return Redisson.create(config);
}
}
测试
@Autowired
RedissonClient redissonClient;
@PostMapping("/redissonlock")
public String redissonlock(@RequestParam String name) {
// 加锁,添加key
String lock = name + "-lock";
RLock rLock = redissonClient.getLock(lock);
try {
rLock.lock();
int apple = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get(name)));
if (apple > 0) {
apple--;
System.out.println("---------扣减成功," + name + "库存:" + apple);
stringRedisTemplate.opsForValue().set("apple", String.valueOf(apple));
} else {
System.out.println("---------库存不足,扣减失败");
}
} finally {
// 解锁,删除key
rLock.unlock();
}
return "";
}
看门狗,redisson不指定leaseTime,默认创建一个30秒的看门狗。
核心源码
// 直接使用lock无参数方法
public void lock() {
try {
lock(-1, null, false);
} catch (InterruptedException e) {
throw new IllegalStateException();
}
}
// 进入该方法 其中leaseTime = -1
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return;
}
//...
}
// 进入 tryAcquire(-1, leaseTime, unit, threadId)
private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}
// 进入 tryAcquireAsync
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
if (leaseTime != -1) {
return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
//当leaseTime = -1 时 启动 watch dog机制
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
//执行完lua脚本后的回调
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
if (ttlRemaining == null) {
// watch dog
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
scheduleExpirationRenewal 方法开启监控:
private void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
//将线程放入缓存中
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
//第二次获得锁后 不会进行延期操作
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
// 第一次获得锁 延期操作
renewExpiration();
}
}
// 进入 renewExpiration()
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
//如果缓存不存在,那不再锁续期
if (ee == null) {
return;
}
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
//执行lua 进行续期
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
if (res) {
//延期成功,继续循环操作
renewExpiration();
}
});
}
//每隔internalLockLeaseTime/3=10秒检查一次
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
//lua脚本 执行包装好的lua脚本进行key续期
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getName()),
internalLockLeaseTime, getLockName(threadId));
}
看到3的时候,可能会有人有疑问,如果释放锁操作本身异常了,watch dog 不会继续续期
// 锁释放
public void unlock() {
try {
get(unlockAsync(Thread.currentThread().getId()));
} catch (RedisException e) {
if (e.getCause() instanceof IllegalMonitorStateException) {
throw (IllegalMonitorStateException) e.getCause();
} else {
throw e;
}
}
}
// 进入 unlockAsync(Thread.currentThread().getId()) 方法 入参是当前线程的id
public RFuture<Void> unlockAsync(long threadId) {
RPromise<Void> result = new RedissonPromise<Void>();
//执行lua脚本 删除key
RFuture<Boolean> future = unlockInnerAsync(threadId);
future.onComplete((opStatus, e) -> {
// 无论执行lua脚本是否成功 执行cancelExpirationRenewal(threadId) 方法来删除EXPIRATION_RENEWAL_MAP中的缓存
cancelExpirationRenewal(threadId);
if (e != null) {
result.tryFailure(e);
return;
}
if (opStatus == null) {
IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
+ id + " thread-id: " + threadId);
result.tryFailure(cause);
return;
}
result.trySuccess(null);
});
return result;
}
// 此方法会停止 watch dog 机制
void cancelExpirationRenewal(Long threadId) {
ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (task == null) {
return;
}
if (threadId != null) {
task.removeThreadId(threadId);
}
if (threadId == null || task.hasNoThreads()) {
Timeout timeout = task.getTimeout();
if (timeout != null) {
timeout.cancel();
}
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
}
}
释放锁的操作中 有一步操作是从 EXPIRATION_RENEWAL_MAP 中获取 ExpirationEntry 对象,然后将其remove,结合watch dog中的续期前的判断:
EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
可以得出结论:
如果释放锁操作本身异常了,watch dog 还会不停的续期吗?不会,因为无论释放锁操作是否成功,EXPIRATION_RENEWAL_MAP中的目标 ExpirationEntry 对象已经被移除了,watch dog 通过判断后就不会继续给锁续期了。
每一个Redisson对象都有一个Redis数据实例相对应。
通用对象桶
Redisson分布式对象RBucket,可以存放任意类型对象
二进制流
Redisson分布式对象RBinaryStream,InputStream和OutoutStream接口实现
地理空间对象桶
Reddisson分布式RGo,储存于地理位置有关的对象桶
BitSet
Reddisson分布式RBitSet,是分布式的可伸缩位向量通过实现RClusteredBitSet接口,可以在集群环境下数据分片
布隆过滤器
Reddisson利用Redis实现了java分布式的布隆过滤器RBloomFilter
public class BloomFilterExamples {
public static void main(String[] args) {
// connects to 127.0.0.1:6379 by default
RedissonClient redisson = Redisson.create();
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("bloomFilter");
bloomFilter.tryInit(100_000_000, 0.03);
bloomFilter.add("a");
bloomFilter.add("b");
bloomFilter.add("c");
bloomFilter.add("d");
bloomFilter.getExpectedInsertions();
bloomFilter.getFalseProbability();
bloomFilter.getHashIterations();
bloomFilter.contains("a");
bloomFilter.count();
redisson.shutdown();
}
}
实现RClusteredBloomFilter接口,可以分片。通过压缩未使用的比特位来释放集群内存空间
基数估计算法(RHyperLogLog)
可以在有限的空间通过概率算法统计大量数据
限流器(RRateLimiter )
可以用来在分布式环境下限制请求方的调用频率。适用于不同或相同的Reddisson实例的多线程限流。并不保证公平性
public class RateLimiterExamples {
public static void main(String[] args) throws InterruptedException {
// connects to 127.0.0.1:6379 by default
RedissonClient redisson = Redisson.create();
RRateLimiter limiter = redisson.getRateLimiter("myLimiter");
// one permit per 2 seconds
limiter.trySetRate(RateType.OVERALL, 1, 2, RateIntervalUnit.SECONDS);
CountDownLatch latch = new CountDownLatch(2);
limiter.acquire(1);
latch.countDown();
Thread t = new Thread(() -> {
limiter.acquire(1);
latch.countDown();
});
t.start();
t.join();
latch.await();
redisson.shutdown();
}
}
及原子整长形(RAtomicLong )、原子双精度浮点(RAtomicDouble )、话题(订阅分发)(RTopic )、整长型累加器(RLongAdder )、双精度浮点累加器(RLongDouble )等分布式对象
在Redis分布式的集合元素与java对象相对应。包括:映射、多值映射(Multimap)、集(Set)、有序集(SortedSet)、计分排序集
(ScoredSortedSet)、字典排序集(LexSortedSet)、列表(List)、队列(Queue)、双端队列(Deque)、阻塞队列(Blocking Queue)
映射类型按照特性主要分为三类:元素淘汰、本地缓存、数据分片。
元素淘汰(Eviction)类:对映射中每个元素单独设置有效时间和最长闲置时间。保留了元素的插入顺序。不支持散列(hash)的元素
淘汰,过期元素通过EvictionScheduler实例定期清理。实现类RMapCache
本地缓存(LocalCache)类:本地缓存也叫就近缓存,主要用在特定场景下。映射缓存上的高度频繁的读取操作,使网络通信被视
为瓶颈的情况下。较分布式映射提高45倍。实现类RLocalCachedMap
数据分片(Sharding):利用分库的原理,将单一映射结构切分若干,并均匀分布在集群的各个槽里。可以突破Redis自身的容量限
制,可以随集群的扩大而增长,也可以使读写性能和元素淘汰能力随之线性增长。主要实现类RClusteredMap
当然还有其他类型,比如映射监听器、LRU有界映射
映射监听器:监听元素的添加(EntryCreatedListener)、过期、删除、更新事件
LRU有界映射:根据时间排序,超过容量限制的元素会被删除
public class MapExamples {
public static void main(String[] args) throws IOException {
// connects to 127.0.0.1:6379 by default
RedissonClient redisson = Redisson.create();
RMap<String, Integer> map = redisson.getMap("myMap");
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);
boolean contains = map.containsKey("a");
Integer value = map.get("c");
Integer updatedValue = map.addAndGet("a", 32);
Integer valueSize = map.valueSize("c");
Set<String> keys = new HashSet<String>();
keys.add("a");
keys.add("b");
keys.add("c");
Map<String, Integer> mapSlice = map.getAll(keys);
// use read* methods to fetch all objects
Set<String> allKeys = map.readAllKeySet();
Collection<Integer> allValues = map.readAllValues();
Set<Entry<String, Integer>> allEntries = map.readAllEntrySet();
// use fast* methods when previous value is not required
boolean isNewKey = map.fastPut("a", 100);
boolean isNewKeyPut = map.fastPutIfAbsent("d", 33);
long removedAmount = map.fastRemove("b");
redisson.shutdown();
}
}
将映射中的数据持久化到外部存储服务的功能
主要场景:
Read-through策略:如果在映射中不存在,则通过Maploader对象加载
Write-through策略(数据同步写入):对映射数据的更改则会通过MapWriter写入到外部存储系统,然后更新redis里面的数据
Write-behind策略(数据异步写入):对映射数据的更改先写到redis,然后使用异步方式写入到外部存储
如果负责存储分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态,这时便会出现死锁。为了避免发生这种状况,提供了
一个看门狗,它的作用是在Redisson实例被关闭之前,不断延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟。
分布式锁种类有:可重入锁(Reentrant Lock)、公平锁、联锁(MultiLock)、红锁(RedLock)、 读写锁、信号量(Semaphore)、
可过期性信号量(PermitExpirableSemaphore)、闭锁(CountDownLatch)
public class LockExamples {
public static void main(String[] args) throws InterruptedException {
// connects to 127.0.0.1:6379 by default
RedissonClient redisson = Redisson.create();
RLock lock = redisson.getLock("lock");
lock.lock(2, TimeUnit.SECONDS);
Thread t = new Thread() {
public void run() {
RLock lock1 = redisson.getLock("lock");
lock1.lock();
lock1.unlock();
};
};
t.start();
t.join();
lock.unlock();
redisson.shutdown();
}
}
红锁
public class RedLockExamples {
public static void main(String[] args) throws InterruptedException {
// connects to 127.0.0.1:6379 by default
RedissonClient client1 = Redisson.create();
RedissonClient client2 = Redisson.create();
RLock lock1 = client1.getLock("lock1");
RLock lock2 = client1.getLock("lock2");
RLock lock3 = client2.getLock("lock3");
Thread t1 = new Thread() {
public void run() {
lock3.lock();
};
};
t1.start();
t1.join();
Thread t = new Thread() {
public void run() {
RedissonMultiLock lock = new RedissonRedLock(lock1, lock2, lock3);
lock.lock();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
lock.unlock();
};
};
t.start();
t.join(1000);
lock3.forceUnlock();
RedissonMultiLock lock = new RedissonRedLock(lock1, lock2, lock3);
lock.lock();
lock.unlock();
client1.shutdown();
client2.shutdown();
}
}
分布式服务包括分布式远程服务(RRemoteService )、分布式实时对象服务(RLiveObjectService )、分布式执行服务(RExecutorService )、分布式调度任务服务(RScheduledExecutorService )、分布式映射归纳服务(MapReduce)
public class ExecutorServiceExamples {
public static class RunnableTask implements Runnable, Serializable {
@RInject
RedissonClient redisson;
@Override
public void run() {
RMap<String, String> map = redisson.getMap("myMap");
map.put("5", "11");
}
}
public static class CallableTask implements Callable<String>, Serializable {
@RInject
RedissonClient redisson;
@Override
public String call() throws Exception {
RMap<String, String> map = redisson.getMap("myMap");
map.put("1", "2");
return map.get("3");
}
}
public static void main(String[] args) {
Config config = new Config();
config.useClusterServers()
.addNodeAddress("127.0.0.1:7001", "127.0.0.1:7002", "127.0.0.1:7003");
RedissonClient redisson = Redisson.create(config);
RedissonNodeConfig nodeConfig = new RedissonNodeConfig(config);
nodeConfig.setExecutorServiceWorkers(Collections.singletonMap("myExecutor", 1));
RedissonNode node = RedissonNode.create(nodeConfig);
node.start();
RExecutorService e = redisson.getExecutorService("myExecutor");
e.execute(new RunnableTask());
e.submit(new CallableTask());
e.shutdown();
node.shutdown();
}
}
4、分布式调度任务服务:对计划任务的设定(可以通过CRON表达式)及去掉计划任务
public class SchedulerServiceExamples {
public static class RunnableTask implements Runnable, Serializable {
@RInject
RedissonClient redisson;
@Override
public void run() {
RMap<String, String> map = redisson.getMap("myMap");
map.put("5", "11");
}
}
public static class CallableTask implements Callable<String>, Serializable {
@RInject
RedissonClient redisson;
@Override
public String call() throws Exception {
RMap<String, String> map = redisson.getMap("myMap");
map.put("1", "2");
return map.get("3");
}
}
public static void main(String[] args) {
Config config = new Config();
config.useClusterServers()
.addNodeAddress("127.0.0.1:7001", "127.0.0.1:7002", "127.0.0.1:7003");
RedissonClient redisson = Redisson.create(config);
RedissonNodeConfig nodeConfig = new RedissonNodeConfig(config);
nodeConfig.setExecutorServiceWorkers(Collections.singletonMap("myExecutor", 5));
RedissonNode node = RedissonNode.create(nodeConfig);
node.start();
RScheduledExecutorService e = redisson.getExecutorService("myExecutor");
e.schedule(new RunnableTask(), 10, TimeUnit.SECONDS);
e.schedule(new CallableTask(), 4, TimeUnit.MINUTES);
e.schedule(new RunnableTask(), CronSchedule.of("10 0/5 * * * ?"));
e.schedule(new RunnableTask(), CronSchedule.dailyAtHourAndMinute(10, 5));
e.schedule(new RunnableTask(), CronSchedule.weeklyOnDayAndHourAndMinute(12, 4, Calendar.MONDAY, Calendar.FRIDAY));
e.shutdown();
node.shutdown();
}
}
5、分布式映射归纳服务:通过映射归纳处理存储在Redis环境里的大量数据。示例代码单词统计
Redisson的分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口,同时还支持自动过期解锁。
public void testReentrantLock(RedissonClient redisson){
RLock lock = redisson.getLock("anyLock");
try{
// 1. 最常见的使用方法
//lock.lock();
// 2. 支持过期解锁功能,10秒钟以后自动解锁, 无需调用unlock方法手动解锁
//lock.lock(10, TimeUnit.SECONDS);
// 3. 尝试加锁,最多等待3秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(3, 10, TimeUnit.SECONDS);
if(res){ //成功
// do your business
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
Redisson同时还为分布式锁提供了异步执行的相关方法:
public void testAsyncReentrantLock(RedissonClient redisson){
RLock lock = redisson.getLock("anyLock");
try{
lock.lockAsync();
lock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = lock.tryLockAsync(3, 10, TimeUnit.SECONDS);
if(res.get()){
// do your business
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
Redisson分布式可重入公平锁也是实现了java.util.concurrent.locks.Lock接口的一种RLock对象。在提供了自动过期解锁功能的同时,保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。
public void testFairLock(RedissonClient redisson){
RLock fairLock = redisson.getFairLock("anyLock");
try{
// 最常见的使用方法
fairLock.lock();
// 支持过期解锁功能, 10秒钟以后自动解锁,无需调用unlock方法手动解锁
fairLock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = fairLock.tryLock(100, 10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
fairLock.unlock();
}
}
Redisson同时还为分布式可重入公平锁提供了异步执行的相关方法:
RLock fairLock = redisson.getFairLock("anyLock");
fairLock.lockAsync();
fairLock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = fairLock.tryLockAsync(100, 10, TimeUnit.SECONDS);
Redisson的RedissonMultiLock对象可以将多个RLock对象关联为一个联锁,每个RLock对象实例可以来自于不同的Redisson实例。
public void testMultiLock(RedissonClient redisson1,
RedissonClient redisson2, RedissonClient redisson3){
RLock lock1 = redisson1.getLock("lock1");
RLock lock2 = redisson2.getLock("lock2");
RLock lock3 = redisson3.getLock("lock3");
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
try {
// 同时加锁:lock1 lock2 lock3, 所有的锁都上锁成功才算成功。
lock.lock();
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
Redisson的RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock
对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例。
public void testRedLock(RedissonClient redisson1,
RedissonClient redisson2, RedissonClient redisson3){
RLock lock1 = redisson1.getLock("lock1");
RLock lock2 = redisson2.getLock("lock2");
RLock lock3 = redisson3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
try {
// 同时加锁:lock1 lock2 lock3, 红锁在大部分节点上加锁成功就算成功。
lock.lock();
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
Redisson的分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。同时还支持自动过期解锁。该对象允许同时有多个读取锁,但是最多只能有一个写入锁。
RReadWriteLock rwlock = redisson.getLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();
// 支持过期解锁功能
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。
RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquire();
//或
semaphore.acquireAsync();
semaphore.acquire(23);
semaphore.tryAcquire();
//或
semaphore.tryAcquireAsync();
semaphore.tryAcquire(23, TimeUnit.SECONDS);
//或
semaphore.tryAcquireAsync(23, TimeUnit.SECONDS);
semaphore.release(10);
semaphore.release();
//或
semaphore.releaseAsync();
Redisson的可过期性信号量(PermitExpirableSemaphore)实在RSemaphore对象的基础上,为每个信号增加了一个过期时间。每个信号可以通过独立的ID来辨识,释放时只能通过提交这个ID才能释放。
RPermitExpirableSemaphore semaphore = redisson.getPermitExpirableSemaphore("mySemaphore");
String permitId = semaphore.acquire();
// 获取一个信号,有效期只有2秒钟。
String permitId = semaphore.acquire(2, TimeUnit.SECONDS);
// ...
semaphore.release(permitId);
Redisson的分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与java.util.concurrent.CountDownLatch相似的接口和用法。
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.trySetCount(1);
latch.await();
// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();