Redis 简介
Redis 是完全开源的,遵守 BSD 协议,是一个高性能的 key-value 数据库。
Redis 与其他 key - value 缓存产品有以下三个特点:
- Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
- Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
- Redis支持数据的备份,即master-slave模式的数据备份。
Redis 优势
- 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
- 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
- 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
- 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。
Redis与其他key-value存储有什么不同?
-
Redis有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。
-
Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。
Redis实现分布式锁
- 在集群模式下,synchronized只能保证单个JVM内部的线程互斥,不能保证跨JVM的互斥
1. Java锁
2. 分布式锁
- 满足分布式系统或集群模式下多进程可见并互斥的锁
# 分布式锁特点
1. 多进程可见: 必须多个jvm都能去访问到该锁资源
2. 互斥: 锁资源必须是互斥
3. 高可用: 锁的稳定性要得到保证
4. 高性能: 加锁本来就会降低系统性能,如何保证
5. 安全性: 锁假如无法释放怎么办
3. 基础版本
- 单线程保证一定只有一个线程来获取锁
# 场景一: 假如锁匙放失败怎么半?
1. 获取: SETNX k v
2. 执行业务
3. 释放锁 DEL k# 场景二:
1. 获取锁,并添加过期时间 SET K V EX 10 NX
2. 执行业务
3. 释放锁
package com.erick.redis;
import redis.clients.jedis.Jedis;
public class Demo01 {
public static final String LOCK_NAME = "LOCK";
public static final String LOCK_VALUE = "ERICK";
public static final int EXPIRE_SECS = 5;
private static Jedis getJedis() {
return new Jedis("60.205.229.31", 6381);
}public static void main(String[] args) throws InterruptedException {
new Thread(() -> secondLock()).start();
new Thread(() -> secondLock()).start();
}/*场景一: 假如释放锁失败,则后面永远无法执行*/
public static void firstLock() {
//1.上锁
Jedis redis = getJedis();
Long lockResult = redis.setnx(LOCK_NAME, LOCK_VALUE);
if (1 == lockResult) {
// 2. 执行业务
executeBusiness();
// 3. 释放锁
redis.del(LOCK_NAME);
} else {
// 获取锁失败
System.out.println("Can not get lock");
}
}/*场景二: 释放锁失败,通过自动过期来保证*/
public static void secondLock() {
Jedis redis = getJedis();
String lockResult = redis.set(LOCK_NAME, LOCK_VALUE, "NX", "EX", EXPIRE_SECS);
if ("OK".equalsIgnoreCase(lockResult)) {
executeBusiness();
redis.del(LOCK_NAME);
} else {
System.out.println("Can not get lock");
}
}private static void executeBusiness() {
System.out.println("Business execution.....");
}
}
4. 增强版本
- 上面分布式锁存在问题: 误删,删已失效
解决方法一:设置超时时间远大于业务执行时间,但是会带来性能问题
解决方法二:删除锁的时候要判断,是不是自己的,如果是再删除 UUID
1. 其中key可以用业务名称来表示
2. value用uuid来表示
2.1 删除锁时,先通过value来判断锁是不是自己线程的
2.2 如果是,则删除,如果不是,就不要删除
package com.erick.redis;
import redis.clients.jedis.Jedis;
import java.util.UUID;
public class Demo11 {
private static Jedis getJedis() {
return new Jedis("60.205.229.31", 6381);
}private static String getLockValue() {
return UUID.randomUUID().toString();
}private static final String LOCK_KEY = "LOCK";
private static final int EXPIRE_SECS = 5;
public static void main(String[] args) {
new Thread(() -> firstMethod(LOCK_KEY, getLockValue())).start();
}private static void firstMethod(String lockKey, String lockValue) {
Jedis redis = getJedis();
String lockResult = redis.set(lockKey, lockValue, "NX", "EX", EXPIRE_SECS);
if ("OK".equalsIgnoreCase(lockResult)) {
executeBusiness();
String presentValue = redis.get(lockKey);
/*判断是否是自己的,是自己的再删除*/
if (lockValue.equalsIgnoreCase(presentValue)) {
redis.del(lockKey);
System.out.println("lock deleted");
}
} else {
System.out.println("Can not get lock");
}
}private static void executeBusiness() {
System.out.println("Business execution.....");
}
}
5.Redisson
- 一个用来进行分布式锁的工具类
入门案例
package com.erick.redis;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;import java.util.concurrent.TimeUnit;
public class Demo03 {
private static final String LOCK_KEY = "COMMERCE-BUSINESS";
/*Redisson的配置类*/
private static RedissonClient redissonClient() {
Config config = new Config();
/* Redis 单节点*/
config.useSingleServer().setAddress("redis://60.205.229.31:6381");
return Redisson.create(config);
}private static void executeBusiness() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Business executing.....");
}public static void main(String[] args) {
new Thread(() -> lockMethodWithRetry()).start();
new Thread(() -> lockMethodWithRetry()).start();
}/*基本使用*/
private static void lockMethod() {
RedissonClient redissonClient = redissonClient();
/* RLock extends Lock*/
RLock lock = redissonClient.getLock(LOCK_KEY);/*可重入锁: 默认超时时间喂30s*/
if (lock.tryLock()) {
try {
executeBusiness();
} catch (Exception e) {
e.printStackTrace();} finally {
lock.unlock();
System.out.println("Lock Released");
}
} else {
System.out.println("Can not get lock");
}
}/*等待超时的锁*/
private static void lockMethodWithRetry() {
RedissonClient redissonClient = redissonClient();
/*获取对应的key的锁*/
RLock lock = redissonClient.getLock(LOCK_KEY);// 内部包含 重试机制,通过Redis的发布订阅者模式来实现
/* 参数一:最长等待时间,超时则不再等待
* 参数二:锁超时释放时间
* 参数三:时间单位 */
boolean hasLok = false;
try {
hasLok = lock.tryLock(6, 20, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}if (hasLok) {
try {
executeBusiness();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("Lock Released");
}
} else {
System.out.println("Can not get lock");
}
}
}
不可重入锁
可重入锁
- 存储的键值对用Hash结构来保存
重试机制
- 通过等待时间结合,发布以及订阅模式来实现
- 不会立即触发重试机制,而是订阅当前锁的使用者发布的消息
锁超时释放
- 业务执行期间,不断有定时任务去更新过期时间
- 业务执行完毕后,取消定时任务
Redis实现接口幂等性
保证其幂等性,通常有以下手段:
1、数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据。
2、token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token。
3、悲观锁或者乐观锁,悲观锁可以保证每次for update的时候其他sql无法update数据(在数据库引擎是innodb的时候,select的条件必须是唯一索引,防止锁全表)
4、先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求,如果没有存在,就证明是第一次进来,直接放行。
redis 实现自动幂等的原理图:
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring:
redis:
host: localhost
port: 6379
password: 123456
database: 0
jedis:
pool:
max-active: 8 #连接池最大连接数
max-wait: -1 #连接池最大阻塞等待时间
max-idle: 8 #连接池中的最大空闲连接
min-idle: 0 #连接池中的最小空闲连接
time-between-eviction-runs: -1
timeout: 0
lettuce:
shutdown-timeout: 0
@Service
public class RedisUtils {
@Autowired
private RedisTemplate redisTemplate;
public boolean set(final String key, Object value) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
public boolean setEx(final String key, Object value, Long expireTime) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
public boolean exists(final String key) {
return redisTemplate.hasKey(key);
}
public Object get(final String key) {
Object result = null;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.get(key);
return result;
}
public boolean remove(final String key) {
if (exists(key)) {
Boolean delete = redisTemplate.delete(key);
return delete;
}
return false;
}
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotency {
boolean required() default true;
}
public interface TokenService {
/**
* 创建token
*/
String createToken();
/**
* 检验token
*/
boolean checkToken(HttpServletRequest request) throws Exception;
}
@Service
public class TokenServiceImpl implements TokenService {
@Autowired
RedisUtils redisUtils;
private final String TOKEN_PREFIX = "idempotency";
private final String TOKEN_NAME = "ACCESS-Token";
@Override
public String createToken() {
String str = UUID.randomUUID().toString();
StringBuilder token = new StringBuilder();
try {
token.append(TOKEN_PREFIX).append(str);
redisUtils.setEx(token.toString(), token.toString(), 10000L);
if (!StringUtils.isEmpty(token.toString())) {
return token.toString();
}
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
@Override
public boolean checkToken(HttpServletRequest request) throws Exception {
String token = request.getHeader(TOKEN_NAME);
if (StringUtils.hasText(token)) {
token = request.getParameter(TOKEN_NAME);
if (StringUtils.hasText(token)) {
throw new CustomException(20001, "缺少参数ACCESS-Token");
}
}
if (!redisUtils.exists(token)) {
throw new CustomException(20001, "不能重复提交");
}
boolean remove = redisUtils.remove(token);
if (!remove) {
throw new CustomException(20001, "Token刷新失败");
}
return true;
}
}
自定义异常
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CustomException extends RuntimeException {
private Integer code;
private String msg;
}
自定义异常拦截
@ControllerAdvice
public class ExceptionHandler {
//指定出现什么异常值执行这个方法 @org.springframework.web.bind.annotation.ExceptionHandler(CustomException.class)
@ResponseBody//为了返回数据
public Map<String, Object> customExceptionHandler(CustomException ex) {
Map<String, Object> map = new HashMap<>();
map.put("code", ex.getCode());
map.put("msg", ex.getMsg());
return map;
}
}
配置拦截器
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<!-- JSONObject对象依赖的jar包 -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.3</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>net.sf.ezmorph</groupId>
<artifactId>ezmorph</artifactId>
<version>1.0.6</version>
</dependency>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.2.3</version>
<classifier>jdk11</classifier><!-- 指定jdk版本 -->
</dependency>
WebConfiguration
@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {
@Resource
private AutoIdempotentInterceptor autoIdempotentInterceptor;
/** * 添加拦截器 * @param registry */
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(autoIdempotentInterceptor);
super.addInterceptors(registry);
}
}
/**
* 拦截器
*/
@Component
public class IdempotencyInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
/**
* 预处理
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {//如果没有注解,直接返回true
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//被ApiIdempotment标记的扫描
if (method.isAnnotationPresent(Idempotency.class)) {
Idempotency idempotencyAnnotation = method.getAnnotation(Idempotency.class);//通过反射获取注解
if (idempotencyAnnotation.required()) {
return tokenService.checkToken(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
private void writeReturnJson(HttpServletResponse response, String json) throws Exception {
PrintWriter writer = null;
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
try {
writer = response.getWriter();
writer.print(json);
} catch (IOException e) {
} finally {
if (writer != null)
writer.close();
}
}
}
Redis淘汰机制
六种淘汰策略
1.noeviction(默认策略):对于写请求不再提供服务,直接返回错误(DEL请求和部分特殊请求除外)
2.allkeys-lru:从所有key中使用LRU算法进行淘汰(LRU算法:即最近最少使用算法)
3.volatile-lru:从设置了过期时间的key中使用LRU算法进行淘汰
4.allkeys-random:从所有key中随机淘汰数据
5.volatile-random:从设置了过期时间的key中随机淘汰
6.volatile-ttl:在设置了过期时间的key中,淘汰过期时间剩余最短的
当使用volatile-lru、volatile-random、volatile-ttl这三种策略时,如果没有key可以被淘汰,则和noeviction一样返回错误
如何获取及设置内存淘汰策略
1、获取当前内存淘汰策略:
127.0.0.1:6379> config get maxmemory-policy
可以看到当前使用的默认的noeviction策略
2、获取Redis能使用的最大内存大小
127.0.0.1:6379> config get maxmemory
如果不设置最大内存大小或者设置最大内存大小为0,在64位操作系统下不限制内存大小,在32位操作系统下最多使用3GB内存。32 位的机器最大只支持 4GB 的内存,而系统本身就需要一定的内存资源来支持运行,所以 32 位机器限制最大 3 GB 的可用内存
3、设置淘汰策略
通过配置文件设置淘汰策略(修改redis.conf文件):
maxmemory-policy allkeys-lru
通过命令修改淘汰策略:
127.0.0.1:6379> config set maxmemory-policy allkeys-lru
4、设置Redis最大占用内存大小
#设置Redis最大占用内存大小为100M
127.0.0.1:6379> config set maxmemory 100mb
Redis持久化
Redis是基于内存操作,作为一个数据库也具备持久化能力,但是为了实现高效的读写操作,并不会及时进行数据的持久化,而是按照一定的规则进行持久化操作的。Redis提供了两种持久化策略:
- RDB(Redis DataBase)
- AOF(Append only File)
RDB
在满足指定的Redis操作条件时,将内存中的数据以数据快照的方式存储到rdb文件中
1. RDB的原理:
RDB是redis默认的持久化策略,当redis中写操作达到指定的次数同时距离上一次持久化达到指定的时间就会将redis内存中数据生成数据快照保存到指定的rdb文件中。
2. RDB默认触发持久化的条件:
RDB默认触发持久化条件
900s 1次: | 当操作次数达到1次,900s就会进行持久化 |
300s 10次: | 当操作次数达到10次,300s就会进行持久化 |
60s 1000次: | 当操作次数达到10000次,60s就会进行持久化 |
操作次数越多,触发持久化的时间间隔就越短(防止数据丢失) ,我们可以通过修改redis.conf文件,来设置RDB策略的触发条件。
## rdb持久化策略开关
rdbcompression yes
## 配置rdb持久化策略
save 900 1
save 300 10
save 60 10000
3. 指定RDB数据存储的文件:
在redis.conf文件中除了可以配置rdb的持久化策略,还可以将生成的数据快照存储的文件进行指定。默认是dbfilename dump.rdb。
4. RDB持久化细节分析:
RDB持久化优点:
在数据量较小的情况下,执行速度比较快
由于RDB是以数据快照形式保存的,我们可以通过检索拷贝rdb文件轻松实现redis数据移植
RDB持久化缺点:
如果redis出现故障,存在数据丢失的风险,丢失上一次持久化之后的操作数据(因为每次持久化,都会有操作次数以及时间间隔)
RDB采用的数据快照的形式进行的持久化,不适合实时性持久化
如果数据量庞大,在RDB持久化过程中生成数据快照子进程执行时间过长,会导致redis卡顿,因此Save的时间周期设置不宜过短(默认配置即可)
AOF
AOF(Append Only File),当达到设定的触发条件时,将redis执行的写操作指令存储到aof文件中,redis默认是未开启aof持久化的。
1. AOF的原理:
redis将每一个成功的写操作写入aof文件中,当redis重启的时候就执行aof文件中的指令以恢复数据
2. AOF触发持久化的条件:
appendfsync always | 只要进行成功写操作,aof就执行 |
appendfsync everysec | 每秒进行一次aof(默认) |
appendfsync no | 让redis执行决定aof |
redis默认是AOF未开启的;可以通过将redis配置文件中‘appendonly no’修改为‘appendonly yes’进行开启 ;AOF也可以设置aof路径,默认是‘appendfilename "appendonly.aof"’
3. AOF持久化细节分析:
可以通过拷贝aof文件jinxingredis数据移植
aof存储的是指令,而且会对指令进行整理,而RDB直接生成的数据快照,在数据量不大的时候会比较快
aof是对文件进行增量更新,更适合实时性持久化
redis官方建议是同时开启两种持久化策略,如果同时存在aof文件以及rdb文件,当我们需要进行数据移植的时候,优先选择aof(数据完整性会相对高一点)
Redis集群搭建
1 先安装6台redis服务
1 这里使用三台节点 10.0.11.54 10.0.11.56 10.0.11.57,每台节点安装2个redis服务,通过不同的端口号区分
2 复制redis.conf 两份,redis-6479.con redis-6480.conf
将cluster-enabled yes 的注释打开,将 port改为6479,6480
3 三台节点进行同样的操作,最终安装6个redis服务
2 启动6台redis服务
# 三台节点挨个启动
$basedir/bin/redis-server $basedir/conf/redis-6479.conf
$basedir/bin/redis-server $basedir/conf/redis-6480.conf
3 创建redis集群
- 创建集群只需要在一台节点上执行即可,但是创建集群之前需要保证6台redis服务都正常启动
# -a 指定密码,
./redis-cli -a 123456 --cluster create 10.0.11.54:6479 10.0.11.54:6480 10.0.11.56:6479 10.0.11.56:6480 10.0.11.57:6479 10.0.11.57:6780 --cluster-replicas 1
4 验证集群
登录任意一台节点
Redis数据类型
Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
String(字符串)
string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。
string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。
string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。
实例
redis 127.0.0.1:6379> SET runoob "菜鸟教程" OK redis 127.0.0.1:6379> GET runoob "菜鸟教程"
在以上实例中我们使用了 Redis 的 SET 和 GET 命令。键为 runoob,对应的值为 菜鸟教程。
注意:一个键最大能存储 512MB。
Hash(哈希)
Redis hash 是一个键值(key=>value)对集合。
Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
实例
DEL runoob 用于删除前面测试用过的 key,不然会报错:(error) WRONGTYPE Operation against a key holding the wrong kind of value
redis 127.0.0.1:6379> DEL runoob redis 127.0.0.1:6379> HMSET runoob field1 "Hello" field2 "World" "OK" redis 127.0.0.1:6379> HGET runoob field1 "Hello" redis 127.0.0.1:6379> HGET runoob field2 "World"
实例中我们使用了 Redis HMSET, HGET 命令,HMSET 设置了两个 field=>value 对, HGET 获取对应 field 对应的 value。
每个 hash 可以存储 232 -1 键值对(40多亿)。
List(列表)
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
实例
redis 127.0.0.1:6379> DEL runoob redis 127.0.0.1:6379> lpush runoob redis (integer) 1 redis 127.0.0.1:6379> lpush runoob mongodb (integer) 2 redis 127.0.0.1:6379> lpush runoob rabbitmq (integer) 3 redis 127.0.0.1:6379> lrange runoob 0 10 1) "rabbitmq" 2) "mongodb" 3) "redis" redis 127.0.0.1:6379>
列表最多可存储 232 - 1 元素 (4294967295, 每个列表可存储40多亿)。
Set(集合)
Redis 的 Set 是 string 类型的无序集合。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
sadd 命令
添加一个 string 元素到 key 对应的 set 集合中,成功返回 1,如果元素已经在集合中返回 0。
sadd key member
实例
redis 127.0.0.1:6379> DEL runoob redis 127.0.0.1:6379> sadd runoob redis (integer) 1 redis 127.0.0.1:6379> sadd runoob mongodb (integer) 1 redis 127.0.0.1:6379> sadd runoob rabbitmq (integer) 1 redis 127.0.0.1:6379> sadd runoob rabbitmq (integer) 0 redis 127.0.0.1:6379> smembers runoob 1) "redis" 2) "rabbitmq" 3) "mongodb"
注意:以上实例中 rabbitmq 添加了两次,但根据集合内元素的唯一性,第二次插入的元素将被忽略。
集合中最大的成员数为 232 - 1(4294967295, 每个集合可存储40多亿个成员)。
zset(sorted set:有序集合)
Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
zset的成员是唯一的,但分数(score)却可以重复。
zadd 命令
添加元素到集合,元素在集合中存在则更新对应score
zadd key score member
实例
redis 127.0.0.1:6379> DEL runoob redis 127.0.0.1:6379> zadd runoob 0 redis (integer) 1 redis 127.0.0.1:6379> zadd runoob 0 mongodb (integer) 1 redis 127.0.0.1:6379> zadd runoob 0 rabbitmq (integer) 1 redis 127.0.0.1:6379> zadd runoob 0 rabbitmq (integer) 0 redis 127.0.0.1:6379> ZRANGEBYSCORE runoob 0 1000 1) "mongodb" 2) "rabbitmq" 3) "redis"
布隆过滤器
什么是布隆过滤器
布隆过滤器(Bloom Filter)是 1970 年由布隆提出的,是一种非常节省空间的概率数据结构,运行速度快,占用内存小,但是有一定的误判率且无法删除元素。它实际上是一个很长的二进制向量和一系列随机映射函数组成,主要用于判断一个元素是否在一个集合中。
通常我们都会遇到判断一个元素是否在某个集合中的业务场景,这个时候我们可能都是采用 HashMap的Put方法或者其他集合将数据保存起来,然后进行比较确定,但是如果元素很多的情况下,采用这种方式就会非常浪费空间,最终达到瓶颈,检索速度也会越来越慢,这时布隆过滤器(Bloom Filter)就应运而生了。
布隆过滤器的优点
- 支持海量数据场景下高效判断元素是否存在
- 布隆过滤器存储空间小,并且节省空间,不存储数据本身,仅存储hash结果取模运算后的位标记
- 不存储数据本身,比较适合某些保密场景
布隆过滤器的缺点
- 不存储数据本身,所以只能添加但不可删除,因为删掉元素会导致误判率增加
- 由于存在hash碰撞,匹配结果如果是“存在于过滤器中”,实际不一定存在
- 当容量快满时,hash碰撞的概率变大,插入、查询的错误率也就随之增加了
布隆过滤器中一个元素如果判断结果为存在的时候元素不一定存在,但是判断结果为不存在的时候则一定不存在。因此,布隆过滤器不适合那些对结果必须精准的应用场景。
其他问题
- 不支持计数,同一个元素可以多次插入,但效果和插入一次相同
- 由于错误率影响hash函数的数量,当hash函数越多,每次插入、查询需做的hash操作就越多
数据结构
布隆过滤器是由一个固定大小的二进制向量或者位图(bitmap)和一系列映射函数组成的。
对于长度为 m 的位数组,在初始状态时,它所有位置都被置为0,如下图所示:
位数组中的每个元素都只占用 1 bit ,并且数组中元素只能是 0 或者 1。这样申请一个 100w 个元素的位数组只占用 1000000Bit / 8 = 125000 Byte = 125000/1024 KB ≈ 122KB 的空间。
增加元素
当一个元素加入布隆过滤器中的时候,会进行如下操作:
- 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)
- 根据得到的哈希值,在位数组中把对应下标的值置为 1
如下图所示:
接着再添加一个值 “xinlang”,哈希函数的值是3、5、8,如下图所示:
这里需要注意的是,5 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此被覆盖了。
查询元素
- 对给定元素再次进行相同的哈希计算
- 得到哈希值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值存在布隆过滤器当中,如果存在一个值不为 1,说明该元素不在布隆过滤器中
例如我们查询 “cunzai” 这个值是否存在,哈希函数返回了 1、5、8三个值
如下图所示:
结果得到三个 1 ,说明 “cunzai” 是有可能存在的。
为什么说是可能存在,而不是一定存在呢?主要分为以下几种情况:
因为映射函数本身就是散列函数,散列函数是会有碰撞的情况发生。
- 情况1:一个字符串可能是 “chongtu” 经过相同的三个映射函数运算得到的三个点跟 “xinlang” 是一样的,这种情况下我们就说出现了误判
- 情况2: “chongtu” 经过运算得到三个点位上的 1 是两个不同的变量经过运算后得到的,这也不能证明字符串 “chongtu” 是一定存在的
如下图所示:
鉴于上面的情况,不同的字符串可能哈希出来的位置相同,这种情况我们可以适当增加位数组大小或者调整哈希函数。
布隆过滤器判定某个元素存在,小概率会误判;布隆过滤器判定某个元素不在,则这个元素一定不在。
删除元素
布隆过滤器对元素的删除,肯定不可以,会出现问题,比如上面添加元素的 bit 位 5 被两个变量的哈希值共同覆盖的情况下,一旦我们删除其中一个值。例如“xinlang”而将其置位 0,那么下次判断另一个值例如“baidu”是否存在的话,会直接返回 false,而实际上我们并没有删除它,这就导致了误判的问题。
Google开源的Guava自带布隆过滤器
首先引入Guava的依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
那么,在数据量很大的情况下,效率如何呢?
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 5000000);
for (int i = 0; i < 5000000; i++) {
bloomFilter.put(i);
}
long start = System.nanoTime();
if (bloomFilter.mightContain(500000)) {
System.out.println("成功过滤到500000");
}
long end = System.nanoTime();
System.out.println("布隆过滤器消耗时间"+(end - start)/1000000L+"毫秒");
成功过滤到500000
布隆过滤器消耗时间0毫秒
那么,在数据量很大的情况下,1%的误判率结果如何?
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),5000000,0.01);
List<String> list = new ArrayList<>(5000000);
for (int i = 0; i < 5000000; i++) {
String uuid = UUID.randomUUID().toString();
bloomFilter.put(uuid);
list.add(uuid);
}
int mightContainNumber1= 0;
NumberFormat percentFormat =NumberFormat.getPercentInstance();
percentFormat.setMaximumFractionDigits(2);
for (int i=0;i < 500;i++){
String key = list.get(i);
if (bloomFilter.mightContain(key)){
mightContainNumber1++;
}
}
System.out.println("【key真实存在的情况】布隆过滤器认为存在的key值数:" + mightContainNumber1);
System.out.println("================================================================================");
int mightContainNumber2 = 0;
for (int i=0;i < 5000000;i++){
String key = UUID.randomUUID().toString();
if (bloomFilter.mightContain(key)){
mightContainNumber2++;
}
}
System.out.println("【key不存在的情况】布隆过滤器认为存在的key值数:" + mightContainNumber2);
System.out.println("【key不存在的情况】布隆过滤器的误判率为:" + percentFormat.format((float)mightContainNumber2 / 5000000));
【key真实存在的情况】布隆过滤器认为存在的key值数:500
================================================================================
【key不存在的情况】布隆过滤器认为存在的key值数:50389
【key不存在的情况】布隆过滤器的误判率为:1.01%
3%的误判率结果如何?
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),5000000,);
List<String> list = new ArrayList<>(5000000);
for (int i = 0; i < 5000000; i++) {
String uuid = UUID.randomUUID().toString();
bloomFilter.put(uuid);
list.add(uuid);
}
int mightContainNumber1= 0;
NumberFormat percentFormat =NumberFormat.getPercentInstance();
percentFormat.setMaximumFractionDigits(2);
for (int i=0;i < 500;i++){
String key = list.get(i);
if (bloomFilter.mightContain(key)){
mightContainNumber1++;
}
}
System.out.println("【key真实存在的情况】布隆过滤器认为存在的key值数:" + mightContainNumber1);
System.out.println("================================================================================");
int mightContainNumber2 = 0;
for (int i=0;i < 5000000;i++){
String key = UUID.randomUUID().toString();
if (bloomFilter.mightContain(key)){
mightContainNumber2++;
}
}
System.out.println("【key不存在的情况】布隆过滤器认为存在的key值数:" + mightContainNumber2);
System.out.println("【key不存在的情况】布隆过滤器的误判率为:" + percentFormat.format((float)mightContainNumber2 / 5000000));
【key真实存在的情况】布隆过滤器认为存在的key值数:500
================================================================================
【key不存在的情况】布隆过滤器认为存在的key值数:150591
【key不存在的情况】布隆过滤器的误判率为:3.01%
Java集成Redis使用布隆过滤器
pom中引入redisson依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.1</version>
</dependency>
编写代码测试
public void patchingConsum(ConsumPatchingVO vo) throws ParseException {
Config config = new Config();
SingleServerConfig singleServerConfig = config.useSingleServer();
singleServerConfig.setAddress("redis://127.0.0.1:6379");
singleServerConfig.setPassword("123456");
RedissonClient redissonClient = Redisson.create(config);
RBloomFilter<String> bloom = redissonClient.getBloomFilter("name");
// 初始化布隆过滤器; 大小:100000,误判率:0.01
bloom.tryInit(100000L, 0.01);
// 新增10万条数据
for(int i=0;i<100000;i++) {
bloom.add("name" + i);
}
// 判断不存在于布隆过滤器中的元素
List<String> notExistList = new ArrayList<>();
for(int i=0;i<100000;i++) {
String str = "name" + i;
boolean notExist = bloom.contains(str);
if (notExist) {
notExistList.add(str);
}
}
if ($.isNotEmpty(notExistList) && notExistList.size() > 0 ) {
System.out.println("误判次数:"+notExistList.size());
}
}