Redis是一种键值型的NoSql数据库。
键值型指Redis中的数据都是以key,value形式存储,并且value形式多样,甚至可以是json格式。
NoSql数据库:
(Not Only Sql)非关系型数据库。
NoSql数据库和传统数据库的区别:
SQL | NoSQL | |
数据结构 | 结构化(每张表都有严格的约束信息,插入数据必须满足约束) | 非结构化(没有严格约束,形式松散、自由) |
数据关联 | 关联的(表与表之间往往存在关联,比如外键) | 非关联(维护表之间的关系要么靠代码中的业务逻辑,要么靠数据之间的耦合) |
查询方式 | sql语句查询 | 非sql |
事务特性 | ACID(解释见表下) | BASE |
存储方式 | 磁盘(对性能有一定影响) | 内存(读写速度会很快,性能好一点) |
扩展型 | 垂直(数据库集群模式一般是主从,主从数据一致,起到数据备份的作用,称为垂直扩展。) | 水平(可以将数据拆分,存储在不同机器上,可以保存海量数据,解决内存大小有限的问题。称为水平扩展。) |
使用场景 | 1,数据结构固定 2,业务对数据安全性,一致性要求较高 |
1,数据结构不固定 2,对一致性,安全性要求不高 3,对性能要求较高 |
ACID解释:是指数据库管理系统在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性:原子性(atomicity,或称不可分割性:一个事务中所有操作,要么全部完成,要么全部不完成,不可能只执行其中一部分SQL操作)、一致性(consistency:数据库总是会从一个一致性的状态转换到另一个一致性的状态,比如即使执行语句在哪一环节崩溃,事务做的修改也不会保存在数据库中)、隔离性(isolation,又称独立性:事务所做的修改在最终提交以前,对其他事务是不可见的)、持久性(durability:事务一旦提交,所做的修改会永久保存在数据库中)。
Redis特点:
键值型
单线程:每个命令具备原子性。
低延迟,速度快(基于内存,I/O多路复用,良好的编码)。
支持数据持久化
支持主从集群,分片集群
支持多语言客户端
背景知识:
cookie介绍:
cookie:小型文本文件,以键值对进行表示。(就比如登录过一些网站,然后关闭后重新打开会自动登录)
cookie的生命周期:会话性:仅保存在客户端内存中,关闭客户端cookie失效;持久性:保存在用户的硬盘中,直至生存期结束或用户主动销毁。
总结:Cookie就是一些数据,用于存储服务器返回给客服端的信息,客户端进行保存。在下一次访问该网站时,客户端会将保存的cookie一同发给服务器,服务器再利用cookie进行一些操作。利用cookie我们就可以实现自动登录,保存游览历史,身份验证等功能。
jsessionid介绍:
JSESSIONID是服务器通过cookie来跟踪用户会话的一种方式。当用户首次访问服务器时,服务器会在响应头中设置Set-Cookie,种下JSESSIONID。之后每次请求,浏览器都会在请求头中携带此cookie,以便服务器识别对应的session。JSESSIONID确保了用户会话的一致性和安全性。
session介绍:
session称为会话控制,是服务器为了保存用户状态而创建的一个特殊的对象。session类似于一个map,里面可以存放多个键值对,key必须是一个字符串,value是一个对象。
session的底层实现机制:
当客户端于服务端进行会话时,服务端会创建一块内存空间存储此次会话的数据,这块内存空间就是session。在通过HTTP请求访问一个网站时,会携带一个cookie,也就是session id,通过id可以在服务端,查找是否有id对应的session(通过request.getSession),如果存在就到对应的sesssion里去取之前存放的数据,如果不存在就直接创建一个新session,并将新session的id发回给浏览器,放入cookies中。
(jsessionid只是tomcat中对session id的叫法,在其它容器里面,不一定就是叫jsessionid了。)
之后,只要浏览器没有关闭,你每向服务器发请求,服务器就会从你发送过来的cookies中拿出这个session id,然后根据这个session id到相应的内存中取你之前存放的数据。但是,如果你退出登录了,服务器会清掉属于你的内存区域,所以你再登的话,会产生一个新的session了。
JSESSIONID、SESSION、cookie-CSDN博客
session实现登录:
1.发送验证码:校验手机号是否合法:合法:后台生成验证码,将验证码保存到session中,同时发送给用户;不合法,要求重新输入。
2.短信验证码登录注册:将用户输入的手机号作为Id查询对应session里的校验码,一致则根据手机号在数据库查询该用户是否存在,存在,则将用户信息保存在session中;不存在创建用户账号,保存在数据库;不一致输出无法通过校验。
3.校验登录状态:登录请求时,携带cookie,通过JsessionId从session中拿到用户信息,有信息就保存用户到ThreadLocal中然后放行。
ThreadLocal介绍:
线程局部变量:同一个ThreadLocal所包含的对象,在不同的Thread下有不同的变量副本。
threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。史上最全ThreadLocal 详解(一)-CSDN博客
具体创建变量副本的过程:
在每条线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是每条线程用来存储变量副本的,key值为当前ThreadLocal对象,value为变量副本(即T类型的变量)。每个Thread线程对象最开始的threadLocals都为空,当线程调用ThreadLocal.set()或ThreadLocal.get()方法时,都会调用createMap()方法对threadLocals进行初始化。然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。一文让你彻底掌握ThreadLocal (这次终于懂了~)_threadlocal 使用注意事项-CSDN博客
tomcat运行原理:
用户发出请求,通过用户端的socket(在抽象层,与端口绑定,监听和发送数据)创建与服务端tomcat的TCP连接,tomcat端的socket接收到数据后,监听线程会从tomcat的线程池中取出一个线程执行用户请求,该线程会找到用户想要访问的工程,然后将数据经过工程的controller,service,dao中,然后访问对应的数据库,用户执行完请求后,统一返回,将需要的数据封装到request对象中写回到用户端的socket。
在返回数据时是用户的全部信息,可以做一个优化就是将用户的敏感信息隐藏。具体思路就是将含有用户敏感信息的User对象转化成没有敏感信息的UserDto对象,具体修改在登录方法处(保存用户信息到session中:session.setAttribute("user",BeanUtils.copyProperties(user,UserDTO.class));)
在拦截器处(保存用户信息到Threadlocal):UserHolder.saveUser((UserDTO) user);
在UserHolder处:将user对象换成UserDTO。
session共享问题:
当用户第一次访问第一台tomcat,信息都存放在第一台服务器的session中,但第二次访问第二台tomcat时,第二台服务器就没有第一台服务器存放的session了。
所以为了解决这个问题,早期的方案是session拷贝,但这样会有两个问题,一个是每台服务器都有一份完整的session,服务器压力过大;另一个问题是session在拷贝数据时可能会出现延迟。
所以基于以上背景,采用redis代替session,因为redis数据本身共享。
---------------------------------------------------------------------------------------------------------------------------------
Redis代替session
Redis的键值对结构
String结构:value值可以以JSON字符串保存。
Hash结构:value值将每个字段和其对应的value值独立存储,可以针对单个字段做CRUD,内存占用更少。CRUD:做计算处理时的增加(Create)、读取查询(Retrieve)、更新(Update)和删除(Delete)。
value值我们可以用String结构,但是key要具有唯一性并且方便从浏览器页面带回来,为了维护信息最好也不能用敏感数据。
所以选择在后台生成一个随机串token。
token:作为计算机术语时,是“令牌”的意思。Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。
根据以上改动,基于Redis的短信登录流程为:
注册完成后(初次登录),用户提交手机号验证码验证,手机号为key在redis中读验证码校验,一致根据手机号查询用户信息,不存在用户就新建,然后将用户数据保存在redis中,并生成token作为redis的key(就不再用手机号了),然后将token返回客户端以等待下一次登录。
下一次登录时,用户在浏览器上发起请求,根据携带的token在redis里找用户数据,用户存在就保存用户到ThreadLocal里,然后放行。
拦截功能:
之前获取用户token查询redis的用户不存在的时候会被拦截
初始方案:使用对应路径拦截,同时刷新令牌token的存活时间。
缺点:假如用户访问了一些不需要拦截的路径,拦截器就不会生效。
改进方案:使用两个拦截器:
商户查询缓存:
介绍:缓存就是数据交换的缓冲区(俗称缓存是缓冲区内的数据,一般从数据库中获取,存储本地代码)。
特点:被static修饰,所以可以随着类的加载而加载到内存中去;
又因为被final修饰,所以其引用(例如map)和对象(HashMap)之间的关系是固定的,不用担心赋值(=)导致缓存失效。
作用:提高服务器读写能力(因为内存读写能力高于磁盘),降低响应时间。
降低后端负载(作为大数据量进系统的“减震器”)。
成本:数据一致性成本,代码维护的成本,运维成本。
应用:构造多级缓存可以使系统运行速率进一步提升。
多级有以下多级缓存:浏览器缓存;
应用层缓存:分为tomcat本地缓存,比如map或者使用redis作为缓存。
数据库缓存:数据库有一片空间是buffer pool,增删改查数据都会先加载到mysql的缓存中。
CPU缓存:为了适应当前内存读写速度跟不上的情况,增加了CPU的L1,L2、L3级内存。
添加商户缓存
缓存模型和思路:
查询信息先查询缓存,有直接返回,没有再查询数据库,将数据存入redis中。
缓存更新策略:
内存淘汰:redis自动进行,redis内存达到设定的max-memery时,会自动触发淘汰机制。
超时剔除:给redis设置过期时间之后,redis会把超时数据删除。
主动更新:手动调用方法把缓存删掉,用于解决缓存和数据库不一致问题。
业务场景:低一致性需求:内存淘汰机制(店铺类型的查询缓存)
高一致性需求:主动更新,超时剔除作为兜底方案
数据库缓存不一致问题及解决方案:
当数据库中数据发生变化,但是缓存没有同步,会出现线程安全问题。
解决方案:
Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案
Read/Write Through Pattern : 由系统本身完成,数据库与缓存的问题交由系统本身去处理
Write Behind Caching Pattern :调用者只操作缓存,其他线程去异步处理数据库,实现最终一致
实现商铺和缓存与数据库双写一致
综合考虑选择策略为:先操作数据库,再删除缓存,具体来说就是根据商户id查询店铺时,如果缓存未命中,就查询数据库,将数据库结构写入缓存并设置超时时间,根据id修改商铺时,先修改数据库,再删除缓存。
具体来说为什么:
1,为什么是删除缓存而不是更新缓存
因为每次更新数据库之后就更新缓存,新更新会覆盖旧更新导致无效写操作较多,删除可以在更新数据库时让缓存失效,查询时再更新缓存。
2,如何保证缓存和数据库操作的原子性(同时失败或成功)
单体系统,将缓存与数据库操作放在一个事务;
分布式系统,利用TCC等分布式事务方案。
3,先删缓存还是先操作数据库
如果先删缓存再操作数据库的话,如果两个线程并存,线程1先删缓存,然后还没更新数据库,线程2就已经查询缓存不存在写入了一个缓存,之后线程1再开始更新数据库,所以2写入缓存的数据还是没更新前的数据。(双写问题)
缓存穿透及解决方案:
缓存穿透是指客户端请求的数据在缓存和数据库中都不存在,缓存永远不会生效,请求都会打到数据库。不断发起请求会给数据库带来巨大压力。
解决方案有:
* 缓存null值
* 布隆过滤
* 增强id的复杂度,避免被猜测id规律
* 做好数据的基础格式校验
* 加强用户权限校验
* 做好热点参数的限流
主要解决方案介绍:
缓存空对象:
客户端访问不存在的数据,先请求redis,但此时redis没有数据,就会返回数据库,数据库也没有,但是数据库能承受的并发不如redis高,所以大量的请求访问该不存在的数据,都会直达数据库,有可能会导致数据库崩溃。所以我们的解决办法是不管这个数据在数据库中存在不存在,我们客户第一次访问该数据时就将其存在redis中,这下即使大量并发请求该数据也只是卡在redis里,而不会穿透数据库的缓存了。
该方法的优点:实现简单,维护方便;缺点:会造成额外的内存消耗,肯造成短期不一致。
布隆过滤:
布隆过滤器它是一个很长的二进制数组(大型位数组,用来存放多个无偏hash函数的结果,只有1和0)和多个无偏hash函数(无偏hash函数将元素的hash值计算的比较均匀的映射到位数组中)实现的。
映射的具体原则:
通过k个无偏hash函数计算得到k个hash值
依次取模数组长度,得到数组索引
将计算得到的数组索引下标位置数据修改为1
查询元素前两步都一样,但是最后一步变为:判断索引处的值是否都为1,出现一个0就是不存在,都是1可能存在(也有可能是其他元素经过多个哈希函数值和它刚好一样,哈希冲突)。
它的特点是:不存在一定不存在,存在不一定存在。
应用:
解决Redis缓存穿透问题
邮件过滤,使用布隆过滤器来做邮件黑名单过滤
对爬虫网址进行过滤,爬过的不再爬
解决新闻推荐过的不再推荐(类似抖音刷过的往下滑动不再刷到)
HBase\RocksDB\LevelDB等数据库内置布隆过滤器,用于判断数据是否存在,可以减少数据库的IO请求
优点:时间复杂度低O(N);保密性强(布隆过滤器不存储元素本身);存储空间小。
缺点:有一定误判性(哈希冲突),可以通过增长位数组或者增加无偏哈希函数个数来降低误判;无法获取元素本身;很难删除元素。
布隆过滤如何解决缓存击穿:
客户端访问不存在的数据时,该数据会先经过布隆过滤器查看是否存在,如果存在则放行,经过redis,有的话就返回数据,没有的话继续访问数据库查询。如果经过布隆过滤器发现不存在,则直接拒绝,发送拒绝信息给客户端。这样查询不存在的信息就会在滤波器处就被隔离掉,不必担心会影响后面的redis和数据库。(但是也有误判的风险)。
缓存雪崩及解决方案
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案有:
* 给不同的Key的TTL添加随机值
* 利用Redis集群提高服务的可用性
* 给缓存业务添加降级限流策略
* 给业务添加多级缓存
缓存击穿问题及解决方案
也叫热点Key问题,一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数请求都无法从缓存中获取数值转而都开始访问数据库,给数据库带来巨大冲击。
解决方案:
1,互斥锁
互斥锁思想:假如线程并行访问会崩溃的话就把访问变成串行。锁能实现互斥性,所以我们可以采用tryLock+double check方法实现。
流程变为:线程1访问缓存,没有数据但是tryLock获得了锁,就会继续查找数据库,写入缓存,最后释放锁;在线程1执行过程中,线程2突然也在缓存中访问同一个数据,但是没有获取锁,它就会休眠重试直到1把锁释放,获取到了就会执行接下来的操作。
2,逻辑过期
之所以会出现线程击穿问题,是因为对key设置了过期时间导致key失效,但又不想没用的key一直占内存。所以解决方案是:将过期时间设置在redis的value中,此时线程1查询缓存,通过value判断出来已经过期了,线程1获得互斥锁,开启新线程2来查询数据库重建缓存数据,然后将数据写入缓存,重置逻辑过期时间,完成以上操作之后释放锁。在线程1转线程2的过程中怕,如果有线程3来访问缓存,发现逻辑时间过期,但无法获取锁,就直接返回过期数据。
优惠券秒杀
优惠券抢购订单信息会保存在数据库中,它的id会有以下问题:
1,id携带敏感信息,比如从id就能看出来商家卖出了多少单。
2,数据量过大之后,得拆分表后重建,不能保证重建后id的唯一性。
所以我们要设计一个全局ID生成器,保证唯一性,递增性,安全性等。
ID的结构为:符号位(1bit)+时间戳(31bit)+序列号(32bit)
实现秒杀下单具体思路:
用户下单--->提交优惠券id--->查询优惠券信息--->判断是否在秒杀开始时间内--->判断库存是否充足--->扣减库存--->创建订单--->返回订单id。(其中任一判断不满足就会返回异常结果)
库存超卖问题:
原本代码的解决方案是:判断如果得到的库存数小于1,就返回库存不足信息。
但是会存在多线程安全问题:比如线程1查询库存,发现大于1,准备扣减库存,但是还没扣减,线程2访问,也发现大于1,也准备扣减,就会出现库存超卖问题。
解决方案有两种:
悲观锁:
认为线程安全问题一定会发生,所以在操作数据之前先获取锁,确保线程串行执行(比如synchronized,lock)。
乐观锁:
认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时判断其他线程有没有对数据修改,没有修改认为安全,更新数据;修改则说明出了安全问题,重试或异常。
实现方式:
版本号修改:一开始数据会有一个版本号,每次操作数据都会对版本号+1;再提交回数据,校验版本号是否比原来只大1,如果满足条件,说明操作过程中没有人对其修改,如果不是只大1,说明已经被修改了。
CAS:内存值等于预期值说明没有其他线程修改,直接结束循环返回得到的数据,如果内存值不等于预期值,说明有别人修改了数据,那就重新获取数据重新判断是否相等,直到数据等于预期值。
int var5;
do {
var5 = this.getIntVolatile(var1, var2);//得到最新的数据值
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
//如果内存值等于预估值,函数返回1,循环打破,执行下面的return;
//内存值不等于预估值,函数返回0,循环条件成立,继续进入循环体内多次获取数据
return var5;
如果线程重复获取数据相当于线程一直在自旋,如果CAS自旋压力过大,可以采用Java8 提供的一个类:LongAdder。
LongAdder中包含了一个cell和base,如果有很多线程同一时间操作base,提供分段CAS,将base分成多个cell。
项目中的方案是,直接设置版本号默认为1,当有线程访问的时候,判断是1,就进入,然后将版本号设为2,这样其他线程访问版本号不一致就不会执行。
如果允许通过乐观锁,之后就要对库存减一,这里的判断条件是优惠券Id正确并且库存大于0;
以上是解决判断超卖问题的方案。
一人一张优惠券秒杀:
用户下单--->提交优惠券id--->查询优惠券信息--->判断是否在秒杀开始时间内--->判断库存是否充足--->判断订单是否存在(否向后执行)--->扣减库存--->创建订单--->返回订单id。(其中任一判断不满足就会返回异常结果)
方案一:
一开始if(count > 0)判断用户有没有购买过订单,大于零返回异常,否则说明用户没有购买过,扣减库存(where(voucher_id == voucher_id)),然后创建订单id。
问题:如果一个用户多次同时请求,也就是多线程的情况下,会有不止一个订单满足上述条件成功下单。
解决思路:加锁
方案二:
确定加悲观锁(因为要处理多线程安全问题)
首先将查询订单是否存在--->扣减库存--->创建订单信息这三部分封装成一个函数createVoucherOrder。
加锁加在哪里?
直接给这个函数上锁?锁的粒度太大,每个线程进来就会被锁住,所以修改方案在createVoucherOrder函数内部获取完userId后上锁:synchronized(userId.toString().intern()){原来的方法体}
注:userId.toString()只是将对象换成字符串,但要拿到对象里面的数据就调用intern,从常量池中拿到数据。
问题:给函数里代码块加锁最后释放的过程是:
return订单Id--->释放锁--->完成函数,提交事务给sql
但这样我们释放锁在事务提交之前,就有可能被其他线程占用锁,而此时之前的订单信息有可能还没有到数据库,又会引起线程安全问题。
方案三:
在之前的判断库存是否充足之后,
先得到用户Id:Long userId = UserHolder.getUser().getId();
然后先上锁:synchronized(userId.toString().intern()){
然后提交事务完成后释放锁:return this.createVoucherOrder(voucherId);
}
但是这时是用this方式来调用的,事务要生效还要利用代理,就需要获取原始事务对象,操作事务:
synchronized(userId.toString().intern()){
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
以上方法仅限于单机模式下使用
集群环境下的并发问题:
集群测试:
将服务启动两份,修改nginx.conf文件,配置反向代理和负载均衡,这样启动一个端口8080就有两个服务分别进入端口8081和8082,实现负载均衡。
这样启动两个服务,就相当于利用了两个JVM,一个JVM只配有一个monitor来监测synchronized锁,所以一个JVM都会有一个线程运行成功,按上述操作,两个JVM就会有两个线程运行成功,如果这两个线程都是指向同一个数据库的数据,就又会出现并发问题。
基于redis分布式锁
满足分布式系统或集群模式下多进程可见并且互斥的锁。
核心思想:只要所有JVM都是一个锁监视器,就相当于所有线程都用的一把锁,就能实现集群模式下程序串行执行。
分布式锁需要满足的条件:
1,可见性:所有线程都能看到相同的结果。
2,互斥:程序串行执行(基本条件)。
3,高可用:程序不用崩溃。
4,高性能:需要较高的加锁性能和释放锁性能。
5,安全性:必不可少。
常见的三种分布式锁:Mysql(不常用,采用mysql本身的锁机制,但是性能一般),Redis(较常用),Zookeeper(较常用,利用节点的唯一性和有序性实现互斥)。
本项目采用redis分布锁:利用setnx这个方法,如果插入key成功,则表示获得到了锁,如果有人插入成功,其他人插入失败则表示无法获得到锁,利用这套逻辑来实现分布式锁。
SETNX命令,它是SET if Not eXists的缩写,如果键不存在,则将键设置为给定值,在这种情况下,它等于SET;当键已存在时,不执行任何操作;成功时返回1,失败返回0。
获取锁:
条件:互斥(只能有一个线程获取锁);非阻塞(尝试一次,成功true,失败false);设置超时时间(超时释放)
set 锁名 线程名 NX EX 10
NX是互斥,EX是设置超时时间。
释放锁:
超时释放+手动释放
delete 锁名
误删锁问题
但是以上逻辑有个问题,就是误删锁问题:
线程一先持有锁,然后业务阻塞,锁内部超时释放锁,线程2获得该锁,在2执行锁的过程中,线程1不阻塞了,但是走到了finally中的释放锁逻辑,就会误删线程2的锁。
解决方案:每个线程释放锁的时候会先判断一下当前锁是否属于自己,如果属于自己的锁,就进行锁的删除,如果不属于就不删。
获取锁时存入线程标识(UUID,每一个JVM创建一个属于自己的UUID,然后将UUID和自己的线程拼接起来用来判断不同JVM),释放锁时先获取锁中线程标识,判断是否与当前线程标识一致,如果一致说明锁属于自己,就可以删除锁,不一致说明不属于,就不释放锁。
分布式锁的原子性问题
极端的误删情况:假如线程1已经完成判断锁是自己的条件,但是还没有删除锁的时候卡住了(比如突然出现JVM里的垃圾回收机制:一种自动的存储管理机制,当一些被占用的内存不再需要时,就应该予以释放,以让出空间),然后锁自身因为超时释放了,于是线程2占用锁,但是在线程2执行业务时,线程1又恢复了,因为之前判断是否为自己的锁条件成立,所以仍旧自动执行删除锁操作,就会导致锁又被释放,其他线程又可以来占有,造成并发问题。
解决方案:保证判断是否为自身锁的条件和删除锁操作具有原子性。
Redis提供了Lua(一种编程语言)脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。
注意:Lua语言数组的角标是从1开始的!
调用Lua中的call函数:
语法:redis.call('命令名称', 'key', '其它参数', ...)
调用该函数即使是多条命令,多个语句都可以原子性运行。
调用脚本的常见命令:EVAL
key和“其他参数”的值可以不直接写进括号里(这样相当于函数写死了,不好改),可以将括号里的key和“其他参数”当作参数传递,真正对应的值写在括号外面。
key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:
EVAL "return redis.call('set',KEYS[1],ARGV[1])" 1 name Rose
//1指的是需要的key类型的参数个数
//name指的是传入KEYS[1]中的数据
//Rose指的是传入ARGV[1]中的数据
所以保证判断条件和删除锁操作的原子性,Lua脚本为:
//判断获取的锁的标识==当前线程标识?
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
-- 一致,则删除锁
return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0
java采用execute方法去执行lua脚本:execute(RedisScipt<T> script,List<K> keys,Object...args)
分别对应Lua脚本EVAL中的 script keys args;
基于以上setnx的锁存在问题:
1,重入问题:实质上是锁嵌套,简单来说就是一个线程抢占到了同步锁资源,并且在释放锁之前再去竞争同一把锁的时候(比如一个方法内调用另一个方法重复获取锁),不需要等待,只需要记录重入的次数。可重入锁解决的问题主要是避免了死锁的情况。
2,不可重试:获取锁失败就会直接return success("获取失败"),合理情况下获取失败应该重复尝试。
3,超时释放:时间太短业务没执行完,锁就自己释放了,时间太长又会有安全隐患。
4,主从一致性:我们向集群写数据时,主机要异步的将数据同步给从机,如果在同步过去之前,主机宕机了,就会出现死锁问题。
基于Redission的分布式锁
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
redission可重入锁内部原理
在分布式锁中,采用hash结构来存储锁,其中hash结构中的KEY表示这把锁是否存在,用VALUE中的field表示该锁被哪个线程持有,VALUE中的value表示该线程重新入锁的次数。
KEY | VALUE | |
field | value | |
lock | thread1 | 2 |
每判断一次锁是否属于自己将value加一。
每执行完一个业务(之前是直接释放锁)现在是将value减一,直到value为0就删除锁。
Redission是用Lua脚本实现可重入锁。
以上内容来自于黑马程序员。
后续内容见点评项目下