redis-实现

redis实现加锁的几种方法示例详解

1. redis加锁分类

redis能用的的加锁命令分表是INCR、SETNX、SET

2. 第一种锁命令INCR

这种加锁的思路是, key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作进行加一。
然后其它用户在执行 INCR 操作进行加一时,如果返回的数大于 1 ,说明这个锁正在被使用当中。

    1、 客户端A请求服务器获取key的值为1表示获取了锁 

    2、 客户端B也去请求服务器获取key的值为2表示获取锁失败

    3、 客户端A执行代码完成,删除锁

    4、 客户端B在等待一段时间后在去请求的时候获取key的值为1表示获取锁成功

    5、 客户端B执行代码完成,删除锁

1
2
$redis->incr($key);
$redis->expire($key$ttl); //设置生成时间为1秒

3. 第二种锁SETNX

这种加锁的思路是,如果 key 不存在,将 key 设置为 value

如果 key 已存在,则 SETNX 不做任何动作

    1、 客户端A请求服务器设置key的值,如果设置成功就表示加锁成功

    2、 客户端B也去请求服务器设置key的值,如果返回失败,那么就代表加锁失败

    3、 客户端A执行代码完成,删除锁

    4、 客户端B在等待一段时间后在去请求设置key的值,设置成功

    5、 客户端B执行代码完成,删除锁   

1
2
$redis->setNX($key$value);
$redis->expire($key$ttl);

4. 第三种锁SET

上面两种方法都有一个问题,会发现,都需要设置 key 过期。那么为什么要设置key过期呢?如果请求执行因为某些原因意外退出了,导致创建了锁但是没有删除锁,那么这个锁将一直存在,以至于以后缓存再也得不到更新。于是乎我们需要给锁加一个过期时间以防不测。

但是借助 Expire 来设置就不是原子性操作了。所以还可以通过事务来确保原子性,但是还是有些问题,所以官方就引用了另外一个,使用 SET 命令本身已经从版本 2.6.12 开始包含了设置过期时间的功能。

    1、 客户端A请求服务器设置key的值,如果设置成功就表示加锁成功

    2、 客户端B也去请求服务器设置key的值,如果返回失败,那么就代表加锁失败

    3、 客户端A执行代码完成,删除锁

    4、 客户端B在等待一段时间后在去请求设置key的值,设置成功

    5、 客户端B执行代码完成,删除锁

1
$redis->set($key$valuearray('nx''ex' => $ttl)); //ex表示秒

5. 其它问题

虽然上面一步已经满足了我们的需求,但是还是要考虑其它问题?

    1、 redis发现锁失败了要怎么办?中断请求还是循环请求?

    2、 循环请求的话,如果有一个获取了锁,其它的在去获取锁的时候,是不是容易发生抢锁的可能?

    3、 锁提前过期后,客户端A还没执行完,然后客户端B获取到了锁,这时候客户端A执行完了,会不会在删锁的时候把B的锁给删掉?

6. 解决办法

针对问题1:使用循环请求,循环请求去获取锁

针对问题2:针对第二个问题,在循环请求获取锁的时候,加入睡眠功能,等待几毫秒在执行循环

