Skip to content

Commit 6e93a67

Browse files
Copilotbinarywang
andcommitted
Fix Redis lock key serialization inconsistency
Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com>
1 parent f8cdcfc commit 6e93a67

File tree

2 files changed

+110
-14
lines changed

2 files changed

+110
-14
lines changed

weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLock.java

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
package me.chanjar.weixin.common.util.locks;
22

33
import lombok.Getter;
4-
import org.springframework.data.redis.connection.RedisStringCommands;
5-
import org.springframework.data.redis.core.RedisCallback;
64
import org.springframework.data.redis.core.StringRedisTemplate;
75
import org.springframework.data.redis.core.script.DefaultRedisScript;
86
import org.springframework.data.redis.core.script.RedisScript;
9-
import org.springframework.data.redis.core.types.Expiration;
107

11-
import java.nio.charset.StandardCharsets;
128
import java.util.Collections;
13-
import java.util.List;
149
import java.util.UUID;
1510
import java.util.concurrent.TimeUnit;
1611
import java.util.concurrent.locks.Condition;
@@ -70,15 +65,16 @@ public boolean tryLock() {
7065
value = UUID.randomUUID().toString();
7166
valueThreadLocal.set(value);
7267
}
73-
final byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
74-
final byte[] valueBytes = value.getBytes(StandardCharsets.UTF_8);
75-
List<Object> redisResults = redisTemplate.executePipelined((RedisCallback<String>) connection -> {
76-
connection.set(keyBytes, valueBytes, Expiration.milliseconds(leaseMilliseconds), RedisStringCommands.SetOption.SET_IF_ABSENT);
77-
connection.get(keyBytes);
78-
return null;
79-
});
80-
Object currentLockSecret = redisResults.size() > 1 ? redisResults.get(1) : redisResults.get(0);
81-
return currentLockSecret != null && currentLockSecret.toString().equals(value);
68+
69+
// Use high-level StringRedisTemplate API to ensure consistent key serialization
70+
Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(key, value, leaseMilliseconds, TimeUnit.MILLISECONDS);
71+
if (Boolean.TRUE.equals(lockAcquired)) {
72+
return true;
73+
}
74+
75+
// Check if we already hold the lock (reentrant behavior)
76+
String currentValue = redisTemplate.opsForValue().get(key);
77+
return value.equals(currentValue);
8278
}
8379

8480
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package me.chanjar.weixin.common.util.locks;
2+
3+
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
4+
import org.springframework.data.redis.core.StringRedisTemplate;
5+
import org.springframework.data.redis.serializer.StringRedisSerializer;
6+
import org.testng.annotations.BeforeTest;
7+
import org.testng.annotations.Test;
8+
9+
import static org.testng.Assert.*;
10+
11+
/**
12+
* 测试 RedisTemplateSimpleDistributedLock 在自定义 Key 序列化时的兼容性
13+
*
14+
* 这个测试验证修复后的实现确保 tryLock 和 unlock 使用一致的键序列化方式
15+
*/
16+
@Test(enabled = false) // 默认禁用,需要Redis实例才能运行
17+
public class RedisTemplateSimpleDistributedLockSerializationTest {
18+
19+
private RedisTemplateSimpleDistributedLock redisLock;
20+
private StringRedisTemplate redisTemplate;
21+
22+
@BeforeTest
23+
public void init() {
24+
JedisConnectionFactory connectionFactory = new JedisConnectionFactory();
25+
connectionFactory.setHostName("127.0.0.1");
26+
connectionFactory.setPort(6379);
27+
connectionFactory.afterPropertiesSet();
28+
29+
// 创建一个带自定义键序列化的 StringRedisTemplate
30+
StringRedisTemplate redisTemplate = new StringRedisTemplate(connectionFactory);
31+
32+
// 使用自定义键序列化器,模拟在键前面添加前缀的场景
33+
redisTemplate.setKeySerializer(new StringRedisSerializer() {
34+
@Override
35+
public byte[] serialize(String string) {
36+
if (string == null) return null;
37+
// 添加 "System:" 前缀,模拟用户自定义的键序列化
38+
return super.serialize("System:" + string);
39+
}
40+
41+
@Override
42+
public String deserialize(byte[] bytes) {
43+
if (bytes == null) return null;
44+
String result = super.deserialize(bytes);
45+
// 移除前缀进行反序列化
46+
return result != null && result.startsWith("System:") ? result.substring(7) : result;
47+
}
48+
});
49+
50+
this.redisTemplate = redisTemplate;
51+
this.redisLock = new RedisTemplateSimpleDistributedLock(redisTemplate, "test_lock_key", 60000);
52+
}
53+
54+
@Test(description = "测试自定义键序列化器下的锁操作一致性")
55+
public void testLockConsistencyWithCustomKeySerializer() {
56+
// 1. 获取锁应该成功
57+
assertTrue(redisLock.tryLock(), "第一次获取锁应该成功");
58+
assertNotNull(redisLock.getLockSecretValue(), "锁值应该存在");
59+
60+
// 2. 验证键已正确存储(通过 redisTemplate 直接查询)
61+
String actualValue = redisTemplate.opsForValue().get("test_lock_key");
62+
assertEquals(actualValue, redisLock.getLockSecretValue(), "通过 redisTemplate 查询的值应该与锁值相同");
63+
64+
// 3. 再次尝试获取同一把锁应该成功(可重入)
65+
assertTrue(redisLock.tryLock(), "可重入锁应该再次获取成功");
66+
67+
// 4. 释放锁应该成功
68+
redisLock.unlock();
69+
assertNull(redisLock.getLockSecretValue(), "释放锁后锁值应该为空");
70+
71+
// 5. 验证键已被删除
72+
actualValue = redisTemplate.opsForValue().get("test_lock_key");
73+
assertNull(actualValue, "释放锁后 Redis 中的键应该被删除");
74+
75+
// 6. 释放已释放的锁应该是安全的
76+
redisLock.unlock(); // 不应该抛出异常
77+
}
78+
79+
@Test(description = "测试不同线程使用相同键的锁排他性")
80+
public void testLockExclusivityWithCustomKeySerializer() throws InterruptedException {
81+
// 第一个锁实例获取锁
82+
assertTrue(redisLock.tryLock(), "第一个锁实例应该成功获取锁");
83+
84+
// 创建第二个锁实例使用相同的键
85+
RedisTemplateSimpleDistributedLock anotherLock = new RedisTemplateSimpleDistributedLock(
86+
redisTemplate, "test_lock_key", 60000);
87+
88+
// 第二个锁实例不应该能获取锁
89+
assertFalse(anotherLock.tryLock(), "第二个锁实例不应该能获取已被占用的锁");
90+
91+
// 释放第一个锁
92+
redisLock.unlock();
93+
94+
// 现在第二个锁实例应该能获取锁
95+
assertTrue(anotherLock.tryLock(), "第一个锁释放后,第二个锁实例应该能获取锁");
96+
97+
// 清理
98+
anotherLock.unlock();
99+
}
100+
}

0 commit comments

Comments
 (0)