# redis的分布式锁
** 问题场景**
例如:一个用户修改操作,修改一个用户的状态,首先在数据库查出用户的状态,然后再内存中进行修改,然后再从回去,在单线程中这个是没问题,但在多线程中,由于读取,修改,存储这三个操作不是原子性,所以在多线程中会出现问题
数据库中事务的四大特性:
原子性:事务包含的操作要么全部成功,要么全部失败回滚
一致性:事务执行前后必须一致
持久性:事务一旦提交,就是永久性的
隔离性 : 每个用户开启的事务,不能影响其他事物
分布式锁的思路就是:进来一个线程先占位,当别的线程 进来的时候发现已经有人占位了,就会放弃或是稍后再试
在redis 中,占位一般用setnx 指令,先进来的线程先占位,等线程执行完了后,用del来释放位子
RedisConfig
public interface RedisConfig {
void call(Jedis jedis);
}
Redis
public class Redis {
private JedisPool jedisPool;
public Redis(){
GenericObjectPoolConfig<Jedis> config=new GenericObjectPoolConfig();
//最大连接数
config.setMaxTotal(1000);
//最大等待时间 如果是-1 等带时间没有限制
config.setMaxWaitMillis(3000);
//最大空闲数
config.setMaxIdle(300);
/**
* reids 地址
* Reids 端口
* Reids 链接超时时间
* reids 密码
*/
jedisPool=new JedisPool(config,"116.62.203.162",6379,300,"494936");
}
public void execute( RedisConfig redisConfig){
try(Jedis jedis = jedisPool.getResource()){
redisConfig.call(jedis);
}
}
}
lockRest
public class LockTest {
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis -> {
//在redis 中,占位一般用setnx 指令,先进来的线程先占位,等线程执行完了后,用del来释放位子
Long setnx = jedis.setnx("k1", "v1");
if(setnx == 1){
//setnxs 不会存在已经存在的值
String set = jedis.set("name", "aike");
String name = jedis.get("name");
System.out.println(name);
jedis.del("k1");
}else {
//有人占位,停止/暂缓操作
}
});
}
}
当然这样是有问题的,如果代码业务挂了或者抛出异常后,会导致del指令没有被调用。这样陷入死循环中,k1 无法释放,后面来的请求全部堵死
要解决这个问题,就是给锁添加一个过期时间,确保在一定的时间后能都得到释放
public class LockTest {
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis -> {
//在redis 中,占位一般用setnx 指令,先进来的线程先占位,等线程执行完了后,用del来释放位子
Long setnx = jedis.setnx("k1", "v1");
if(setnx == 1){
//给锁添加一个过期时间,防止锁无法释放出现阻塞
jedis.expire("k1",5);
//setnxs 不会存在已经存在的值
String set = jedis.set("name", "aike");
String name = jedis.get("name");
System.out.println(name);
jedis.del("k1");
}else {
//有人占位,停止/暂缓操作
}
});
}
}
这样也有问题, 就是获取锁和设置过期时间,如果服务器挂掉,这时候锁被占用,无法释放,会造成死锁。因为获取锁和设置过期时间是两个操作。不具备原子性。
为了解决这个问题,setnx 和 expire 可以通过一个命令一起执行,
public class LockTest {
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis -> {
String set1 = jedis.set("k1", "v1", new SetParams().nx().ex(5));
System.out.println(set1);
if(set1 !=null && "ok".equals(set1)){
//在redis 中,占位一般用setnx 指令,先进来的线程先占位,等线程执行完了后,用del来释放位子
//给锁添加一个过期时间,防止锁无法释放出现阻塞
jedis.expire("k1",5);
//setnxs 不会存在已经存在的值
String set = jedis.set("name", "aike");
String name = jedis.get("name");
System.out.println(name);
jedis.del("k1");
}else {
//有人占位,停止/暂缓操作
}
});
}
}
## 解决超时问题
为了防止业务代码在执行时抛出异常,我们给每个所添加了超时时间,超时之后锁,会自动释放,但这是这样会有一个新问题,如果开始执行业务代码,业务代码执行8秒,这样第一个线程任务还没有执行成功就已经释放了,此时第二个线程会获取到锁开始执行,当第二个任务执行到3秒时,第一个任务也执行完了,此时第一个线程会释放锁,但释放的是第二个线程的锁,释放之后,第三个线程进来了
对于这个问题我们有两个角度入手:
— 业务代码的执行时间小于锁释放时间
— 将锁的value 设置为随机字符串,每次释放锁的时候,都无比较随机字符串是否一致,如果一致再去释放,否则不释放;
对于第二种方案,由于释放锁的时候,要去查看所得value 的值是否正确,第三步释放锁有三步,明显不具备原子性,为了解决这个问题,我们引入了Lua脚本。
lua脚本的优势:
— 使用方便,redis 中内置了lua脚本
— Lua 脚本可以在Redis 服务端原子的执行多个reids命令
使用lua脚本两中种思路:
提前在reids服务端写好Lua脚本,然后在Java 客户端去调用脚本(推荐)
## 消息队列
Reids 适用于简单的场景。
## 小插曲
redis 做消息队列,使用List数据结构就可以实先,我们可以使用lpush/rpush 操作来实现入队,然后使用lpop/rpop来实现出队
在客户端(java 端),我们会维护一个死循环来不停的从队列中读取消息,并处理,
如果有消息就处理,没消息就会陷入死循环,这种死循环会造成资源浪费。这时候可以使用blpop/brpop
## 延迟消息队列
延迟消息队列可以是通过zset来实现的,因为有个score ,我们可以把时间做为score, 将时间作为value存在redis 中,然后通过轮询的方式,去不断读取消息出来
如果消息是字符串,直接发送即可,如果是对象,则需要对对象经行序列化,这里我们使用的Json 来实现序列化和反序列化
package com.king;
public class JavaMessgae {
private String id;
private Object data;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
@Override
public String toString() {
return "JavaMessgae{" +
"id='" + id + '\'' +
", data=" + data +
'}';
}
}
消息队列
public class DelaMsgQueue {
private Jedis jedis;
private String queue;
public DelaMsgQueue(Jedis jedis,String queue){
this.jedis=jedis;
this.queue=queue;
}
/***
* 消息入队
* @param data 要发送的消息
*/
public void queue(Object data){
JavaMessgae msg = new JavaMessgae();
msg.setId(UUID.randomUUID().toString());
msg.setData(data);
//序列化
try {
String s = new ObjectMapper().writeValueAsString(msg);
// 消息发送
jedis.zadd(queue,System.currentTimeMillis()+5000,s);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
/**
* 消息消费
*/
public void loop(){
//消息不中断就
while(Thread.interrupted()){
//读取时间在0到当前时间
Set<String> strings = jedis.zrangeByScore(queue, 0, System.currentTimeMillis(), 0, 1);
//如果为空则休息500毫秒
if(strings.isEmpty()){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
//如果出现异常就退出去
break;
}
continue;
}
//如果读取到了这条消息,则直接加载
String next = strings.iterator().next();
if(jedis.zrem(queue,next)>0){
//抢到了,接下来直接处理业务
try {
JavaMessgae javaMessgae = new ObjectMapper().readValue(next, JavaMessgae.class);
System.out.println("搜到消息"+javaMessgae);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
}
}
package com.king.config;
import redis.clients.jedis.Jedis;
public class DelaMsgTest {
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis -> {
//消息队列
DelaMsgQueue delaMsgQueue = new DelaMsgQueue(jedis, "jjavaboy-delay-queue");
// 构造一个生产者
Thread producer = new Thread() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
delaMsgQueue.queue("www.java.com" + i);
}
}
};
//构造一个消费者
Thread consumer = new Thread() {
@Override
public void run() {
delaMsgQueue.loop();
}
};
//启动
producer.start();
consumer.start();
//休息7秒 停止程序
try {
Thread.sleep(7000);
consumer.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}