基于Redis分布式锁(一)

基于Redis分布式锁(一)

仅适用于单机版Redis(非集群)

1. Redis 指令

1.1 永久锁

SETNX key value

KEY不存在返回1,KEY已经存在返回0

1.2 限时锁

SET key value [EX seconds][PX milliseconds] [NX|XX]
  • EX seconds 设置指定的到期时间(单位:秒)
  • PX milliseconds 设置指定的到期时间(单位:毫秒)
  • NX 仅在键不存在时设置
  • XX 仅在键存在时才设置

1.3 解锁

DEL key

1.4 设置过期时间

# 1.面向剩余时间
# 单位秒
EXPIRE key seconds

# 单位毫秒
PEXPIRE key milliseconds


# 2.面向时间戳
# 时间戳(秒)
EXPIREAT key seconds

# 时间戳(毫秒)
PEXPIREAT key milliseconds

2. 代码实现

使用RedisTemplate<String, Object>

2.1 基础代码

  • 1、使用SETNX key value 上锁
  • 2、使用EXPIRE key seconds 设置锁过期时间
  • 3、使用DEL key 解锁
	public void redisTest() throws Exception {
        String key = "Lock";
        // 1.上锁
        Boolean isLock = redisTemplate.opsForValue().setIfAbsent(key, "Lock");
        // 2.设置过期时间
        redisTemplate.expire(key, 2, TimeUnit.SECONDS);
        while (isLock == null || !isLock) {
            TimeUnit.MILLISECONDS.sleep(100);
            redisTest();
            return;
        }
        try {
            // 业务代码 ...
        } finally {
            // 2.解锁
            redisTemplate.delete(key);
        }
    }
存在问题:
  • 1、非原子性:上锁-设置过期时间期间服务器宕机会导致锁无法释放,发生死锁
  • 2、会存在误删:线程A业务未执行完成锁过期了,线程B获取锁,线程A执行完后把锁释放了

2.2 优化上述问题

  • 1、使用SET key value [EX seconds][PX milliseconds] [NX|XX]指令
  • 2、释放锁前,先判断是否是自己的锁
	public void redisTest() throws Exception {
        String key = "Lock";
        String uuid = UUID.randomUUID().toString();
        // 1.上锁+设置过期时间
        Boolean isLock = redisTemplate.opsForValue().setIfAbsent(key, uuid, 2, TimeUnit.SECONDS);
        while (isLock == null || !isLock) {
            TimeUnit.MILLISECONDS.sleep(100);
            redisTest();
            return;
        }
        try {
                // 业务代码 ...
            } finally {
                // 2.解锁(先判断,再解锁)
                if (StringUtils.equals(uuid, redisTemplate.opsForValue().get(key))){
                    redisTemplate.delete(key);
                }
            }  
    }
存在问题:
  • 1、解锁非原子性:线程A在获取锁内容之后锁过期了,线程B获取锁成功,线程A将B上的锁释放了

2.3 使用Lua脚本解锁

1、EVAL 指令

Redis 提供 EVAL 指令执行 Lua 脚本

  • 一次性可以发送多个指令给redis执行,并且是有序的,遵循one-by-one规则
  • EVAL执行的脚本需要有返回值
EVAL script numkeys key [key...] arg [arg...]

# EVAL "if KEYS[1]>KEYS[2] then return ARGV[1] else return ARGV[2] end" 2 10 20 true false
# EVAL "return {KEYS[1], KEYS[2], ARGV[1],ARGV[2]}" 2 10 20 30 40
  • script:执行的脚本(必须)
  • numkeys:key的个数(必须)
  • key:键列表,下标从1开始:KEYS[1]
  • arg:值列表,下标从1开始:ARGV[1]
2、使用Lua
  • 使用Lua脚本的类库:redis.call() 执行redis指令
# 获取lock值
EVAL "return redis.call('get', 'lock')" 0

# 设置lock值为111
EVAL "return redis.call('set', 'lock', '111')" 0

# 传参设置lock值为111
EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 lock 111

# 删除lock值
EVAL "return redis.call('del', 'lock')" 0
3、Java代码使用
  • lua脚本
if redis.call('get', 'KEYS[1]')==ARGV[1]
then
	return redis.call('del', 'KEYS[1]')
else
	return 0
end
  • Java代码
	public void redisTest() throws Exception {
        String key = "Lock";
        String uuid = UUID.randomUUID().toString();
        // 1.上锁+设置过期时间
        Boolean isLock = redisTemplate.opsForValue().setIfAbsent(key, uuid, 2, TimeUnit.SECONDS);
        while (isLock == null || !isLock) {
            TimeUnit.MILLISECONDS.sleep(100);
            redisTest();
            return;
        }
        try {
                // 业务代码 ...
            } finally {
                String script = "if redis.call('get', 'KEYS[1]')==ARGV[1] " +
                        " then return redis.call('del', 'KEYS[1]') else return 0 end";
                // 2.原子性判断解锁
                redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class)
                , Collections.singletonList(key)
                , Collections.singletonList(uuid));
            }
    }

共同存在的问题:

1、A线程未执行完、锁过期失效了,B线程重新上锁成功(锁不会自动续期)

2、不可重入锁(当前线程只可获取锁·一次)

3. 失败重试机制

  • 高并发情况下可能存在获取不到锁情况,可以尝试等待-重试机制

3.1 循环模式

	public Object tautology(int time) throws Exception {
        Boolean isLock;
        for (int i = 0; i <= time; i++) {
            // 获取锁
            isLock = true;
            if (isLock) {
                // 成功获取锁就跳出循环
                break;
            }
            TimeUnit.MILLISECONDS.sleep(300);
        }

        try {
            // 业务代码 ...
            return null;
        } finally {
            // 释放锁
        }
    }

3.2 递归模式

	// 最大重试次数
    private final Integer MAX_TIME = 3;

    public Object tautology(int time) throws Exception {
        // 超过最大重试次数
        if (time > MAX_TIME) {
            // 打印日志
            return null;
        }

        // 获取锁并判断
        Boolean isLock = true;
        if (isLock) {
            try {
                // 业务代码 ...
                return null;
            } finally {
                // 释放锁
            }
        } else {
            TimeUnit.MILLISECONDS.sleep(300);
            // 重试
            return tautology(time + 1);
        }
    }

4.Redis序列化配置

  • 将对象类型存入redis,以便能够正确反序列化
  • 对NULL属性、集合、数组不进行序列化
  • 解决jackson2无法反序列化LocalDateTime
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 解决value的序列化方式
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);

        // 首先解决key的序列化方式
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // Hash序列方式
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        // 序列化时将类的数据类型存入json,以便反序列化的时候转换成正确的类型
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance
        , ObjectMapper.DefaultTyping.NON_FINAL);
        // 只针对非空的属性、集合、数组进行序列化
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);

        // 解决jackson2无法反序列化LocalDateTime的问题
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.registerModule(new JavaTimeModule());

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        return redisTemplate;
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hanlin-hl

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值