Redis学习笔记(上)
学习视频【狂神说Java】Redis最新超详细版教程通俗易懂
即使再小的帆也能远航
目录
什么是基数?
基数(cardinal number)在数学上,是集合论中刻画任意集合大小的一个概念。两个能够建立元素间一一对应的集合称为互相对等集合。例如3个人的集合和3匹马的集合可以建立一一对应,是两个对等的集合。
简介
Redis在2.8.9版本更新了Hyperloglog数据结构
Redis Hyperloglog是做基数统计的算法
有点:占用的内存是固定的,2^64不同的元素的技术,只需要费12kb的内存,如果要从内存角度考虑,Hyperloglog是首选,大概有0.81的错误率
网页的UV(Unique Visitor,独立访客数)–> 一个人访问网站多次,任然算作一个人
传统方式:使用set集合保存用户的ID,然后就可以统计set集合中元素的数量作为判断标准
这种方法如果保存大量的用户ID,会十分耗内存,我们的目的是计数而不是记录用户ID
命令
pdadd key element [element]
:向Hyperloglog中加入element
pfcount key
:返回Hyperloglog的个数
pfmerge destkey sourcekey [sourcekey ...]
:将多个名为sourcekey的HyperLogLog合并为一个名为destkey的HyperLogLog
测试
pi:0>pfadd mykey a b c d e f
"1"
pi:0>pfcount mykey
"6"
pi:0>pfadd mykey2 a b c d e f g h i j
"1"
pi:0>pfmerge hy mykey mykey2
"OK"
pi:0>pfcount hy
"10"
如果允许容错,就使用这个
如果不允许容错,就使用set
位存储
统计用户信息,活跃|不活跃,登录|未登录,打卡|未打卡 => 这种两个状态的都可以使用Bitmap存储
Bitmaps,位图,一种数据结构,都是操作二进制来进行记录,就只有0和1两个状态
例如:365天 -> 365位,1B = 8bit,46个字节左右就可以存储一个人一年的打卡情况
命令
setbit key offset value
:设置名为key的Bitmap在第offset位的值为value
getbit key offset
:获取名为key的Bitmap在第offset位的值
bitcount key [start end]
:获取Bitmap的中值为1的位数
测试
pi:0>setbit sign 0 0
"0"
pi:0>setbit sign 1 1
"0"
pi:0>setbit sign 2 1
"0"
pi:0>setbit sign 3 0
"0"
pi:0>setbit sign 4 0
"0"
pi:0>setbit sign 5 1
"0"
pi:0>setbit sign 6 1
"0"
pi:0>getbit sign 5
"1"
pi:0>getbit sign 4
"0"
pi:0>bitcount sign
"4"
参考官网:http://www.redis.cn/topics/data-types-intro.html#bitmaps
要不同时成功,要不同时失败 -> 原子性
Redis单条命令是保证原子性,但Redis的事务不保证原子性,Redis的事务没有隔离级别的概念,所以命令在事务中,没有直接执行,只有发起执行命令才会执行
Redis事务本质:一组命令的集合,一个事务中的所有命令都会被序列化,在事务的执行过程中,会按顺序执行。
一致性,顺序性,排他性
Redis的事务:
pi:0>multi
"OK"
pi:0>set k1 v1
"QUEUED"
pi:0>set k2 v2
"QUEUED"
pi:0>get k2
"QUEUED"
pi:0>set k3 v3
"QUEUED"
pi:0>exec
1) "OK"
2) "OK"
3) "v2"
4) "OK"
pi:0>multi
"OK"
pi:0>set k1 v1 k2 v2 k3 v3
"QUEUED"
pi:0>discard
"OK"
pi:0>get k1
null
命令错误,事务中的所有命令都不会执行
pi:0>multi
"OK"
pi:0>set k1 v1 k2 v2 k3 v3
"QUEUED"
pi:0>getset k4
"ERR wrong number of arguments for 'getset' command"
pi:0>set k5 v5
"QUEUED"
pi:0>exec
"EXECABORT Transaction discarded because of previous errors."
pi:0>get k1
null
如果事务队列中存在存在语法性异常,那么执行命令的时候,其他命令是可以正常执行的,错误命令会抛出异常
pi:0>set k1 v1
"OK"
pi:0>multi
"OK"
pi:0>incr k1
"QUEUED"
pi:0>set k2 v2
"QUEUED"
pi:0>exec
1) "ERR value is not an integer or out of range"
2) "OK"
pi:0>get k1
"v1"
pi:0>get k2
"v2"
乐观锁
悲观锁
watch key [key ...]
:监视key
正常执行成功
pi:0>set money 100
"OK"
pi:0>set out 0
"OK"
pi:0>watch money
"OK"
pi:0>multi
"OK"
pi:0>decrby money 20
"QUEUED"
pi:0>incrby out 20
"QUEUED"
pi:0>exec
1) "80"
2) "20"
失败
测试多线程修改值,使用watch可以当作Redis的乐观锁操作
# 线程一
pi:0>set money 100
"OK"
pi:0>set out 0
"OK"
pi:0>watch money
"OK"
pi:0>multi
"OK"
pi:0>decrby money 10
"QUEUED"
pi:0>incrby out 10
"QUEUED"
pi:0>exec
(nil)
pi:0>
# 线程二
pi:0>get money
"100"
pi:0>incrby money 100
"200"
如果修改失败,获取最新值再次执行即可
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ashTnm5C-1594539903502)(741848070B614E4599C7DCE234DDEF44)]
使用Java操作客户端
Jedis是Redis官方推荐的操作Redis的jar包,那么一定要对Jedis十分的熟悉
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
@Test
void Test01() {
//1. new一个对象
Jedis jedis = new Jedis(new HostAndPort("192.168.0.105", 6379));
//Jedis的所有的方法就是Redis的命令
String ping = jedis.ping();
System.out.println(ping);
jedis.close();
}
@Test
void TestKey() {
Jedis jedis = new Jedis(new HostAndPort("192.168.0.105", 6379));
System.out.println("清空数据:" + jedis.flushDB());
System.out.println("判断某个键是否存在" + jedis.exists("username"));
System.out.println("新增键值对" + jedis.set("username", "Esion"));
System.out.println("新增键值对" + jedis.set("password", "123456"));
System.out.println("系统中所有的键值对:" + jedis.keys("*"));
System.out.println("删除键password:" + jedis.del("password"));
System.out.println("判断password是否存在:" + jedis.exists("password"));
System.out.println("判断键username的值的类型:" + jedis.type("username"));
System.out.println("随机返回key空间中的一个:" + jedis.randomKey());
System.out.println("重命名key:" + jedis.rename("username", "nickname"));
System.out.println("取出改后的nickname:" + jedis.get("nickname"));
System.out.println("按索引查询:" + jedis.select(0));
System.out.println("删除当前数据库中所有的key:" + jedis.flushDB());
System.out.println("返回当前数据库中key的数目:" + jedis.dbSize());
System.out.println("删除所有数据库中所有的key:" + jedis.flushAll());
}
//结果
/*
清空数据:OK
判断某个键是否存在false
新增键值对OK
新增键值对OK
系统中所有的键值对:[password, username]
删除键password:1
判断password是否存在:false
判断键username的值的类型:string
随机返回key空间中的一个:username
重命名key:OK
取出改后的nickname:Esion
按索引查询:OK
删除当前数据库中所有的key:OK
返回当前数据库中key的数目:0
删除所有数据库中所有的key:OK
*/
@Test
void TestString() {
Jedis jedis = new Jedis(new HostAndPort("192.168.0.105", 6379));
jedis.flushAll();
System.out.println("======增加数据======");
System.out.println(jedis.set("key1", "value1"));
System.out.println(jedis.set("key2", "value2"));
System.out.println(jedis.set("key3", "value3"));
System.out.println("删除键key2:" + jedis.del("key2"));
System.out.println("获取键key2:" + jedis.get("key2"));
System.out.println("修改key1的值:" + jedis.set("key2", "newvalue2"));
System.out.println("获取key1的值:" + jedis.get("key1"));
System.out.println("在key3后面加入值" + jedis.append("key3", "_new"));
System.out.println("获取key3的值:" + jedis.get("key3"));
System.out.println("增加多个键值对:" + jedis.mset("key4", "value4", "key5", "value5"));
System.out.println("获取多个键值对" + jedis.mget("key1", "key2", "key3"));
System.out.println("删除多个键值对:" + jedis.del("key1", "key2"));
System.out.println("======新增键值对防止覆盖旧的值======");
System.out.println(jedis.setnx("key01", "value01"));
System.out.println(jedis.setnx("key02", "value02"));
System.out.println(jedis.setnx("key02", "new_value02"));
System.out.println(jedis.get("key01"));
System.out.println(jedis.get("key02"));
System.out.println("======新增键值对并设置有效时间======");
System.out.println(jedis.setex("key001", 3, "value001"));
System.out.println(jedis.get("key001"));
try {
Thread.sleep(3);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println(jedis.get("key001"));
System.out.println("获取key02中的子串:" + jedis.getrange("key02", 2, 3));
}
// console
/*
======增加数据======
OK
OK
OK
删除键key2:1
获取键key2:null
修改key1的值:OK
获取key1的值:value1
在key3后面加入值10
获取key3的值:value3_new
增加多个键值对:OK
获取多个键值对[value1, newvalue2, value3_new]
删除多个键值对:2
======新增键值对防止覆盖旧的值======
1
1
0
value01
value02
======新增键值对并设置有效时间======
OK
value001
value001
获取key02中的子串:lu
*/
@Test
void TestList() {
Jedis jedis = new Jedis(new HostAndPort("192.168.0.105", 6379));
jedis.flushAll();
System.out.println("======添加一个List======");
System.out.println(jedis.lpush("list", "value1", "value2", "value3"));
System.out.println(jedis.lpush("list", "value2"));
System.out.println(jedis.lpush("list", "value4"));
System.out.println("list的内容:" + jedis.lrange("list", 0, -1));
System.out.println("list0-2的元素:" + jedis.lrange("list", 0, 2));
System.out.println("============");
System.out.println("删除列表指定的值,第二个参数为删除的个数(有重复时),后add进去的值会被先删掉,类似于出栈");
System.out.println("删除指定元素个数:" + jedis.lrem("list", 1, "value2"));
System.out.println("list的内容:" + jedis.lrange("list", 0, -1));
System.out.println("删除下标0-3之外的元素:" + jedis.ltrim("list", 0, 3));
System.out.println("list的内容:" + jedis.lrange("list", 0, -1));
System.out.println("list列表出栈,左端:" + jedis.lpop("list"));
System.out.println("list的内容:" + jedis.lrange("list", 0, -1));
System.out.println("list添加元素,从列表的右端,与lpush对应:" + jedis.rpush("list", "value5"));
System.out.println("list的内容:" + jedis.lrange("list", 0, -1));
System.out.println("list列表出栈,右端:" + jedis.rpop("list"));
System.out.println("list的内容:" + jedis.lrange("list", 0, -1));
System.out.println("修改list指定下标为1的内容:" + jedis.lset("list", 1, "newvalue2"));
System.out.println("list的内容:" + jedis.lrange("list", 0, -1));
System.out.println("============");
System.out.println("list的长度:" + jedis.llen("list"));
System.out.println("获取list指定下标为2的内容:" + jedis.lindex("list", 2));
System.out.println("============");
jedis.lpush("sort", "3", "1", "4", "5", "2", "6");
System.out.println("sort排序前:" + jedis.lrange("sort", 0, -1));
System.out.println(jedis.sort("sort"));
System.out.println("sort排序后:" + jedis.lrange("sort", 0, -1));
}
// 结果
/*
======添加一个List======
3
4
5
list的内容:[value4, value2, value3, value2, value1]
list0-2的元素:[value4, value2, value3]
============
删除列表指定的值,第二个参数为删除的个数(有重复时),后add进去的值会被先删掉,类似于出栈
删除指定元素个数:1
list的内容:[value4, value3, value2, value1]
删除下标0-3之外的元素:OK
list的内容:[value4, value3, value2, value1]
list列表出栈,左端:value4
list的内容:[value3, value2, value1]
list添加元素,从列表的右端,与lpush对应:4
list的内容:[value3, value2, value1, value5]
list列表出栈,右端:value5
list的内容:[value3, value2, value1]
修改list指定下标为1的内容:OK
list的内容:[value3, newvalue2, value1]
============
list的长度:3
获取list指定下标为2的内容:value1
============
sort排序前:[6, 2, 5, 4, 1, 3]
[1, 2, 3, 4, 5, 6]
sort排序后:[6, 2, 5, 4, 1, 3]
*/
其他的一样,跳过(所有的API就是命令)
@Test
void TestTransaction() {
Jedis jedis = new Jedis(new HostAndPort("192.168.0.105", 6379));
jedis.flushAll();
//开启事务
Transaction transaction = jedis.multi();
JSONObject json = new JSONObject();
json.set("name", "esion");
json.set("age", 22);
//jedis.watch("user:1");
try {
transaction.set("user:1", json.toString());
transaction.set("user:2", json.toString());
int i = 1 / 0;//这个地方会发生异常,事务执行失败
transaction.exec();
} catch(Exception e) {
transaction.discard();
System.err.println(e.getClass() + e.getMessage());
} finally {
System.out.println(jedis.get("user:1"));
System.out.println(jedis.get("user:2"));
jedis.close();
}
}
// 结果
/*
class java.lang.ArithmeticException/ by zero
null
null
*/
在SPringboot2.x之后,原来使用的Jedis被替换为lettuce
Jedis:底层采用的是直连,如果有多个线程操作是不安全的,如果想避免,就使用Jedis Pool
lettuce:底层使用netty,实例可以再过个线程中共享,不存在线程不安全的状况,可以减少线程数量
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
@Bean
//如果Bean不存在则生效,所以我们可以自己定义一个redisTemplate来替换默认的redisTemplate
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
//默认的RedisTemplate,没有过多的设置,Redis对象都是需要序列化的
//两个泛型类型都是Object,后面需要强制转换,我们洗完是String
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
//由于String类型是最长使用的,
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
spring:
redis:
host: 192.168.0.105
port: 6379
@SpringBootTest
class RedisApplicationTests {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Test
void contextLoads() {
//redisTemplate操作不同的数据类型,API和命令是一样的
//opsForCluster
//opsForGeo
//opsForHash
//opsForHyperLogLog
//opsForList
//opsForStream
//opsForValue -> 操作字符串
//opsForZSet
//处理基本的操作,常用的方法可以直接通过redisTemplate操作,比如事务和基本的增删改查
redisTemplate.opsForValue().set("key", "value");
System.out.println(redisTemplate.opsForValue().get("key"));
//获取连接操作
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
connection.flushAll();
connection.flushDb();
}
}
源码截取
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {
private boolean enableTransactionSupport = false;
private boolean exposeConnection = false;
private boolean initialized = false;
private boolean enableDefaultSerializer = true;
private @Nullable RedisSerializer<?> defaultSerializer;
private @Nullable ClassLoader classLoader;
//配置序列化
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer keySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer valueSerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashKeySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashValueSerializer = null;
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
boolean defaultUsed = false;
if (defaultSerializer == null) {
//默认的序列化是JDK序列化,我们可能使用JSON来序列化
defaultSerializer = new JdkSerializationRedisSerializer(
classLoader != null ? classLoader : this.getClass().getClassLoader());
}
if (enableDefaultSerializer) {
if (keySerializer == null) {
keySerializer = defaultSerializer;
defaultUsed = true;
}
if (valueSerializer == null) {
valueSerializer = defaultSerializer;
defaultUsed = true;
}
if (hashKeySerializer == null) {
hashKeySerializer = defaultSerializer;
defaultUsed = true;
}
if (hashValueSerializer == null) {
hashValueSerializer = defaultSerializer;
defaultUsed = true;
}
}
if (enableDefaultSerializer && defaultUsed) {
Assert.notNull(defaultSerializer, "default serializer null and not all serializers initialized");
}
if (scriptExecutor == null) {
this.scriptExecutor = new DefaultScriptExecutor<>(this);
}
initialized = true;
}
}
如果没有实现序列化接口,汇报异常
@getter
@setter
@toString
public class User {
private String username;
private String password;
}
@Test
void TestObject() throws JsonProcessingException {
User user = new User("admin", "admin");
// 真实的开发都是使用JSON来保存对象
String json = new ObjectMapper().writeValueAsString(user);
redisTemplate.opsForValue().set("user", user);
System.out.println(redisTemplate.opsForValue().get("user"));
}
/*
org.springframework.data.redis.serializer.SerializationException: Cannot serialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer; nested exception is java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.qsd.redis.po.User]
*/
如果实现序列化
// 在企业中,实体类都会实现序列化接口
public class User implements Serializable {
private static final long serialVersionUID = -4869333122434031467L;
private String username;
private String password;
}
/*
User [username=admin, password=admin]
*/
自定义序列化
@Configuration
public class RedisConfig {
//编写自定义RedisTemplate
//写好的固定模板,在企业中可以直接使用
@Bean(value = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
//为了开发方便,一般采用
RedisTemplate<String, Object> template = new RedisTemplate<>();
//一定要先设置,否则会报错
template.setConnectionFactory(redisConnectionFactory);
//配置具体的序列化方法
//JSON的序列化
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(objectMapper);
//String的序列化
StringRedisSerializer serializer2 = new StringRedisSerializer();
//key采用StringRedisSerializer序列化
template.setKeySerializer(serializer2);
//hash的key也采用StringRedisSerializer序列化
template.setHashKeySerializer(serializer2);
//value采用Jackson2JsonRedisSerializer序列化
template.setValueSerializer(serializer);
//hash的value采用Jackson2JsonRedisSerializer序列化
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
具体方式参考springboot redis 项目实战 完整篇
Redis的操作对于一个java程序员来说十分的简单,所以更应该学习Redis的原理和理解Redis的思想,还有数据类型的使用场景
连接:Redis配置 - 菜鸟教程
# units are case insensitive so 1GB 1Gb 1gB are all the same.
# 对大小写不敏感
################################## INCLUDES ###################################
# include /path/to/local.conf
# include /path/to/other.conf
# 可以包含其他配置文件
################################## MODULES #####################################
################################## NETWORK #####################################
bind 127.0.0.1 192.168.0.105
# 绑定ip,注意,这个ip是你的网卡ip,通过ip判断允许那个网卡可以访问,如果想限制ip就只能防火墙
protected-mode yes
# 是否是受保护的模式,一般开起
port 6379
# 绑定端口号
################################# TLS/SSL #####################################
################################# GENERAL #####################################
daemonize yes
# 是否开启守护进程,默认no,前台运行,如果yes则后台运行
pidfile /var/run/redis_6379.pid
# 如果以后台方式运行,我们就需要制定一个pid文件
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably) 生产环境使用
# warning (only very important / critical messages are logged)
loglevel notice
# 日志级别
logfile ""
# 生成的日志文件名
databases 16
# 数据库的数量,默认16个
always-show-logo yes
# 是否总是显示logo
################################ SNAPSHOTTING ################################
# 快照,持久化,在规定的时间内执行了多少次操作,就会被持久化
# Redis是个内存数据库,如果不持久化,断电即失去
save 900 1
# 如果900秒内,至少有1个key被修改,就进行持久化
save 300 10
# 如果300秒内,至少有10个key被修改,就进行持久化
save 60 10000
# 如果60秒内,至少有10000个key被修改,就进行持久化
stop-writes-on-bgsave-error yes
# 持久化出错后是否继续工作,默认开启
rdbcompression yes
# 是否压缩rdb文件,需要消耗一些CPU资源
rdbchecksum yes
# 保存rdb文件时,进行检查校验
dbfilename dump.rdb
# rdb文件名
dir ./
# rdb文件保存位置
################################# REPLICATION #################################
# 复制,后面讲解主从复制再进行讲解
############################### KEYS TRACKING #################################
################################## SECURITY ###################################
# 安全相关
# requirepass foobared
# 默认不设置密码,一般不在配置文件中设置,可以使用命令设置:config set requirepass 123456
#####################################################################
pi@raspberrypi:/opt/redis $ bin/redis-cli
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) ""
127.0.0.1:6379> config set requirepass 123456
OK
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "123456"
127.0.0.1:6379> exit
pi@raspberrypi:/opt/redis $ bin/redis-cli
127.0.0.1:6379> ping
(error) NOAUTH Authentication required.
127.0.0.1:6379> auth 123456
OK
127.0.0.1:6379> ping
PONG
#####################################################################
# 限制 -> 了解
################################### CLIENTS ####################################
# maxclients 10000
# 设置最大连接客户端数量
############################## MEMORY MANAGEMENT ################################
# maxmemory
# 配置最大的内存容量
# maxmemory-policy noeviction [连接]
# 内存到达最大值的处理策略
############################# LAZY FREEING ####################################
############################### THREADED I/O #################################
############################## APPEND ONLY MODE ###############################
# AOF配置
appendonly no
# 默认不开启aof模式,默认使用rdb持久化,因为在所有的情况下rdb够用了
appendfilename "appendonly.aof"
# aof文件名
# appendfsync always
# 每次修改都会同步
appendfsync everysec
# 每秒执行一次sync,可能会丢失这一秒的数据
# appendfsync no
# 不执行同步,这个时候操作系统自己同步数据,速度最快
# aof在aof时详解
连接:redis 设置过期Key 的 maxmemory-policy 六种方式