JAVA 分布式锁
分布式锁
JVM 自带的 synchronized 及 ReentrantLock 锁都是单进程内的,不能跨进程,如下,同时来个两个请求被分配到不同的tomcat,这种锁将失效:
REDIS 实现分布式锁
可以借助 REDIS 的setnx 命令实现:
https://blog.csdn.net/T_Y_F_/article/details/144238022
注:redis 的工作线程为单线程
实现代码,版本1:
目前版本还存在问题:
这两行代码为非原子操作, 解决方案:
设置锁的时候使用带过期时间的重载方法:
版本1 存在问题:
在释放锁的时候有可能释放到非当前线程的锁:
解决方案:锁的value 值 使用一个唯一值,如下版本2:
版本2存在问题:以下代码没有保证原子性
redison
以上分布式锁的实现在redison 已经帮助我们实现,可以直接使用:
缓存击穿:大量缓存失效,导致请求全部查询数据库
解决方案1: 放入缓存redis 缓存时设置一个随机的过期时间
缓存穿透:秒杀商品被从数据库和reids删除,导致大量请求同时查询了reids和数据库
缓存穿透解决方案:
加锁之后的性能问题?
- 分段锁
- 读写锁: 针对同一把锁可以分出一把读锁和写锁。读锁与写锁是互斥的,但是读锁与读锁是非互斥的
Redis 核心数据底层结构核心原理
Redis 为什么高效?
- 命令执行时为单线程,没有上下文切换
- 基于内存操作
- 多路复用,epoll模型
- 高效的数据存储结构
# 查看值的存储类型
object encoding key
string
字符串string 底层存储的时候还有三种结构:
应用场景1:
应用场景2:
hash
问题:
redis 存储对象时用json串还是 hash?
区分具体场景: 如果说表里存储的行数据只有一个字段需要经常修改,如支付宝的余额,此时可以使用hash来存储效率更高
实现购物车:
list
常见操作:
应用场景:
思考:JDK 已经给我们实现了一些队列,栈,为什么还要要用redis 来实现?
JDK提供的数据结构都是单机的,在集群环境下的用分布式数据结构
微信公众号消息及微博可以借助list实现:
几千几万的粉丝以上实现还可以,如果是上亿粉丝 如何实现?
思路:
1.进行给用户打标签,如活跃用户和非活跃用户
2. 大V 在发微博后只向 活跃用户写消息
3. 非活跃用户登录后直接去读大V的发件箱
set
应用场景1:微信抽奖小程序
# 随机获取count 个元素,获取后元素还在集合中
SRANDMEMBER key count
# 随机获取cout个元素,获取后元素从集合中移除
SPOP key count
# 求交集
SINTER
# 求并集
SUNION
# 求差集
SDIFF
关注模型可以用set 实现:
zset
zset 的两种底层存储结构:
不同存储结构的演示:
压缩列表(ziplist)是一种紧凑、高效的线性数据结构,被广泛应用于需要高效存储和访问数据的场景,特别是在内存空间有限的情况下,常用于 Redis 等数据库中。以下是关于它的详细介绍:
结构特点
紧凑存储:压缩列表是一种连续内存空间的数据结构,它将多个元素紧凑地存储在一起,最大限度地减少了内存碎片和空间浪费。每个元素可以是不同长度的字节数组或整数,相邻元素之间紧密排列,没有额外的填充或间隔。
双向链表特性:从逻辑上看,压缩列表具有双向链表的一些特性,它允许在表头和表尾进行快速的插入和删除操作,并且可以双向遍历。但与传统双向链表不同的是,它并不是通过指针来连接节点,而是通过记录每个元素的长度和位置信息来实现双向访问。
组成部分
表头:包含了压缩列表的一些元信息,如列表的总字节数、表头的大小、表尾节点的偏移量等。通过表头信息,程序可以快速定位和操作压缩列表的各个部分。
节点:是压缩列表存储数据的基本单元,每个节点包含了数据本身以及一些额外的元数据,如前一个节点的长度、当前节点的编码方式等。前一个节点长度字段用于实现双向遍历,通过记录前一个节点的长度,程序可以从当前节点快速定位到前一个节点。编码方式字段则表示当前节点数据的存储格式,它可以是整数、字符串等不同类型。
表尾:一个特殊的节点,用于标识压缩列表的结束。它通常只包含一个特殊的结束标记,用于告诉程序已经到达列表的末尾。
优势
内存高效:由于其紧凑的存储方式,压缩列表在存储大量数据时能够显著节省内存空间。这对于内存资源有限的系统或需要处理大量数据的应用来说非常重要,可以有效地降低内存成本,提高系统的整体性能。
操作快速:在进行插入、删除和查找操作时,压缩列表可以利用其特殊的结构和元信息,快速定位到目标位置,实现高效的操作。尤其是在表头和表尾进行插入和删除操作时,时间复杂度较低,能够满足实时性要求较高的应用场景。
局限性
不适合频繁的随机访问:虽然压缩列表支持双向遍历,但它并不像数组那样可以通过索引直接访问任意位置的元素。在进行随机访问时,需要从表头或表尾开始逐个节点地遍历,时间复杂度较高,因此不适合需要频繁随机访问元素的场景。
元素大小受限:由于压缩列表的节点长度是固定的,当存储的元素过大时,可能会导致节点溢出,需要对压缩列表进行重新分配和调整,这会带来一定的性能开销。
===================================================================================================================================================================================================================================================
跳表(Skip List)是一种概率性数据结构,由William Pugh在1990年提出,主要用于在有序的元素集合上进行快速的搜索、插入和删除操作。以下是对跳表的详细解析:
一、跳表的结构
分层结构:跳表由多层链表组成,每一层都是一个有序的链表。最底层(Level 0)包含所有元素,而每一上层都是下一层的稀疏子集,包含的元素逐层减少。
节点结构:每个节点不仅包含数据,还包含指针。这些指针指向同层下一个节点以及上层或下层的节点。具体来说,每个节点至少包含两个指针:一个指向同一链表中的下一个元素,另一个指向下面一层的对应元素(如果存在)。
二、跳表的操作
查找:查找操作从最顶层开始,如果当前层的下一个节点值大于查找值,就下降到下一层继续查找,直到最底层。如果找到目标值,则查找成功;否则,查找失败。由于跳表采用了多层索引,因此查找过程类似于二分查找,时间复杂度为O(logn)。
插入:插入元素时,先在底层插入该元素,然后通过随机方式决定是否将该元素提升到上层。这个过程可能会一直持续到顶层。随机提升的方式保证了跳表的索引结构在插入时能够保持一定的稀疏性。
删除:删除元素时,先通过查找方式找到该元素,然后从最底层向上逐层删除该元素。
三、跳表的特点
高效性:跳表的搜索、插入和删除操作的平均时间复杂度均为O(logn),这使得跳表在处理大量数据时具有高效性。
简单性:与平衡树相比,跳表的实现和维护更加简单。跳表不需要像平衡树那样频繁地调整节点的高度和位置来保持平衡。
概率性:跳表是一种概率性数据结构,其性能在一定程度上取决于随机数的生成。然而,由于跳表采用了多层索引和随机提升的方式,因此在实际应用中其性能通常是非常稳定的。
四、跳表的应用场景
数据库索引:许多数据库使用跳表作为索引结构,以提供高效的插入、查找和删除操作。
缓存系统:在分布式缓存系统中,如Redis的实现中,跳表提供了快速的缓存数据查找和管理机制。
负载均衡及路由管理:跳表常用于管理节点列表和路由表,以加快服务节点的查找和分配。
有序缓存:跳表可以用来保持缓存条目的有序性,便于实现如LRU(最近最少使用)等缓存淘汰算法。
动态内存分配:跳表可以用于动态内存分配器中,以追踪空闲内存块的大小和位置,快速分配和释放内存。
综上所述,跳表是一种高效且简单的数据结构,适用于需要快速查找、插入和删除操作的场景。
本文来自博客园,作者:chuangzhou,转载请注明原文链接:https://www.cnblogs.com/czzz/p/18646123
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2024-01-01 JAVA实现 - AVL树
2022-01-01 UnitTest 框架学习