针对问题3:在加锁的时候存入的key是随机的。这样的话,每次在删除key的时候判断下存入的key里的value和自己存的是否一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   do //针对问题1,使用循环
     $timeout = 10;
     $roomid = 10001;
     $key 'room_lock';
     $value 'room_'.$roomid//分配一个随机的值针对问题3
     $isLock = Redis::set($key$value'ex'$timeout'nx');//ex 秒
     if ($isLock) {
if (Redis::get($key) == $value) { //防止提前过期,误删其它请求创建的锁
  //执行内部代码
  Redis::del($key);
  continue;//执行成功删除key并跳出循环
}
     else {
usleep(5000); //睡眠,降低抢锁频率,缓解redis压力,针对问题2
     }
   while(!$isLock);

7. 另外一个锁

以上的锁完全满足了需求,但是官方另外还提供了一套加锁的算法,这里以PHP为例

1
2
3
4
5
6
7
8
9
10
11
12
13
$servers = [
  ['127.0.0.1', 6379, 0.01],
  ['127.0.0.1', 6389, 0.01],
  ['127.0.0.1', 6399, 0.01],
];
 
$redLock new RedLock($servers);
 
//加锁
$lock $redLock->lock('my_resource_name', 1000);
 
//删除锁
$redLock->unlock($lock)

上面是官方提供的一个加锁方法,就是和第6的大体方法一样,只不过官方写的更健壮。所以可以直接使用官方提供写好的类方法进行调用。官方提供了各种语言如何实现锁。

------------------------------------------------------------------------------------------------

springboot-redis-redisson 设置多库

package cn.iocoder.yudao.framework.redis.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.StringCodec;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.redisson.spring.data.connection.RedissonConnectionFactory;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;

/**
* Redis 配置类
*/
@Configuration
public class YudaoRedisAutoConfiguration {

/**
* 创建 RedisTemplate Bean,使用 JSON 序列化方式
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// 创建 RedisTemplate 对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 设置 RedisConnection 工厂。
template.setConnectionFactory(factory);
// 使用 String 序列化方式,序列化 KEY 。
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
template.setValueSerializer(RedisSerializer.json());
template.setHashValueSerializer(RedisSerializer.json());
return template;
}

@Bean(name = "redisTemplateOne")
public RedisTemplate<String, Object> redisTemplateOneDatabase(RedisProperties redisProperties) {
// 创建 RedisTemplate 对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 配置类
Config config = new Config();
config.setCodec(StringCodec.INSTANCE);
SingleServerConfig singleConfig = config.useSingleServer();
//"redis://127.0.0.1:6379"
singleConfig.setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort());
singleConfig.setPassword(redisProperties.getPassword());
// 指定连接的数据库
singleConfig.setDatabase(1);
RedissonClient redissonClient = Redisson.create(config);
RedissonConnectionFactory factory = new RedissonConnectionFactory(redissonClient);

// 设置 RedisConnection 工厂。
template.setConnectionFactory(factory);
// 使用 String 序列化方式,序列化 KEY 。
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
template.setValueSerializer(RedisSerializer.json());
template.setHashValueSerializer(RedisSerializer.json());

return template;
}

}

------------------------------------------------------------------------------------------------

springboot使用Redis+Redisson实现分布式锁Demo&&源码分析

对于问题1到问题4,在下面手写Demo中都有解决,并添加了注释,下面看代码。

2.1.1.1 主线程
public class RedisLockDemo {
//随便弄个key的名字
private static final String LOCK_KEY = "distributedLock:key";

//主线程
public static void main(String[] args) {
//获取redis客户端
RedisClient redisClient = RedisClient.getInstance();
//开启两个工作线程,模拟分布式服务中的两个服务
for (int i = 0; i < 1; i++) {
startAWork(redisClient, String.valueOf(i), 10);
}
}

/**
* 开启一个工作线程,模拟分布式中的一个服务,抢分布式锁
*
* @param redisClient redis客户端
* @param threadName 线程名称
* @param lengthOfWork 工作时长 秒
*/
public static void startAWork(RedisClient redisClient, String threadName, int lengthOfWork) {
new Thread(() -> {
try {
//生成并保存 获取分布式锁的 请求id,解决问题二
String requestId = UUID.randomUUID().toString();
RedisLockThreadLocalContext.getThreadLocal().set(requestId);

//获取分布式锁,设置过期时间2s,解决问题一
boolean result = RedisTool.tryGetDistributedLock(redisClient.getJedis(), LOCK_KEY, requestId, 2000);

if (result) {//如果成功获取到锁
//开一个守护线程延长锁的过期时间
Thread thread = new Thread(() -> {
while (true) {
Jedis jedis = redisClient.getJedis();
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("守护线程延长锁的过期时间1s");
jedis.setex(LOCK_KEY, 1, requestId);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
}
}
});
thread.setDaemon(true);
thread.start();

System.out.println("线程" + threadName + "拿到锁,干点事情");
//睡眠一定时间,模拟业务耗时
TimeUnit.SECONDS.sleep(lengthOfWork);
} else {
System.out.println("线程" + threadName + "没有拿到锁");
}
} catch (Exception e) {
//
} finally {
//释放分布式锁
String requestId = RedisLockThreadLocalContext.getThreadLocal().get();
boolean result = RedisTool.releaseDistributedLock(redisClient.getJedis(), LOCK_KEY, requestId);
if (result) {
System.out.println("线程" + threadName + "释放锁");
} else {
System.out.println("线程" + threadName + "释放锁失败");
}

}
System.out.println("线程" + threadName + "结束");
}).start();
}
}

主线程说明:

主线程比较简单,只开启了两个工作线程,模拟抢分布式锁的过程;
具体的startAWork()方法中,新建了工作线程,使用睡眠时间来模拟执行业务逻辑的耗时;
在 RedisTool#tryGetDistributedLock()方法中,传入了过期时间参数,方法内容看下问代码。这个参数解决了问题一;
在 RedisTool#tryGetDistributedLock()方法中,传入了requestId参数,这个是一个随机UUID,用来标识每一次加锁的线程,同时这个参数保存在了线程本地变量ThreadLocal中,解决了问题二。
在开启工作线程后,代码中紧接着又开启另外一个线程,并使用thread.setDaemon(true);标识为守护线程;这个守护线程的任务就是死循环延长锁的过期时间;当业务线程执行完毕后,这个守护线程会自动销毁。注意循环的时间间隔要小于锁的过期时间,一般设置为过期时间的一半即可。
2.1.1.2 其他辅助类
添加jedis依赖包:

<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>

使用JedisPool初始化一个Jedis客户端:

/**
* Description:Redis客户端
*/
public class RedisClient {
private static final Logger LOGGER = LoggerFactory.getLogger(RedisClient.class);
private static RedisClient instance = new RedisClient();
private JedisPool pool;

private RedisClient() {
init();
}

public static RedisClient getInstance() {
return instance;
}

public Jedis getJedis() {
return pool.getResource();
}

/**
* 初始化redis连接池
*/
private void init() {
int maxTotal = 10;
String ip = "redis IP";
String pwd = "redis 密码";
int port = 6379;

JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(maxTotal);
jedisPoolConfig.setMaxIdle(20);
jedisPoolConfig.setMaxWaitMillis(6000);
pool = new JedisPool(jedisPoolConfig, ip, port, 5000, pwd);
LOGGER.info("连接池初始化成功 ip={}, port={}, maxTotal={}", ip, port, maxTotal);
}
}

上述代码初始化了redis连接信息,属于固定代码,没啥好解释的,继续往下看代码。

/**
* Description:redis分布式锁访问工具类,提供具体的获取锁,释放锁方法
*/
public class RedisTool {

private static final String LOCK_SUCCESS = "OK";
private static final Long RELEASE_SUCCESS = 1L;

private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";

/**
* 尝试获取分布式锁
*
* @param jedis Redis客户端
* @param lockKey 锁的key
* @param requestId 锁的Value,值是个唯一标识,用来标记加锁的线程请求;可以使用UUID.randomUUID().toString()方法生成
* @param expireTime 过期时间 ms
* @return 是否获取成功,成功返回true,否则false
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = null;
try {
result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
}
return LOCK_SUCCESS.equals(result);
}

/**
* 释放分布式锁
*
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识,锁的Value
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
Object result = null;
try {
//使用lua脚本保证原子性
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
}
return RELEASE_SUCCESS.equals(result);
}
}

RedisTool工具类,提供了加锁和解锁的两个方法;
tryGetDistributedLock()加锁方法设置了过期时间,解决了问题一;
releaseDistributedLock()解锁方法中使用了lua脚本,具备原子性,解锁时先判断key的value值,也就是当初加锁保存的requestId是不是和自己线程保存的一致,一致才说明是自己当初加的锁,方可进行解锁;不一致说明自己加锁已经自动过期,无需解锁;这个解决了问题二和问题三。
/**
* Description:保存redis分布式锁的请求id
*/
public class RedisLockThreadLocalContext {

private static ThreadLocal<String> threadLocal = new NamedThreadLocal<>("REDIS-LOCK-LOCAL-CONTEXT");

public static ThreadLocal<String> getThreadLocal() {
return threadLocal;
}
}
1
2
3
4
5
6
7
8
9
10
11
上述RedisLockThreadLocalContext中创建了一个threadLocal单例,用于保存加锁时设置的requestId。当然在使用线程池时,get完数据要注意清除里面的保存信息,这里就不写那么详细了。

2.1.2 redisson方案
对于问题1到问题4,上面手写的方案,实际Redisson框架已经帮我们实现了,只需要简单的几行代码。

Redisson实现了可重入锁,公平锁等各种java中定义的锁类型,相关资料可参考官方文档:https://github.com/redisson/redisson/wiki/目录

2.1.2.1 redisson原生方式
依赖包:

<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>

客户端配置:

@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}

使用:

public void test() {
RLock lock = redisson.getLock("keykeykey");
try {
boolean b = lock.tryLock(30, TimeUnit.SECONDS);
if(b){
//执行业务
}
//
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

2.1.2.2 springboot starter方式
依赖:

<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.10.6</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.10.6</version>
</dependency>

使用:
使用redisson-spring-boot-starter更简单,上面的@Bean Redisson 都不用配置,可直接在业务代码中注入RedissonClient:

@Autowired
private RedissonClient redissonClient;
1
2
RedissonClient是个接口,它的实现类就是Redisson,因此使用RedissonClient就是使用Redisson:


public void test() {
RLock lock = redissonClient.getLock("keykeykey");
try {
boolean b = lock.tryLock(30, TimeUnit.SECONDS);
if(b){
//执行业务
}
//
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
方法说明
上面两种方式得到的锁,都是RLock 类型,实现类是RedissonLock:

RLock lock = redissonClient.getLock("keykeykey");
或者:
RLock lock = redisson.getLock("keykeykey");
getLock源码如下:

下面针对RedissonLock中的常用方法,进行一些说明:


public void lock();
public void lock(long leaseTime, TimeUnit unit);
public boolean tryLock();
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException

lock(long leaseTime, TimeUnit unit):阻塞获取锁,拿到锁之前,线程处于阻塞等待状态,拿到锁后,设置leaseTime:
leaseTime:持有锁的时间,也就是设置的redis key 过期时间,超过此时间没有主动释放锁的话,会被redis释放;
unit :单位;
lock() :阻塞获取锁,拿到锁之前,线程处于阻塞等待状态;拿到锁之后,没有设置过期时间,除非主动释放锁,否则锁不会被释放;
tryLock(long waitTime, long leaseTime, TimeUnit unit):尝试获取锁,最长等待waitTime,等待内拿到锁返回true,否则false;
waitTime :阻塞等待获取锁的时间,超过此时间,则不继续等待;
leaseTime :拿到锁后,设置redis key的过期时间;
unit :时间单位;
tryLock(long waitTime, TimeUnit unit):与第3个一样,不同指出是此方法设置的leaseTime=-1,也就是在线程存活期间,redis key 默认不会被redis释放;
tryLock() :锁可用,就立马返回ture,否则立马返回false;
2.1.2.4 leaseTime特殊说明
上文中提到加锁的参数leaseTime,这里再对其进行进一步阐述,leaseTime的含义是持有锁的时间。

本文一开头提到的第4个问题,Redisson已经帮我们解决,就是子线程会对key的过期时间进行续期,那么是否续期不是必然的,而是通过leaseTime参数控制。

下面我们从lock(long leaseTime, TimeUnit unit)方法作为加锁入口,分析下leaseTime参数的具体作用:

先看下官方对leaseTime的解释:

* @param leaseTime the maximum time to hold the lock after granting it,
* before automatically releasing it if it hasn't already been released by invoking <code>unlock</code>.
* If leaseTime is -1, hold the lock until explicitly unlocked.
其中提到,If leaseTime is -1,如果leaseTime=-1,则锁会被一直持有,直到主动unlock,那么锁是如何一直被持有的?难道真的没有设置锁的过期时间吗?

从上图的调用栈,进入到第2步骤:

private RFuture<Boolean> tryAcquireOnceAsync(long leaseTime, TimeUnit unit, long threadId) {
//如果leaseTime不是-1,比如设置了30s,那么redis key的过期时间就是30秒
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
}
// 如果leaseTime 等于 -1,代码继续
//注意tryLockInnerAsync第一个参数,跟进去发现值是lockWatchdogTimeout:
//private long lockWatchdogTimeout = 30 * 1000;
//也即是不设置过期时间,默认也是加了过期时间的,默认是30s
RFuture<Boolean> ttlRemainingFuture =
tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}

// lock acquired
if (ttlRemaining) {
//这里进行锁过期时间续期
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}


直接看代码上的注释,
也就是不设置过期时间,默认也是加了过期时间的,默认是30s。然后通过 scheduleExpirationRenewal(threadId);方法进行锁过期时间的续期:

private void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
//需要续期的key和线程信息,放到map中,后面有单独的线程从此map中获取并进行续期
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
//执行续期
renewExpiration();
}
}

点renewExpiration()进去跟进:

//更新过期时间
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}

