一、Redis事务
1.1 Redis事务
1.1.1 Redis事务简介
Redis 事务的本质是一组命令的集合。在Redis中开启事务后,事务中的命令并不会立即执行,而是会推送到一个事务队列中,该队列积攒此次事务的所有命令,等到事务提交(执行)后,会逐步执行此队列中的命令,执行队列中的命令过程是一个整体,不会被其他客户端所干扰。在事务中如果有命令执行错误,那么此次事务队列中的所有命令取消。
- 开启事务,并创建队列
multi
- 执行事务
exec
- 取消事务
discard
1.1.2 watch监视
我们知道redis在开启事务时,其实就是创建了一个事务队列,此次事务所执行的所有命令将不会被立即执行,而是等事务执行之后(exec
),才会将队列中的命令一起执行。有时候我们希望在执行队列中的指令之前不允许其他客户端对某个值进行修改,那么此时需要就需要加锁。
- watch:监视某个key
watch key1 [key2……]
如果被监视的值在exec命令之前被其他客户端修改过了,那么此次事务队列中的命令全部失效。
- unwatch:取消监视的所有key
unwatch
tips:执行exec会释放所有被监视的key
1.1.3 Jedis实现Redis事务
在Jdis中提供有Transaction类,用于执行Redis事务相关指令;
示例代码:
package com.dfbz.demo01;
import org.junit.Before;
import org.junit.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Transaction;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01 {
private JedisPool pool;
@Before
public void before() {
pool = new JedisPool("192.168.18.155", 6379);
}
@Test
public void test1() {
Jedis jedis = pool.getResource();
// 开启事务
Transaction tx = jedis.multi();
try {
tx.set("name", "zhangsan");
int i = 1 / 0;
tx.set("age", "20");
// 执行事务
tx.exec();
} catch (Exception e) {
e.printStackTrace();
// 取消事务
tx.discard();
}
}
}
锁的监视:
@Test
public void test2() {
Jedis jedis = pool.getResource();
jedis.set("count", "0");
// 监视count
jedis.watch("count");
// 开启事务
Transaction tx = jedis.multi();
try {
tx.incr("count");
// 执行事务
tx.exec();
} catch (Exception e) {
e.printStackTrace();
// 取消事务
tx.discard();
}
}
@Test
public void test3() {
Jedis jedis = pool.getResource();
jedis.incr("count");
}
1.1.4 RedisTemplate 实现Redis事务
1)引入依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dfbz</groupId>
<artifactId>01_Redis_tx_lock</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.0.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>
2)启动类:
注意:RedisTemplate默认情况下是不支持事务的,需要手动开启;
测试类:
package com.dfbz.demo01;
// 取消事务
redisTemplate.discard();
System.out.println("事务取消!");
}
}
@Test
public void test3() { // 使用SessionCallback执行事务
redisTemplate.execute(new SessionCallback<String>() {
@Override
public String execute(RedisOperations redisOperations) throws DataAccessException {
try {
redisTemplate.opsForValue().set("count", 0);
// 开启key的监视
// redisTemplate.watch("count");
// 开启事务
redisTemplate.multi();
// 自增操作
redisTemplate.opsForValue().increment("count", 1);
// 执行事务
redisTemplate.exec();
return "事务执行成功!";
} catch (Exception e) {
e.printStackTrace();
// 取消事务
redisTemplate.discard();
return "事务取消!";
}
}
});
}
}
1.3 Redis 实现分布式锁
在并发编程中,我们通常通过锁来保证多个多线程操作同一资源的一致性,在Java中,提供有synchroized、Lock等来实现锁的功能,但Java中的锁只能保证在单体项目中(单个JVM进程中),无法应用在分布式环境下。
在分布式环境下,想要实现多个服务(多个进程)操作同一资源时,保证数据的一致性。可以使用redis来实现分布式锁。
1.3.1 Redis分布式锁场景
12306在售票时,是有多个售票窗口的,也就是售票的服务是有多个的,不管是在哪个售票微服卖出去的票都需要减掉库存中的余票,此时库存微服也是分布式的,即也存在多个减库存的微服接口,调用其中任意某个库存微服都可以实现减票操作。我们在单体项目时,只需将关键代码加上同步锁(多个售票窗口保证锁是同一个)。
伪代码如下:
public class Ticket implements Runnable {
//票数
private static Integer ticket = 100;
//锁对象
private static Object obj = new Object();
@Override
public void run() {
while (true) {
//加上同步代码块,把需要同步的代码放入代码块中,同步代码块中的锁对象必须保证一致!
synchronized (obj) {
if (ticket <= 0) {
break; //票卖完了
}
System.out.println(Thread.currentThread().getName() + "正在卖第: " + (101 - ticket) + "张票");
ticket--;
}
}
}
}
售票窗口:
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Ticket ticket=new Ticket();
//开启三个窗口,卖票
Thread t1=new Thread(ticket,"南昌西站");
Thread t2=new Thread(ticket,"南昌东站");
Thread t3=new Thread(ticket,"南昌站");
t1.start();
t2.start();
t3.start();
}
}
在单体项目中,以上代码是可以保证票数不会被超卖。但是在分布式环境中无法保证,原因是分布式的每台机器上都有各自的锁。
我们需要将锁抽取出来,所有的微服公用同一把锁,谁拿到了锁,谁就可以操作资源。没有拿到锁的只能等待。
1.3.2 Redis实现分布式锁
- 设置分布式锁
setnx lock-key value
示例:
setnx ticket_lock 1
说明:设置一个key,如果成功返回1,失败返回0
分布式锁案例代码:
while(true){
// 获取到了分布式锁才可以进行卖票操作
if("1".equals(redis.setnx("ticket_lock","1"))){
if (ticket <= 0) {
break; //票卖完了
}
System.out.println(Thread.currentThread().getName() + "正在卖第: " + (101 - ticket) + "张票");
ticket--;
redis.del("ticket_lock");
}else{
Thread.sleep(10);
}
}
1.3.3 分布式锁改良
上面设置的分布式锁,可以保证分布式环境下的资源一致问题,但是有一个非常大的弊端,那就是如果获取到分布式锁的那个微服出现故障(如宕机),那么锁将永远不会释放,造成锁的永远阻塞,其他窗口再也卖不出任何的票,因此为了防止这种现象,我们可以给分布式锁设置一个过期时间,如果时间到了那么锁自动释放。
expire lock-key second
expire ticket_lock 5
二、Redis的删除策略
2.1 定时删除策略
在设置键的过期时间的同时,Redis内部会创建一个定时器,当key设置有过期时间,且过期时间到达时,或者执行了del的key,由定时器任务立即执行对键的删除操作
-
优点:定时器时间到了就执行删除,能够有效快速释放内存空间,节约内存
-
缺点:定时器时间到了就执行删除,在这一时刻如果CPU正在处理其他命令,那么CPU的负载会一下子变得非常高,会直接影响Redis处理请求的响应速度。
总结:牺牲CPU性能来释放内存(时间换空间),定时删除策略是对内存清除最有效的方式;
2.2 惰性删除策略
数据到达过期时间不删除,等下次访问该数据时再删除。
-
优点:有效节约CPU性能,发现必须要删除时才删除。
-
缺点:如果数据过期后一直没有访问数据,那么将长期占用内存使用量,内存压力大
总结:牺牲内存使用量来换取CPU的性能(空间换时间)
2.3 定期删除策略
从定时删除和惰性删除策略来看,惰性删除浪费内存容易造成内存溢出,而定时删除影响CPU性能降低redis的响应速度和吞吐量。两种删除方式在不管是在CPU还是内存上都有些极端。定期删除策略则是一种折中办法。
redis底层采用的是定期删除+惰性删除策略
2.4 三种策略总结
- 定时删除(对内存清除最有效)
- 不管CPU当前使用率是否紧张,时间到了立马删除,内存压力小,CPU压力大,采用时间换空间策略
- 惰性删除(等到二次访问再删除)
- 等到确切删除的时候再删除,造成内存使用量浪费,CPU压力小,采用空间换时间策略
- 定期删除(采取智能抽取检查)
- 周期性删除,随机抽取一部分key删除,如发现占整体key的比例较大,则重新抽取一部分key删除。之后轮询下一个数据、下一台redis…
三、内存淘汰机制
我们知道Redis底层默认采用的是惰性删除+定期删除,如果定期删除没有删除的key,再加上也没有访问过这个key,那么redis内存的占用量会越来越高,假设此时来了新的数据,而redis的存储空间已经满了,那么此时redis的内存淘汰机制可以根据淘汰策略帮我删除一部分部分key来释放新的空间。
3.1 淘汰策略
- allkeys-lru:在16个数据库(前提你没有修改databases)的所有key中,选择使用次数最少的数据进行淘汰
- allkeys-lfu:在16个数据库的所有key中,选择最后一次使用离现在最长的数据进行淘汰
- allkeys-random:在16个数据库的所有key中,随机选择一部分数据进行淘汰
- volatile-lru:在所有有设置过期时间的key中,选择使用次数最少的数据进行淘汰
- volatile-lfu:在所有有设置过期时间的key中,选择最后一次使用离现在最长的数据进行淘汰
- volatile-ttl:在所有有设置过期时间的key中,选择将要过期的数据进行淘汰
- volatile-random:在所有有设置过期时间的key中,随机选择一部分数据进行淘汰
- no-envication:放弃淘汰数据,Redis4.0及以上版本默认策略
3.2 修改Redis淘汰策略
修改淘汰策略:
maxmemory-policy value
maxmemory-policy volatile-ttl
四、Redis高级数据类型
4.1 BitMaps
4.1.1 BitMaps 简介
Redis在2.2.0 版本之后添加了bitmaps操作,bitmaps事实上并不是一种新的数据类型,而是基于字符串位操作的集合,由于字符串是二进制安全的,并且最长可支持512M,所以它们可以用来存储2的32次方(512 1024 1024 * 8 )不同位的数据。
bitmaps的位操作分成两组:
- 1)把字符串的某个位设置为1或者0,或者获取某个位上的值
- 2)对于一组位的操作,对给定的比特范围内,统计设定值为1的数目。
bitmaps最大的优势是在存储数据时可以极大的节省空间,比如在一个项目中采用自增长的id来标识用户,就可以仅用512M的内存来记录40多亿用户的信息
tips:Bitmaps本身不是一种数据结构,他实际上就是一个可以进行位运算的字符串
4.1.2 BitMaps位图原理
假设我们存储了asd
字符串在redis当中,a的ASCII为97,对应的为115、100,那么BitMaps将其对应的二进制数据进行存储
getbit key offset
- 说明:获取指定key偏移量位置的二进制值。
- offset:偏移量
getbit命令示例:
127.0.0.1:6379> set test1 asd
OK
127.0.0.1:6379> getbit test1 0
(integer) 0
127.0.0.1:6379> getbit test1 1
(integer) 1
127.0.0.1:6379> getbit test1 2
(integer) 1
127.0.0.1:6379> getbit test1 3
(integer) 0
127.0.0.1:6379> getbit test1 4
(integer) 0
127.0.0.1:6379> getbit test1 5
(integer) 0
127.0.0.1:6379> getbit test1 6
(integer) 0
127.0.0.1:6379> getbit test1 7
(integer) 1
127.0.0.1:6379> getbit test1 8
(integer) 0
127.0.0.1:6379>
图解:
setbit key offset value
- 说明:设置key的指定偏移量上的值
- offset:索引偏移量(从0开始计算)
- value:具体的值(只能是0或1)
如果偏移量间隔过大,那么势必会造成中间补0现象较大
例如:
setbit test2 300 1
注意:在第一次setbit时, 如果偏移量非常大, 那么意味着前面的空出都需要0位来补充, 因此会造成Redis的阻塞。
bitcount key [start end]
- 说明:统计指定key的二进制位上的1的个数,不指定start和end就获取全部
- start:起始索引
- end:终止索引
127.0.0.1:6379> bitcount test2
(integer) 6
127.0.0.1:6379>
bitop operation destkey key [key...]
- 说明:用于多个BitMap做交集(and)、并集(or)、非(not)、异或(xor)操作,并将其结果计算保存到新的key中
- operation:操作类型
- and:交集
- or:并集
- not:非
- xor:异或
- destkey:存储计算的结果
and
与(&),取交集,有假为假
准备数据:
127.0.0.1:6379> setbit test3 0 1
(integer) 0
127.0.0.1:6379> setbit test3 1 1
(integer) 0
127.0.0.1:6379> setbit test3 2 1
(integer) 0
127.0.0.1:6379> setbit test3 3 0
(integer) 0
127.0.0.1:6379> setbit test4 0 1
(integer) 0
127.0.0.1:6379> setbit test4 1 0
(integer) 0
127.0.0.1:6379> setbit test4 2 1
(integer) 0
127.0.0.1:6379> setbit test4 3 1
(integer) 0
取and操作:
127.0.0.1:6379> bitop and and-test test3 test4
(integer) 1
127.0.0.1:6379> getbit and-test 0
(integer) 1
127.0.0.1:6379> getbit and-test 1
(integer) 0
127.0.0.1:6379> getbit and-test 2
(integer) 1
127.0.0.1:6379> getbit and-test 3
(integer) 0
or
或(|),取并集,按位非操作只能指定一个key,代表对哪个key进行"按位非"操作,有真为真
not
非(~),按位取反,1为0,0为1,按位取反时如果不够8的整数位后面的均会以0补齐。
xor
异或(^),相同为0,不同为1
4.1.3 Bitmaps应用场景
- 统计每天、每月网站的用户登录数量
- 统计每天、每月网站的用户访问数量
- 统计每天、每月文件被下载数
- 统计xxx-xxx时间内网站登录用户
- 统计本月未打卡人数
- 用于统计非真既假类的数据,不是0就是1
- …
案例:统计周期内用户登录的人数
setbit 登录日期 用户id 是否登录
127.0.0.1:6379> setbit pv:2020-0101 0 1
(integer) 0
127.0.0.1:6379> setbit pv:2020-0101 3 1
(integer) 0
127.0.0.1:6379> setbit pv:2020-0101 6 1
(integer) 0
127.0.0.1:6379> setbit pv:2020-0101 9 0
(integer) 0
127.0.0.1:6379> setbit pv:2020-0102 2 1
(integer) 0
127.0.0.1:6379> setbit pv:2020-0102 1 0
(integer) 0
127.0.0.1:6379> setbit pv:2020-0102 8 1
(integer) 0
统计2020年1月1日到2020年1月2日有多少用户登录过。
127.0.0.1:6379> bitop or pv:01-02 pv:2020-0101 pv:2020-0102
(integer) 2
127.0.0.1:6379> bitcount pv:01-02
(integer) 5
127.0.0.1:6379>
4.1.4 BitMap总结
bitmap操作起来这么复杂,为什么我们还要使用它呢?
因为bitmap存储单位是"位",咱们计算机最小的存储单位都是byte(字节),而一个字节是8个位,因此存储效率明显是bitmap高多了,而实际开发中id往往是Long类型进行存储(8个字节),8*8=64。因此存储效率可想而知,但由于bitmap直接操作的是"位",操作起来不方便,在开发中操作"位"给开发者带来一定的困难,需要花费一定的时间来操作,因此在数据结构的角度上来看,bitmap实质上是采取时间换空间的操作
- 优点:存储效率高
- 缺点
- 操作不便,给开发者带来一定的困难
- setbit偏移量较高时,需要补零,造成Redis阻塞(在此期间,Redis主线程将会被阻塞)。
4.2 HyperLogLog
4.2.1 HyperLogLog简介
Redis 在 2.8.9 版本添加了 HyperLogLog 结构。
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
2.2.2 HyperLogLog命令
- 添加数据
pfadd key val [val ...]
示例:
127.0.0.1:6379> pfadd a1 0
(integer) 1
127.0.0.1:6379> pfadd a1 1
(integer) 1
127.0.0.1:6379> pfadd a1 1
(integer) 0
127.0.0.1:6379> pfadd a1 2
(integer) 1
127.0.0.1:6379>
- 统计数据:每个key同样的val只会统计一次
pfcount key [key ...]
示例:
127.0.0.1:6379> pfcount a1
(integer) 3
127.0.0.1:6379>
- 合并数据:将多个heperloglog合并在一起,同样的key的val只会保留一个
pfmerge destkey sourcekey [sourcekey ....]
示例:
127.0.0.1:6379> pfadd a2 0
(integer) 1
127.0.0.1:6379> pfadd a2 1
(integer) 1
127.0.0.1:6379> pfadd a2 3
(integer) 1
127.0.0.1:6379> pfadd a2 3
(integer) 0
127.0.0.1:6379> pfadd a2 4
(integer) 1
127.0.0.1:6379> pfadd a2 4
(integer) 0
127.0.0.1:6379> pfmerge a3 a1 a2
OK
127.0.0.1:6379> pfcount a3
(integer) 5
127.0.0.1:6379>
4.2.3 HyperLogLog总结
应用场景:和bitmap相比,属于两种特定统计情况,简单来说,HyperLogLog 去重比 bitmap 方便很多
HyperLogLog有局限性,就是只能统计基数数量,而没办法去知道具体的内容是什么
- HyperLogLog是一种算法,并非redis独有
- 目的是做基数统计,不是集合,不会保存元数据,只记录数量而不是数值(也是与BitMaps的区别)。耗空间极小,支持海量数据统计。
- 核心是基数估算算法,主要表现为计算时内存的使用和数据合并的处理。最终数值存在一定误差
- 误差说明:基数估计的结果是一个带有 0.81% 标准错误(standard error)的近似值。是可接受的范围
- Redis 对 HyperLogLog 的存储进行了优化,pfadd命令并不是一次性分配12K内存使用,会随着基数的增加内存逐渐增大
4.3 GEO
3.3.1 GEO简介
GEO是Redis在3.2版本之后增加的针对于地理位置处理的一种数据类型。主要是用于存储坐标点的经度与维度、通过经度纬度计算两地距离等。
拾取坐标系统
4.3.2 GEO命令
添加坐标
geoadd key longitude latitude member [longitude latitude member ...]
- longitude:经度
- latitude:纬度
- member:地点名称
示例:获取"滕王阁"的经度纬度,添加到一个指定集合中。
geoadd nanchang 115.887667 28.687311 twg
获取坐标点
geopos key member [member ...]
示例:查看nanchang地理位置集合中twg的经度纬度
127.0.0.1:6379> geopos nanchang twg
1) 1) "115.88766485452651978"
2) "28.6873112518150819"
127.0.0.1:6379>
计算两点坐标距离
GEODIST key member1 member2 [unit]
- unit:单位
- km:千米
- m:米(默认)
- ml:英里
- ft:尺
示例:添加"八一广场"经度纬度,并计算"滕王阁"与"八一广场"之间的距离
127.0.0.1:6379> geoadd nanchang 115.910937 28.68009 bygc
(integer) 1
127.0.0.1:6379> geopos nanchang twg bygc
1) 1) "115.88766485452651978"
2) "28.6873112518150819"
2) 1) "115.91093569993972778"
2) "28.68008983123212374"
127.0.0.1:6379> geodist nanchang twg bygc
"2408.5712"
127.0.0.1:6379>
在指定位置集合中根据指定的经度纬度计算范围内的所有位置
georadius key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC]
- WITHCOORD:返回当前位置的经度纬度
- WITHDIST:返回当前位置与中心位置之间的距离
- WITHHASH:返回geohash编码
- ASC:按离中心距离的位置排序(升序),DESC(降序)
- COUNT:根据排序结构查询前几位
和georadius类似,georadiusbymember命令是以位置集合中某个位置为中心来查询范围内的位置
georadiusbymember key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC]