本文主要为了回顾一下 Java 对 Redis 的操作,对 SpringBoot 自带的 Redis 组件进行简单的封装,实现一个基础版的分布式锁。后续会进行扩展。话不多说,直接进入正题。
1.配置文件
POM
文件:
<dependencies>
<!-- cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
application.yml
文件:
spring:
## redis 配置
redis:
host: 127.0.0.1
port: 6379
password: 123456
## 连接 redis 超时时间
timeout: 3000ms
jedis:
## redis 连接池配置
pool:
## 最大空闲连接数
max-idle: 20
## 最小空闲连接数
min-idle: 0
## 最长阻塞等待时间
max-wait: 30
## 最大活跃连接数
max-active: 15
2.配置 RedisTemplate
@Configuration
public class RedisConfig
{
/**
* 配置RedisTemplate,设置key和value的序列化方式
*
* @return RedisTemplate
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
// 配置序列化
setSerializer(redisTemplate);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* 配置 RedisTemplate 序列化
*
* @param redisTemplate redisTemplate
*/
@SuppressWarnings("unchecked")
private void setSerializer(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//noinspection rawtypes,unchecked
Jackson2JsonRedisSerializer jsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jsonRedisSerializer.setObjectMapper(om);
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
}
}
这里如果不设置 RedisTemplate
的序列化方式话,默认会使用 JdkSerializationRedisSerializer
,虽然对使用上没什么影响,但数据的可读性会比较差。(可以使用默认的序列化器体验一下)
3.简单地封装常用功能
@SuppressWarnings("unchecked")
@Component
public class RedisTool {
private Logger logger = LoggerFactory.getLogger(RedisTool.class);
@Autowired
@SuppressWarnings("rawtypes")
private RedisTemplate redisTemplate;
@Autowired
private ObjectMapper objectMapper;
/**
* 添加缓存
*
* @param key key
* @param value 缓存内容
*/
public void put(Object key, Object value) {
if (verifyParam(key, value)) {
redisTemplate.opsForValue().set(key, value);;
}
}
/**
* 添加缓存
*
* @param key key
* @param value 缓存内容
* @param expireTime 过期时间
*/
public void put(Object key, Object value, Long expireTime) {
if (verifyParam(key, value)) {
redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(expireTime));
}
}
/**
* 添加缓存
*
* @param key key
* @param value value
* @param expireTime 过期时间
* @param timeUnit 时间单位
*/
public void put(Object key, Object value, Long expireTime, TimeUnit timeUnit) {
if (verifyParam(key, value)) {
redisTemplate.opsForValue().set(key, value, expireTime, timeUnit);
}
}
/**
* 获取 对象
*
* @param key key
* @param clazz 对象class
* @param <T> 对象泛型
* @return 对象
*/
public <T> T get(Object key, Class<T> clazz) {
if (verifyParam(key)) {
Object value = redisTemplate.opsForValue().get(key);
if (value != null) {
try {
String jsonStr = objectMapper.writeValueAsString(value);
return objectMapper.readValue(jsonStr, clazz);
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
/**
* 获取 字符串
*
* @param key key
* @return 字符串
*/
public String get(Object key) {
if (verifyParam(key)) {
Object o = redisTemplate.opsForValue().get(key);
return o == null ? null : String.valueOf(o);
}
return null;
}
/**
* 删除缓存
*
* @param key key
* @return 是否删除成功
*/
public Boolean delete(Object key) {
if (verifyParam(key)) {
return redisTemplate.delete(key);
}
return Boolean.FALSE;
}
/**
* 如果缓存不存在 添加
*
* @param key key
* @param value value
*/
public void setIfAbsent(Object key, Object value) {
if (verifyParam(key, value)) {
redisTemplate.opsForValue().setIfAbsent(key, value);
}
}
/**
* 如果缓存不存在 添加
*
* @param key key
* @param value value
* @param expireTime 过期时间
*/
public void setIfAbsent(Object key, Object value, long expireTime) {
if (!verifyParam(key, value)) {
return;
}
redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofSeconds(expireTime));
}
/**
* 检查 键值对合法性
*
* @param key key
* @param value value
* @return 是否合法
*/
private boolean verifyParam(Object key, Object value) {
if (key == null || value == null) {
logger.warn("Illegal param, null key or null value.");
return Boolean.FALSE;
}
return Boolean.TRUE;
}
/**
* 检查 key 合法性
*
* @param key key
* @return 是否合法
*/
private boolean verifyParam(Object key) {
if (key == null) {
logger.warn("Illegal param, null key.");
return Boolean.FALSE;
}
return Boolean.TRUE;
}
}
4. 锁的实现
抽象锁 RedisLock
,尝试获取锁和释放锁具体由其子类实现。
public abstract class RedisLock {
public RedisTool redisTool;
private String lockUUId;
public RedisLock(RedisTool redisTool) {
this.redisTool = redisTool;
}
public String getLockUUId() {
return lockUUId;
}
public void setLockUUId(String lockUUId) {
this.lockUUId = lockUUId;
}
/**
* 加锁
*/
public void acquire() {
if (tryAcquire()) {
redisTool.put(RedisTool.LOCK_NAME, lockUUId);
}
}
/**
* 加锁一定时间后自动释放锁
*
* @param leaseTime 超过该时间,自动释放锁
* @param timeUnit 时间单位
*/
public void acquire(Long leaseTime, TimeUnit timeUnit) {
if (tryAcquire()) {
redisTool.put(RedisTool.LOCK_NAME, lockUUId, leaseTime, timeUnit);
}
}
/**
* 尝试加锁
*
* @return 是否可以加锁
*/
public abstract Boolean tryAcquire();
/**
* 释放锁
*/
public void release() {
if (tryRelease()) {
redisTool.delete(RedisTool.LOCK_NAME);
}
}
/**
* 尝试释放锁
*
* @return 是否可以释放锁
*/
public abstract Boolean tryRelease();
}
SimpleRedisLock
:锁的简单实现
public class SimpleRedisLock extends RedisLock {
private Logger logger = LoggerFactory.getLogger(SimpleRedisLock.class);
private Long waitTime = 3000L;
public SimpleRedisLock(RedisTool redisTool) {
super(redisTool);
}
@Override
public Boolean tryAcquire() {
long startMills = System.currentTimeMillis();
String lockUUID = getLockUUId();
logger.debug("{} try to acquire the lock {}", Thread.currentThread().getName(), lockUUID);
while (System.currentTimeMillis() - startMills < waitTime) {
String existLock = redisTool.get(RedisTool.LOCK_NAME);
if (!(StringUtils.isNotBlank(existLock) && StringUtils.equals(lockUUID, existLock))) {
logger.debug("lock {} can be acquired", lockUUID);
return Boolean.TRUE;
}
}
logger.debug("{} try to acquire lock {} failed", Thread.currentThread().getName(), lockUUID);
return Boolean.FALSE;
}
@Override
public Boolean tryRelease() {
String lockUUID = getLockUUId();
String existLock = redisTool.get(RedisTool.LOCK_NAME);
if (StringUtils.isBlank(existLock) || !StringUtils.equals(lockUUID, existLock)) {
logger.debug("lock {} has been released", lockUUID);
return Boolean.TRUE;
}
if (StringUtils.equals(lockUUID, existLock)) {
logger.debug("lock {} can be released", lockUUID);
return Boolean.TRUE;
}
return Boolean.FALSE;
}
}
6.测试锁的功能
测试类:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RedisApplication.class)
public class TestSimpleRedisLock
{
private Logger logger = LoggerFactory.getLogger(TestSimpleRedisLock.class);
@Autowired
private RedisTool redisTool;
@Test
public void testSimpleRedisLock() throws InterruptedException
{
RedisLock lock = new SimpleRedisLock(redisTool);
lock.setLockUUId(UUID.randomUUID().toString());
// 2秒后自动释放锁
lock.acquire(2L, TimeUnit.SECONDS);
logger.info("main acquired lock {}", lock.getLockUUId());
Thread thread = new Thread(() -> {
lock.acquire();
logger.info("thread acquire lock {}", lock.getLockUUId());
lock.release();
logger.info("thread release lock {}", lock.getLockUUId());
});
thread.start();
// 手动释放锁,
lock.release();
logger.info("main release lock {}", lock.getLockUUId());
thread.join();
}
}
测试结果:
20:08:10.047 INFO 11020 --- [ main] com.demo.redis.core.TestSimpleRedisLock : main acquired lock 78cd8137-90f1-4830-9509-ff69b718083f
20:08:12.085 INFO 11020 --- [ Thread-3] com.demo.redis.core.TestSimpleRedisLock : thread acquire lock 78cd8137-90f1-4830-9509-ff69b718083f
20:08:12.157 INFO 11020 --- [ Thread-3] com.demo.redis.core.TestSimpleRedisLock : thread release lock 78cd8137-90f1-4830-9509-ff69b718083f
后记
代码中还有很多地方不够严谨,例如获取锁超时,失败重试等机制的实现,本文主要讨论的是分布式锁实现的思想,如有不当欢迎指正。
源码地址:https://github.com/NekoChips/SpringDemo/09.springboot-redis
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于