//使用定时任务进行更新,这里是一个新的线程,使用的是netty的定时工具HashedWheelTimer
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}

//执行续期动作
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}

if (res) {
// reschedule itself
renewExpiration();
}
});
}
//定时任务执行时间internalLockLeaseTime / 3,internalLockLeaseTime默认是30s
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

ee.setTimeout(task);
}


renewExpirationAsync(threadId);执行了续期动作,跟进去:

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.<Object>singletonList(getName()),
//ARGV[1])是internalLockLeaseTime=30s
internalLockLeaseTime, getLockName(threadId));
}
总结:设置releaseTime=-1时,Redisson并不是不设置锁的持有时间,而是默认设置了30s,然后通过netty的定时任务每10s就去进行续期,续期长度是30s。 当然,如果拿到锁的主线程挂了,挂了分两种情况:

线程抛出异常:这种情况,我们会在try finaly中进行解锁处理;
整个机器挂了:那么续期任务的子线程自然也没了,也就不会对锁进行续期,锁等30s也就被redis释放了,不会产生锁不被释放的问题。
2.2 问题5方案
对于宕机的问题,redis作者已经给出了方案,那就是RedLock算法,原理是对redis集群的每个节点都加锁,然后判断超过半数的节点返回true,表示加锁成功。Redisson框架实现了RedLock算法,具体使用如下。

Demo:

RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");

RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();
关键是使用了红锁:RedissonRedLock 。

2.3 redisson分布式锁失效场景
1.应用运行过程中发生了fullgc,导致系统长时间停滞,redisson锁守护线程无法自动进行锁的续期,导致锁过期被释放。
这种目前没有好的方案,只能从业务上来规避,或者建立完善的告警机制,及时发现问题。

------------------------------------------------------------------------------------------------

StringRedisTemplate和RedisTemplate

一、spring-data-redis
1、spring-data-redis是什么
spring-data-redis是spring-data模块的一部分,专门用来支持在spring管理项目对redis的操作,使用java操作redis最常用的是使用jedis,但并不是只有jedis可以使用,像jdbc-redis,jredis也都属于redis的java客户端,他们之间是无法兼容的,如果你在一个项目中使用了jedis,然后后来决定弃用掉改用jdbc-redis就比较麻烦了。而spring-data-redis提供了redis的java客户端的抽象,在开发中可以忽略掉切换具体的客户端所带来的影响,而且他本身就属于spring的一部分,比起单纯的使用jedis,更加稳定,管理起来更加自动化。

2、spring-data-redis的特性
1)自动管理连接池,提供了一个高度封装的RedisTemplate类

2)针对jedis客户端的大量api进行了归类封装,把同一类型的操作封装成了Operation接口,支持redis中的五种数据类型的操作。

3)针对数据的"序列化与反序列化"提供了多种可以选择的策略(RedisSerializer)

JdkSerializationRedisSerializer:当需要存储java对象时使用
StringRedisSerializer:当需要存储string类型的字符串时使用
JacksonJsonRedisSerializer:将对象序列化成json的格式存储在redis中,需要jackson-json工具的支持
二、StringRedisTemplate和RedisTemplate
1、RedisTemplate和StringRedisTemplate区别
1)StringRedisTemplate继承RedisTemplate。

2)RedisTemplate看这个类的名字后缀是Template,如果了解过Spring如何连接关系型数据库的,大概不会难猜出这个类是做什么的 ,它跟JdbcTemplate一样封装了对Redis的一些常用的操作,当然StringRedisTemplate跟RedisTemplate功能类似那么肯定就会有人问,为什么会需要两个Template呢,一个不就够了吗?其实他们两者之间的区别主要在于他们使用的序列化类是不同的。

StringRedisTemplate的API假定所有的数据类型化都是字符类型,即key和value都是字符串类型。默认采用的是String的序列化策略,即StringRedisSerializer,保存的key和value都是采用此策略序列化保存的。
RedisTemplate默认采用的是JDK的序列化策略,即JdkSerializationRedisSerializer,保存的key和value都是采用此策略序列化保存的。
2、RedisTemplate常用方法
redisTemplate有两个方法经常用到,一个是opsForXXX一个是boundXXXOps,XXX是value的类型,前者获取到一个Opercation,但是没有指定操作的key,可以在一个连接(事务)内操作多个key以及对应的value;后者会获取到一个指定了key的operation,在一个连接内只操作这个key对应的value。
 
