Redis主要有5种数据类型:String,List,Set,Zset,Hash
应用场景
1)缓存
set(k,v)
get(k)
2)计数、分布式ID
incr(k)
get(k)
3)分布式锁
setNx
del
应用场景
1)对象
Map<Integer, String> skuIdAndNameMap = new HashMap<>();
skuIdAndNameMap.put(1, "苹果");
skuIdAndNameMap.put(2, "香蕉");
skuIdAndNameMap.put(3, "橘子");
skuIdAndNameMap.forEach((field, value) -> jedis.hset("323-20240909", field, value));
Map<Integer, String> map = jedis.hgetAll("323-20240909");
String name = jedis.hget("323-20240909", "3");// 橘子
粉丝列 表、评论列表;分布式队列:可以通过 lpush 和 rpop 写入和读取消息、或者将库存存入,然后扣减
交集、并 集、差集 的操作,两个人的共同好友
去重、排序, 获取排名前几名的用户
1、计数器
2、自增ID
2、缓存
3、分布式锁
缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:布隆过滤器(Bloom Filter),先用布隆过滤器判断下,如果不存在,直接不用查了
并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
过期时间打散
set("mjp", String.valueOf(18),
30 + ThreadLocalRandom.current().nextInt(10), TimeUnit.SECONDS);
在setNx时,出现异常,异常
e.getMessage().contains("ReadTimeout") {
// redis服务端,已经加锁成功了,只不过在返回给客户端的时候,超时了,这种场景我们认为是加锁成功了
}
1、思路
读的时候,先读缓存,缓存没有的话,就读db,然后取出数据后放入缓存,同时返回响应
更新的时候,先更新db,然后再删除缓存
2、问题
如果删除缓存失败了,那么会导致db是新数据,缓存中是旧数据,数据就出现了不一致
3、解决
归根原因是,CAP中的C一致性
如果业务可以接受:最终一致性,则
putToDB(key,value);
deleteFromRedis(key);
// 数秒后重新执行删除操作,时间 = 读业务逻辑数据的耗时 + 几百毫秒
deleteFromRedis(key,5);
把删除失败的key放到消息队列,重试删除
分布式事务,参考:分布式事务-队列实现最终一致性
1、背景
锁资源粒度,将100个库存,分别放在redis的10个list中
2、下单,扣减库存(先更新db,再更新缓存)
ListOperations<String, String> list = redisTemplate.opsForList();
String val = list.leftPop("key");
if (StringUtils.isEmpty(val)) {
// 已卖完
} else {
// 扣减db
}
3、少买问题
Redis扣减了100个,但是db90个扣减成功,10个扣减失败。少买了10个品
4、解决
本质是数据一致性问题,即分布式事务问题
可以catch住db异常,然后将redis的库存补回
//获取库存数目
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
//库存数目大于零则减库存
if(stock > 0){
int finalStock = stock - 1;
//更新库存
redisTemplate.opsForValue().set("stock",Integer.toString(finalStock));
}
1、背景
2、解决:
分布式锁
1、背景
2、解决
分布式锁,在缓存和db之间加分布式锁
// 读缓存
String key = "key";
String val = redisTemplate.opsForValue().get(key);
// 缓存中无数据,则可能击穿
if (StringUtils.isEmpty(val)) {
// 1.加分布式锁
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, "v");
// 2、获取锁成功
if (success) {
// 3.再读一次缓存
val = redisTemplate.opsForValue().get(key);
if (StringUtils.isNoneEmpty(val)) {
return val;
} else {
// 读db
// 回写缓存
return "查询结果";
}
}
} else {
return val;
}
1、背景
2、解决
只要是加锁,就必须设置锁过期时间,否则一旦服务器宕机,未释放锁,其它服务器,都没有机会再获取到此锁,再处理了
1、背景
// 1.加锁-setNx
// 2.执行业务
// 3.释放锁-delete
t1,setNx(k1,v1),并设置锁超时时间为3s
此时t2,进来
此时t1,执行完业务,然后释放锁。此时t1释放的是t2的锁了
导致锁失效
这里有两个问题
问题1:t1线程未执行完,锁就过期了,对于t1而言,本质相当于没加锁,对于共享数据,相当于没加锁
问题2:发生了问题1的情况下,t1释放了t2的锁,是另一个问题,解铃必须系铃人!
这里如果没有问题1,就不会有问题2
2、解决
在加锁,返回true之前:
Timer定时器 + lua脚本为锁续期
private boolean lock(String key, String val, Long expire) {
stringRedisTemplate.opsForValue().set(key, val, expire, TimeUnit.SECONDS);
renewExpire(key,val,expire, 10, 0);
return true;
}
private void renewExpire(String key, String val, Long expire, int maxRetries, int retryCount) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
// KEYS[1]即key、ARGV[1]即val(一般是我们设置的uuid-解铃还须系铃人)、expire锁过期时间
String lua = "if redis.call('exists', KEYS[1], ARGV[1]) == 1 then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end";
DefaultRedisScript<Boolean> script = new DefaultRedisScript<>(lua, Boolean.class);
Boolean executed = stringRedisTemplate.execute(script, Collections.singletonList(key), val, String.valueOf(expire));
if (executed) {
// 如果锁存在,lua重置锁过期时间成功了,则延时1/3过期时间后,再执行一次
// 加锁时,设置了过期时间为30s,返回true加锁成功之前,执行此方法。
// 此方法,延时1/3时间执行run任务,即10s后执行任务将锁的过期时间重置为30s
// 下一次再延时10s,执行run任务
// 直到业务方手动释放了锁,lua脚本执行返回false,就不会再执行renewExpire了
if (retryCount <= maxRetries) {
renewExpire(key, val, expire, maxRetries, retryCount+1);
}
}
}
}, expire * 1000 / 3); // 延时(1/3的过期时间)执行一次
}
1、背景
1)场景1
上述锁过期问题,会发生释放别人的锁
2)执行释放锁-恶意解锁
3)防御式编程
某些场景下加锁失败,只剩下释放锁逻辑,就会释放别人的锁
代码问题导致
// 场景1下加锁成功
// 场景2下加锁失败了
finally中释放锁
t1,场景2下加锁失败了,t2进来,加锁成功,t1执行释放锁。t2加的锁被别人释放了
2、解决
只能释放自己加的锁
public void t(String key){
ThreadLocalRandom random = ThreadLocalRandom.current();
UUID uuid = new UUID(random.nextLong(), random.nextLong());
String val = String.valueOf(uuid);
try {
Boolean lock = redisTemplate.opsForValue().setIfAbsent(key, val, 3, TimeUnit.SECONDS);
if (lock) {
// 业务
}
} finally {
val = redisTemplate.opsForValue().get(key);
if (StringUtils.isNotEmpty(val) && val.contains(uuid.toString())) {
redisTemplate.delete(key);
}
}
}
生成uuid
setNx时,val加个前缀uuid
释放时,先判断k-对应的val前缀为此uuid,再删除
3、存在问题
判断是自己的,刚准备删除时,锁已经过期释放了
然后再执行删除锁,此时锁刚好被别人获取了,相当于还是删除了别人的锁
根本原因:判断 + 删除锁 ,不是原子操作
4、解决
将判断 + 删除,使用lua脚本打包。lua脚本将二者一次性发送给redis,对于redis而言这个lua脚本就是一个指令
finally{
// 使用Lua脚本释放锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
// 执行脚本
Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), val);
// 根据结果判断锁是否释放成功
if (result == 0L) {
// 锁不属于当前客户端,释放失败
} else {
// 锁释放成功
}
}
此时完整的加锁 、续约锁时间、 释放锁流程
public void t() throws Exception {
String key = "mjp";
ThreadLocalRandom random = ThreadLocalRandom.current();
UUID uuid = new UUID(random.nextLong(), random.nextLong());
String val = String.valueOf(uuid);
Long expire = 30L;
try {
lock(key, val, expire);
TimeUnit.SECONDS.sleep(20);
} finally {
// 使用Lua脚本释放锁
String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript<Long> script = new DefaultRedisScript<>(lua, Long.class);
// 执行脚本
Long result = stringRedisTemplate.execute(script, Collections.singletonList(key), val);
// 根据结果判断锁是否释放成功
if (result == 0L) {
// 锁不属于当前客户端,释放失败
} else {
// 锁释放成功
System.out.println("/success");
}
}
}
// 加锁 和 重置超时
private boolean lock(String key, String val, Long expire) {
stringRedisTemplate.opsForValue().set(key, val, expire, TimeUnit.SECONDS);
renewExpire(key,val,expire, 10, 0);
return true;
}
private void renewExpire(String key, String val, Long expire, int maxRetries, int retryCount) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
// KEYS[1]即key、ARGV[1]即val(一般是我们设置的uuid-解铃还须系铃人)、expire锁过期时间
String lua = "if redis.call('exists', KEYS[1], ARGV[1]) == 1 then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end";
DefaultRedisScript<Boolean> script = new DefaultRedisScript<>(lua, Boolean.class);
Boolean executed = stringRedisTemplate.execute(script, Collections.singletonList(key), val, String.valueOf(expire));
if (executed) {
// 如果锁存在,lua重置锁过期时间成功了,则延时1/3过期时间后,再执行一次
// 加锁时,设置了过期时间为30s,返回true加锁成功之前,执行此方法。
// 此方法,延时1/3时间执行run任务,即10s后执行任务将锁的过期时间重置为30s
// 下一次再延时10s,执行run任务
// 直到业务方手动释放了锁,lua脚本执行返回false,就不会再执行renewExpire了
if (retryCount <= maxRetries) {
renewExpire(key, val, expire, maxRetries, retryCount+1);
}
}
}
}, expire * 1000 / 3); // 延时(1/3的过期时间)执行一次
}
1、问题描述
t1
server2-选举为新master
t2
2、解决:
RedLock红锁
1、背景
redis中red即红锁,专门用于解决集群单节点故障,导致的锁失效问题
redis集群,节点之间是独立的,无master、slaver
1)应用程序,系统当前时间,curTime
2)使用setNx并设置超时时间,依次从每个redis server中尝试获取锁
3)判断获取锁是否成功
依次从server1-server5执行setNx后,应用程序,系统当前时间 = newCurTime
diff = newCurTime - curTime
当条件1和条件2都满足时,才认为获取锁成功
3、特点
实际中不易实现、且非主从架构
官网中文文档、官网离线
1、pom
code栏下,快速开启指南
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.1</version>
</dependency>
2、程序方式配置
Wiki栏下,配置
1)单节点-自己主机
2)单节点-其它主机
3)主从集群
RedissonClient
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
// 单节点-主机
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
return Redisson.create(config);
}
}
3、使用分布式锁锁
Wiki栏下,分布式锁和同步器
@Resource
private RedissonClient redissonClient;
@Test
public void t() throws Exception {
String key = "mjp";
long expire = 30L;
RLock lock = redissonClient.getLock(key);
lock.lock(expire, TimeUnit.SECONDS);
}
lock ->RedissonLock#lock -> tryAcquire -> tryAcquireAsync -> tryLockInnerAsync
底层就是lua脚本
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
lock ->RedissonLock#lock -> tryAcquire -> tryAcquireAsync -> scheduleExpirationRenewal -> renewExpiration
底层使用的netty的HashedWheelTimer,而非juc的Timer
正常的lua脚本执行renewExpirationAsync
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getRawName(), 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(getRawName()),
internalLockLeaseTime, getLockName(threadId));
}
if (res) {
// reschedule itself
renewExpiration();
} else {
cancelExpirationRenewal(null);
}
重置成功,则后续会再次重置,否则取消
unlock -> RedissonLock#unlock -> unlockAsync -> unlockInnerAsync -> RedissonLock#unlockInnerAsync
lua脚本
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end; " +
"return nil;",
Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
// 分布式
RSemaphore semaphore = redissonClient.getSemaphore("semaphore");
//限流的量,允许的并发量
semaphore.tryAcquire(10);
// 尝试获取资源(10个资源之一)
semaphore.acquire();
// 业务
// 释放资源
semaphore.release();
作用等效juc中的CountDownLatch,这里是分布式
// main线程
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
// 英雄联盟,一局游戏10个玩家
latch.trySetCount(10);
latch.await();//必须10个玩家都加载到100%,才能进入游戏画面
// 在其他线程或其他JVM里
// 其他每个玩家(10分之一),完成加载100%,则执行countDown
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();
@Resource
private RedissonClient redissonClient;
@GetMapping("/test/latch")
public String testLatch(){
RCountDownLatch cdl = redissonClient.getCountDownLatch("cdl");
cdl.trySetCount(5);
try {
cdl.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "5个人玩家都ready了";
}
@GetMapping("/test/countdown")
public String testCountdown(){
RCountDownLatch cdl = redissonClient.getCountDownLatch("cdl");
try {
TimeUnit.SECONDS.sleep(1);
cdl.countDown();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "一个玩家准备好了";
}
执行5次testCountdown方法,testLatch方法才会返回结果
public class RedissonRedLockExample {
public static void main(String[] args) {
// 配置多个 Redis 服务器
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://128.10.10.101:6379");
Config config2 = new Config();
config2.useSingleServer().setAddress("redis://127.0.0.1:6379");
// 创建多个 RedissonClient
RedissonClient redisson1 = Redisson.create(config1);
RedissonClient redisson2 = Redisson.create(config2);
// 创建多个 RLock 实例
RLock lock1 = redisson1.getLock("anyLock");
RLock lock2 = redisson2.getLock("anyLock");
// 使用 RedLock
RLock redLock = new RedissonRedLock(Arrays.asList(lock1, lock2));
try {
// 尝试获取锁,最多等待 100 秒,上锁以后 10 秒自动解锁
boolean res = redLock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
// 执行业务逻辑
} finally {
// 释放锁
redLock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// 关闭 RedissonClient
redisson1.shutdown();
redisson2.shutdown();
}
}
1、问题
在事务内部使用锁,锁在事务提交前释放了
2、解决
不要在事务内部使用锁
public void func() {
RLock lock = redissonClient.getLock(paymentOrder.getBizNo());
lock.lock();
try {
createPaymentOrderNoLock(paymentOrder);
} finally {
//释放锁
lock.unlock();
}
}
@Transactional
public void createPaymentOrderNoLock(PaymentOrder paymentOrder) {
// 双本地写
}
假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?
SCAN
命令时,你可以指定一个模式(pattern)来匹配key
1、独占排他(必须)
使用唯一键:lock_name
insert语句,insert失败支持重试
2、超时时间(必须有,否则客户端宕机,则死锁)
使用lock_time
3、可重入
funcA() {
// 加锁key成功
// 业务
funcB();
// 业务
}
funcB(){
// 加锁key也应该能成功,锁要满足可重入
}
使用tl字段,使用ThreadLocal存tl,保证一个线程可重入(线程id、重入次数)
4、释放锁(必须)
deleteById(insert成功后,会将主键id回写到DO中)
5、续时
总结
1、简易:mysql
2、性能:redis
3、可靠性:zk
参考:redis热点key的发现和解决
主要是三种方式: