- 教程网址:
https://www.bilibili.com/video/BV1xA411G7wY?p=4&spm_id_from=pageDriver&vd_source=b63e9afd510deaf9d2a1b680368b9935
1. Redisson配置和使用
redis + son redis的儿子
- 导入pom包
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
- 配置
单机 集群 主从 复制 哨兵
@Configuration
public class RedissonConfig {
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
//集群
config.useClusterServers();
//哨兵
config.useSentinelServers();
//主从
config.useMasterSlaveServers();
//复制
config.useReplicatedServers();
- 使用
@Autowired
private Redisson redisson;
//controller
//封装了 如下
//redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
RLock lock = redisson.getLock(REDIS_LOCK);
lock.lock();
lock.unlock();
案例 RLock 和 Redisson
RLock lock = redisson.getLock(REDIS_LOCK);
lock.lock();
try {
String result = redis.opsForValue().get("goods:001");
//如果为null,就为0,否则 转成 int
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
//售卖了一个,还是这么多
int realNumber = goodsNumber - 1;
redis.opsForValue().set("goods:001", String.valueOf(realNumber));
String r = "成功买到商品,还剩:" + realNumber + "\t 服务端口为:" + serverPort;
System.out.println(r);
return r;
} else {
System.out.println("意外情况,已经售空等" + serverPort);
}
return "fail";
} finally {
//如果是被锁定的
if (lock.isLocked()) {
//如果是 当前线程持有的话
if (lock.isHeldByCurrentThread()) {
//才进行解锁
lock.unlock();
}
}
}
2. 两大缺点:主未同步挂,排队性能差
- 拿到 主节点的锁,主节点 还没同步到 子节点,就挂了。
- 分布式锁,把 线程进行排队,性能差。
问题1 主节点挂了未同步
- 使用 zookeeper,AP的,强一致性,最低半数以上 都写成功了,才加锁成功
- zookeeper也是 key value,也能实现分布式锁。
- 是树形结构的目录。主节点叫 lead,从节点叫 follow
- C一致性,A可用性,P是分区容错性。
- lead挂了,一定是 同步成功key的节点,成为 新的 lead
- 如果能容忍,这种情况,用 redis
Redlock 解决 主节点挂了
-
还没同步到 子节点
-
对应 类为:RedissonLock
-
超过半数redis节点加锁成功才算加锁成功
- 对等的 3个 redis节点,半数以上 setNx加锁成功了,才是成功。
-
旧版本是这样,使用 3个 客户端接口。
-
新版本的构造为:public RedissonLock(CommandAsyncExecutor commandExecutor, String name)
//创建配置
Config config1 = new Config();
//单机 服务端
config1.useSingleServer().setAddress("redis://127.0.0.1:6379");
//创建 RedissonClient
RedissonClient redissonClient1 = Redisson.create(config1);
//获取一个 锁
RLock lock1 = redissonClient1.getLock("LOCK_KEY");
//多个锁 组成 红锁 RedissonReadLock
RedissonReadLock redLock = new RedissonReadLock(lock1, lock2, lock3);
boolean isLock = false;
try {
//尝试 加锁
isLock = redLock.tryLock(500, 1000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
redLock.unlock();
}
问题2 高并发和线程安全
- ConcurrentHashMap 分段锁
- 200 个产品,段位1 20个,段位2 20个,可以分10个段位
- 分10段,并发 提升10倍
- 段位 不够减去(一下买好多个),合并 其他段位。
3. Redis 缓存和数据库 双写不一致
案例1:值还未写入redis,值已经被改变
线程1:
写数据库stock=10
更新缓存stock=10,这里还未执行,被线程2 插队,插入完毕。
- 更新缓存stock=10 开始执行,造成 不一致。
线程2:
写数据库stock=6
更新缓存stock=6
案例2:部分线程在 数据库更新后 才更新旧值
更新数据库,删除缓存。
查询数据库,加载缓存。
线程1:
- 写数据库stock=10
更新缓存stock=10
线程3:
-
查缓存 空
-
查数据库 stock = 10
-
更新缓存 (被 线程2 抢先执行完毕),更新为 10
线程2:
- 写数据库stock=20
- 删除缓存
线程3:
- 此时才更新缓存,更新为 10
解决
1. 延迟双删 无用
1、延迟双删:删除缓存后,睡X毫秒,再去删除
- 如果 更新缓存 (被 线程2 抢先执行完毕),更新为 10。在这之后 执行,
- 依然 无法解决
- 并且 写的吞吐量 会降低。
2. 内存队列 无用
内存队列:所有对 redis的操作 放入 内存队列
- 没用,降低 吞吐量
3. 加分布式锁
- 加 分布式锁,
- 写数据库stock=10 删除缓存,这样的操作 加锁。
配置 Redisson
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
@Configuration
public class RedissonConfig {
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
3.1 加读写锁 RReadWriteLock
String productId = "1";
//使用 读写锁。
RReadWriteLock readWriteLock = redisson.getReadWriteLock(REDIS_LOCK + productId);
//读锁
RLock rLock = readWriteLock.readLock();
rLock.lock();
rLock.unlock();//这是 读的情况。
//写锁
RLock wLock = readWriteLock.writeLock();
3.2 加DCL 双重锁
- 是 JVM 加锁,部署多个项目的话,没用。
//检查缓存,如果存在,直接返回
synchronized(this){
//先获取缓存,如果有 直接返回
//查询数据
//放入到缓存
}
4. 设置超时时间
设置 Redis 的超时时间
- 商品 详情也 的库存,不是真实的 库存。
4. 分布式读写锁源码
-
如果 都是 .readLock() 过来,第一 设置 setNx key,标志为 read模式,第二个会判断,是 read模式,
- 当前是 读锁,就 +1
- 锁释放 -1,知道 减到0 为止,锁删除。
-
如果 是 readLock() 已经执行了,模式为 read 模式,
- 一个 .writeLock() 过来,也会set 但是会失败,进入自旋。
-
如果先执行 writeLock(),模式设置为了 write,其他的 读锁 和 写锁,也会进入 自旋。
读锁
源码
@Override
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(
getName(),
LongCodec.INSTANCE, command,
"local mode = redis.call('hget', KEYS[1], 'mode'); ",
Arrays.<Object>asList(getName(),
getReadWriteTimeoutNamePrefix(threadId)),
internalLockLeaseTime,
getLockName(threadId),
getWriteLockName(threadId));
}
redis里键值对说明
redis的键为:atguiguLock101
key为:mode,value为:read
key为:fb7c6838-6a88-4117-a660-088493b67489:63,value为:1
redis的键为:{atguiguLock101}:fb7c6838-6a88-4117-a660-088493b67489:63:rwlock_timeout:1
redis的值为:1
lua脚本说明
"local mode = redis.call('hget', KEYS[1], 'mode'); " +
"if (mode == false) then " +
//keys[1]为前缀xxx+101 商品ID。如:atguiguLock101
//使用hash 设置这个 redis键,hash的key为mode,value为 read
"redis.call('hset', KEYS[1], 'mode', 'read'); " +
//进行加锁,加锁的 hash key为如下 ,value为1
//fb7c6838-6a88-4117-a660-088493b67489:69:write
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
//设置这个:{atguiguLock101}:fb7c6838:63:rwlock_timeout:1 值为1
"redis.call('set', KEYS[2] .. ':1', 1); " +
"redis.call('pexpire', KEYS[2] .. ':1', ARGV[1]); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
//如果模式为 读锁, or mode为写锁,并且 这个hash 存在
//如果为 读锁 或者 为同一个写锁
"if (mode == 'read') or
(mode == 'write' and redis.call('hexists', KEYS[1], ARGV[3]) == 1) then " +
//然后 增加1
"local ind = redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
//这个redis 设置为 1
"local key = KEYS[2] .. ':' .. ind;" +
"redis.call('set', key, 1); " +
"redis.call('pexpire', key, ARGV[1]); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end;" +
"return redis.call('pttl', KEYS[1]);"
写锁的
@Override
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"local mode = redis.call('hget', KEYS[1], 'mode'); " +
"if (mode == false) then " +
"redis.call('hset', KEYS[1], 'mode', 'write'); " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (mode == 'write') then " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"local currentExpire = redis.call('pttl', KEYS[1]); " +
"redis.call('pexpire', KEYS[1], currentExpire + ARGV[1]); " +
"return nil; " +
"end; " +
"end;" +
"return redis.call('pttl', KEYS[1]);",
Arrays.<Object>asList(getName()),
internalLockLeaseTime, getLockName(threadId));
}
5. 多级缓存 和 只有查询:tryLock使用场景
- 查询逻辑,第一次执行 放入redis,有了 缓存后,将 走不到 tryLock锁的逻辑里。
//判断缓存是否存在
RLock lock = redisson.getLock(REDIS_LOCK + productId);
//lock.Lock() 如果不能容忍,偶尔大量的请求,打到数据库。
//锁只等 3秒。抢锁失败,返回false,执行 下面的逻辑
boolean b = lock.tryLock(3, TimeUnit.SECONDS);
//再次判断
//DCL 双重锁的逻辑
//检查缓存,如果存在,直接返回
synchronized(this){//换成 分布式锁
//先获取缓存,如果有 直接返回
//查询数据
//放入到缓存
}
查询的方法总览 多级缓存
从cache中 获取缓存
private Product getProductFromCache(String productCacheKey) {
Product product = null;
//先去 map中 ,查询,多级 缓存 。内存能抗 更高的并发。因为 redis 有一次 远程的开销。
Product mapProduct = productMap.get(productCacheKey);
if (mapProduct != null) {
return mapProduct;
}
String productStr = redis.opsForValue().get(productCacheKey);
if (!StringUtils.isEmpty(productStr)) {
if ("".equals(productCacheKey)) {
product = new Product();
}
product = new Product();//productStr 转为 Product
}
return null;
}
ID获产品:Map redis 获取锁 尝试上锁 在走缓存 读写锁
public Product get(Long productId) throws Exception {
Product product = null;
String productCacheKey = Product_cache + productId;
//一般会在前面 + JVM 内存级别的 缓存。
//先从缓存中 获取
product = getProductFromCache(productCacheKey);
if (product != null) {
return product;
}
RLock lock = redisson.getLock("LOCK_PRODUCT" + product);
//允许 短时间 大量请求 进入,使用 tryLock 增加性能
lock.tryLock(3, TimeUnit.SECONDS);
try {
//再次 从缓冲中 读取
product = getProductFromCache(productCacheKey);
if (product != null) {
return product;
}
//获取 读写锁
RReadWriteLock readWriteLock = redisson.getReadWriteLock("LOCK_PRODUCT" + product);
RLock rLock = readWriteLock.readLock();
rLock.lock();//这里使用 读锁,上锁
try {
//查询数据库
product = new Product();
if (product != null) {
//10分钟 超时,应该转为 json
redis.opsForValue().set(productCacheKey, product.toString(), 10, TimeUnit.MINUTES);
//map中也 放一份
productMap.put(productCacheKey, product);
} else {
//为null,设置为空
redis.opsForValue().set(productCacheKey, new Product().toString(), 10, TimeUnit.MINUTES);
}
} finally {
rLock.unlock();
}
} finally {
lock.unlock();
}
return null;
}
一线互联网 技术栈
分布式架构
微服务架构
源码分析
并发编程
高并发实战
性能优化
数据结构与算法
工程化协作
项目经验
Redis底层
字符串(String)
- 整型(int)
embstr编码的简单动态字符串
raw编码的简单动态字符串
列表(List)
- 双端链表(Linkedlist)
压缩表(Ziplist)
哈希(Hash)
- 哈希表(Hashtable)
压缩表(Ziplist)
集合(Set)
- 整型数组集合(lintset)
哈希表(Hashtable)
有序集合(Zset)
- 压缩表(ziplisi)
跳跃表(Skiplist)
Bitmap 位图
GEO 地图
Hyperloglog是用来做基数统计的
其可以非常省内存的去统计各种计数,比如注册ip数、每日访问IP数、页面实时UV(PV肯定字符串就搞定了)、在线用户数
pfadd language "java" #添加成功
integer) 1
pfadd language "python"
pfcount language
integer) 2
pfadd language "java" #再次添加失败
integer) 0
- List item