redisTemplate的5个常用方法如下:

redisTemplate.opsForValue();//操作字符串
redisTemplate.opsForHash();//操作hash
redisTemplate.opsForList();//操作list
redisTemplate.opsForSet();//操作set
redisTemplate.opsForZSet();//操作有序set
看下ValueOperation和BoundValueOperation的区别

ValueOperations valueOperations = redisTemplate.opsForValue();
BoundValueOperations<String, User> boundValueOps = redisTemplate.boundValueOps("key");

三、具体使用
spring-boot-autoconfigure-2.0.4.RELEASE.jar包中RedisAutoConfiguration.java已经自动声明了两个redis操作bean:

RedisAutoConfiguration.java源码:

@Configuration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}

@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}

因此我们只要在使用的地方注入使用即可:

@Autowired
StringRedisTemplate stringRedisTemplate; //操作 k-v 字符串
@Autowired
RedisTemplate redisTemplate; //k- v 都是对象

---------------------------------------

一、RedisTemplate和StringRedisTemplate的区别:
两者的关系是StringRedisTemplate继承RedisTemplate。
两者的数据是不共通的;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据,RedisTemplate只能管理RedisTemplate中的数据。
SDR默认采用的序列化策略有两种,一种是String的序列化策略,一种是JDK的序列化策略。
StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的(StringRedisSerializer)。

RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。(JdkSerializationRedisSerializer)

二、StringRedistemplate的源码
StringRedistemplate的源码

package org.springframework.data.redis.core;

import org.springframework.data.redis.connection.DefaultStringRedisConnection;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializer;

public class StringRedisTemplate extends RedisTemplate<String, String> {
public StringRedisTemplate() {
this.setKeySerializer(RedisSerializer.string());
this.setValueSerializer(RedisSerializer.string());
this.setHashKeySerializer(RedisSerializer.string());
this.setHashValueSerializer(RedisSerializer.string());
}

public StringRedisTemplate(RedisConnectionFactory connectionFactory) {
this();
this.setConnectionFactory(connectionFactory);
this.afterPropertiesSet();
}

protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) {
return new DefaultStringRedisConnection(connection);
}
}

从上面创建StringRedisTemplate的无参构造方法可以看出,此时将keySerializer、valueSerializer、hashKeySerializer、hashValueSerializer的序列化方式为stringSerializer,也就是StringRedisSerializer序列化方式;此时执行完整个方法后,还需要接着执行setConnectionFactory()方法,然后转向他的父类RedisTemplate中的afterPropertiesSet方法,此时上述四个序列化方式已经设置;

三、Redistemplate的序列化
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 1.创建 redisTemplate 模版
RedisTemplate<Object, Object> template = new RedisTemplate<>();
// 2.关联 redisConnectionFactory
template.setConnectionFactory(redisConnectionFactory);
// 3.创建 序列化类
GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class);
// 6.序列化类,对象映射设置
// 7.设置 value 的转化格式和 key 的转化格式
template.setValueSerializer(genericToStringSerializer);
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}

三、Redis使用的区别
RedisTemplate使用的序列类在在操作数据的时候,比如说存入数据会将数据先序列化成字节数组然后在存入Redis数据库,这个时候打开Redis查看的时候,你会看到你的数据不是以可读的形式展现的,而是以字节数组显示,类似下面(RedisTemplate)

当然从Redis获取数据的时候也会默认将数据当做字节数组转化,这样就会导致一个问题,当需要获取的数据不是以字节数组存在redis当中而是正常的可读的字符串的时候,比如说下面这种形式的数据(StringRedisTemplate)

当Redis当中的数据值是以数组形式显示出来的时候,只能使用RedisTemplate才能获取到里面的数据。
当Redis当中的数据值是以可读的形式显示出来的时候,只能使用StringRedisTemplate才能获取到里面的数据。
所以当你使用RedisTemplate获取不到数据为NULL时,一般是获取的方式错误。检查一下数据是否可读即可。

四、使用总结:
StringRedisTemplate:当你的redis数据库里面本来存的是字符串数据或者你要存取的数据就是字符串类型数据的时候。
RedisTemplate:但是如果你的数据是复杂的对象类型,而取出的时候又不想做任何的数据转换,直接从Redis里面取出一个对象。

------------------------------------------------------------------------------------------------

SpringBoot 操作 Redis的各种实现(以及Jedis、Redisson、Lettuce的区别比较)

一、Jedis,Redisson,Lettuce三者的区别

共同点:都提供了基于Redis操作的Java API,只是封装程度,具体实现稍有不同。

不同点:

1.1、Jedis

是Redis的Java实现的客户端。支持基本的数据类型如:String、Hash、List、Set、Sorted Set。

特点:使用阻塞的I/O,方法调用同步,程序流需要等到socket处理完I/O才能执行,不支持异步操作。Jedis客户端实例不是线程安全的,需要通过连接池来使用Jedis。

1.2、Redisson

优点点:分布式锁,分布式集合,可通过Redis支持延迟队列。

1.3、 Lettuce

用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。

基于Netty框架的事件驱动的通信层,其方法调用是异步的。Lettuce的API是线程安全的,所以可以操作单个Lettuce连接来完成各种操作。

------------------------------------------------------------------------------------------------

 

Redis源码分析--结构解析

 

 从今天起,本人将会展开对Redis源码的学习,Redis的代码规模比较小,非常适合学习,是一份非常不错的学习资料,数了一下大概100个文件左右的样子,用的是C语言写的。希望最终能把他啃完吧,C语言好久不用,快忘光了。分析源码的第一步,先别急着想着从哪开始看起,先浏览一下源码结构,可以模块式的渐入,不过比较坑爹的是,Redis的源码全部放在在里面的src目录里,一下90多个文件统统在里面了,所以我选择了拆分,按功能拆分,有些文件你看名字就知道那是干什么的。我拆分好后的而结果如下:

11个包,这样每个包中的文件就比较可接受了,但是分出这个类别,我也是花了一定时间,思考了下,Redis下的主要的一些文件的特征,最后定的,应该算是比较全的了。

下面开始一个包一个包的介绍:

test:(测试)
1.memtest.c 内存检测
2.redis_benchmark.c 用于redis性能测试的实现。
3.redis_check_aof.c 用于更新日志检查的实现。
4.redis_check_dump.c 用于本地数据库检查的实现。
5.testhelp.c 一个C风格的小型测试框架。

struct:(结构体)
1.adlist.c 用于对list的定义,它是个双向链表结构
2.dict.c 主要对于内存中的hash进行管理
3.sds.c 用于对字符串的定义
4.sparkline.c 一个拥有sample列表的序列
5.t_hash.c hash在Server/Client中的应答操作。主要通过redisObject进行类型转换。
6.t_list.c list在Server/Client中的应答操作。主要通过redisObject进行类型转换。
7.t_set.c  set在Server/Client中的应答操作。主要通过redisObject进行类型转换。
8.t_string.c string在Server/Client中的应答操作。主要通过redisObject进行类型转换。
9.t_zset.c zset在Server/Client中的应答操作。主要通过redisObject进行类型转换。
10.ziplist.c  ziplist是一个类似于list的存储对象。它的原理类似于zipmap。
11.zipmap.c  zipmap是一个类似于hash的存储对象。

data:(数据操作)
1.aof.c 全称为append only file,作用就是记录每次的写操作,在遇到断电等问题时可以用它来恢复数据库状态。
2.config.c 用于将配置文件redis.conf文件中的配置读取出来的属性通过程序放到server对象中。
3.db.c对于Redis内存数据库的相关操作。
4.multi.c用于事务处理操作。
5.rdb.c  对于Redis本地数据库的相关操作,默认文件是dump.rdb(通过配置文件获得),包括的操作包括保存,移除,查询等等。
6.replication.c 用于主从数据库的复制操作的实现。

tool:(工具)
1.bitops.c 位操作相关类
2.debug.c 用于调试时使用
3.endianconv.c 高低位转换,不同系统,高低位顺序不同
4.help.h  辅助于命令的提示信息
5.lzf_c.c 压缩算法系列
6.lzf_d.c  压缩算法系列
7.rand.c 用于产生随机数
8.release.c 用于发步时使用
9.sha1.c sha加密算法的实现
10.util.c  通用工具方法
11.crc64.c 循环冗余校验

event:(事件)
1.ae.c 用于Redis的事件处理,包括句柄事件和超时事件。
2.ae_epoll.c 实现了epoll系统调用的接口
3.ae_evport.c 实现了evport系统调用的接口
4.ae_kqueue.c 实现了kqueuex系统调用的接口
5.ae_select.c 实现了select系统调用的接口

baseinfo:(基本信息)
1.asciilogo,c redis的logo显示
2.version.h定有Redis的版本号

compatible:(兼容)
1.fmacros.h 兼容Mac系统下的问题
2.solarisfixes.h 兼容solary下的问题

main:(主程序)
1.redis.c redis服务端程序
2.redis_cli.c redis客户端程序

net:(网络)
1.anet.c 作为Server/Client通信的基础封装
2.networking.c 网络协议传输方法定义相关的都放在这个文件里面了。

wrapper:(封装类)
1.bio.c background I/O的意思,开启后台线程用的
2.hyperloglog.c 一种日志类型的
3.intset.c  整数范围内的使用set,并包含相关set操作。
4.latency.c 延迟类
5.migrate.c 命令迁移类,包括命令的还原迁移等
6.notify.c 通知类
7.object.c  用于创建和释放redisObject对象
8.pqsort.c  排序算法类
9.pubsub.c 用于订阅模式的实现,有点类似于Client广播发送的方式。
10.rio.c redis定义的一个I/O类
11.slowlog.c 一种日志类型的,与hyperloglog.c类似
12.sort.c 排序算法类,与pqsort.c使用的场景不同
13.syncio.c 用于同步Socket和文件I/O操作。
14.zmalloc.c 关于Redis的内存分配的封装实现

others:(存放了一些我暂时还不是很清楚的类,所以没有解释了)
1.scripting.c
2.sentinel.c
2.setproctitle.c
3.valgrind.sh
4.redisassert.h

twemproxy explore

twemproxy,也叫nutcraker。是一个twtter开源的一个redis和memcache代理服务器。 redis作为一个高效的缓存服务器,非常具有应用价值。但是当使用比较多的时候,就希望可以通过某种方式 统一进行管理。避免每个应用每个客户端管理连接的松散性。同时在一定程度上变得可以控制。 搜索了不少的开源代理项目,知乎实现的python分片客户端。node的代理中间层,还有各种restfull的开源代理。

  • RedBrige

    • C + epoll实现的一个小的webserver
    • redis自身执行lua脚本的功能来执行redis命令
    • 访问时在url中带上lua脚本文件的名字,直接调用执行该lua脚本
    • 本质是通过http协议远程执行lua脚本
  • Webdis

    • libevent, http-parser...实现的小型web服务器
    • C 语言实现,通过unix-socket,TCP调用redis命令。
    • 访问方法: /cmd/key/arg0,arg1,... 实质是对redis命令的简单变换
  • redis-proxy

    • 使用node写的redis代理层。
    • 支持主从节点的失败处理(可以仔细研究)
    • 测试后发现性能为原生的1/3
  • twemproxy

    • 支持失败节点自动删除

      • 可以设置重新连接该节点的时间
      • 可以设置连接多少次之后删除该节点
      • 该方式适合作为cache存储
    • 支持设置HashTag

      • 通过HashTag可以自己设定将两个KEYhash到同一个实例上去。
    • 减少与redis的直接连接数

      • 保持与redis的长连接
      • 可设置代理与后台每个redis连接的数目
    • 自动分片到后端多个redis实例上

      • 多种hash算法(部分还没有研究明白)
      • 可以设置后端实例的权重
    • 避免单点问题

      • 可以平行部署多个代理层.client自动选择可用的一个
    • 支持redis pipelining request

    • 支持状态监控

      • 可设置状态监控ip和端口,访问ip和端口可以得到一个json格式的状态信息串
      • 可设置监控信息刷新间隔时间
    • 高吞吐量

      • 连接复用,内存复用。
      • 将多个连接请求,组成reids pipelining统一向redis请求。

另外可以修改redis的源代码,抽取出redis中的前半部分,作为一个中间代理层。最终都是通过linux下的epoll 事件机制提高并发效率,其中nutcraker本身也是使用epoll的事件机制。并且在性能测试上的表现非常出色。

配置部署建议: 编译时候打开logging模块。

redis部署知识: AOF;一种记录redis写操作的文件,用于恢复redis数据。

 

 

------------------------------------------------------------------------------------------------

 

posted @ 2022-12-02 14:32  hanease  阅读(234)  评论(0)    收藏  举报