Redis内存优化、集群、分布式锁、事务等总结
一、Redis内存优化
1、满足业务的情况下,减少键值长度。
2、尽量的将数据模型存放到散列表(ziplist)中,散列表使用的内存比较小。减少key-value的使用。比如一个用户信息,不要单独把用户的姓名、性别等信息存为一个key,而应该在整体存放在散列表中。
3、数据0-9999的时候,默认使用共享对象池,设置maxmemory并启动LUR相关淘汰策略,对象池无效,通过object refcount 查看其引用数会是1
二、Redis集群方案
分区:
分区是分割数据到多个Redis实例的处理过程,因此每个实例只保存key的一个子集。
分区类型:
1、范围分区:最简单的分区方式是按范围分区,就是映射一定范围的对象到特定的Redis实例。
2、哈希分区:hash函数(crc32 hash函数)将key转换为一个数字,这个整数和redis实例进行取模运算,确定数据存储在哪个redis中。
集群实现的几种方式:
1、客户端分片:由客户端决定key写入或者读取的节点(jedis)
2、基于代理的分片:客户端发送请求到一个代理,代理解析客户端的数据,将请求转发至正确的节点,然后将结果回复给客户端(Twemproxy、codis)。
3、路由查询:将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行。(Redis-cluster)
三、Redis分布式锁
分布式锁:
当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。
满足可靠性的四个条件:
1、互斥性。在任意时刻,只有一个客户端能持有锁。
2、不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
3、具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
4、解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
正确加锁
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean lock(Jedis jedis, String lockKey, String requestId, int expireTime) {
//如果key存在返回false;不存在,保存key并返回ok
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
return true;
}
return false;
}
错误加锁
public static boolean lock2(Jedis jedis, String lockKey, String requestId, int expireTime) {
Long result = jedis.setnx(lockKey, requestId);
if (result == 1) {
// 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
jedis.expire(lockKey, expireTime);
return true;
}
return false;
}
public static boolean lock3(Jedis jedis, String lockKey, String requestId, int expireTime) {
long expires = System.currentTimeMillis() + expireTime;
String expiresStr = String.valueOf(expires);
// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(lockKey, expiresStr) == 1) {
return true;
}
// 如果锁存在,获取锁的过期时间
String currentValueStr = jedis.get(lockKey);
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
String oldValueStr = jedis.getSet(lockKey, expiresStr);
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
return true;
}
}
// 其他情况,一律返回加锁失败
return false;
}
正确解锁
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean unlock(Jedis jedis, String lockKey, String requestId) {
//LUA代码
String script =
"if redis.call('get',KEYS[1]) == ARGV[1] then" +
" return redis.call('del',KEYS[1]) " +
"else" +
" return 0 " +
"end";
//eval原子操作
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
错误解锁
/**
*问题:所有客户端都可以解锁,即使不是他的错
*/
public static void unlock1(Jedis jedis, String lockKey, String requestId) {
jedis.del(lockKey);
}
/**
*A客户端在del之前,锁正好过期,B客户端加锁成功,A客户端就会把B客户端的锁删除.
*/
public static void unlock2(Jedis jedis, String lockKey, String requestId) {
// 判断加锁与解锁是不是同一个客户端
if (requestId.equals(jedis.get(lockKey))) {
// 若在此时,这把锁突然不是这个客户端的,则会误解锁
jedis.del(lockKey);
}
}
四、Redis 事务
本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
Redis不保证原子性:
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
Redis事务的三个阶段:
开始事务
命令入队
执行事务
Redis事务相关命令:
watch key1 key2 … : 监视一或多个key,如果在事务执行之前,被监视的key被其他命令改动,则事务被打断 ( 类似乐观锁 )
multi : 标记一个事务块的开始( queued )
exec : 执行所有事务块的命令 ( 一旦执行exec后,之前加的监控锁都会被取消掉 )
discard : 取消事务,放弃事务块中的所有命令
unwatch : 取消watch对所有key的监控
案例:
public static boolean redisTra(Jedis jedis,String balance,String debt,Long delAmount) {
// Watch 命令用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断
jedis.watch(balance);
Long amount = Long.parseLong(jedis.get(balance));
Long debts = Long.parseLong(jedis.get(debt));
if (amount < delAmount) {
//Redis Unwatch 命令用于取消 WATCH 命令对所有 key 的监视。
jedis.unwatch();
System.out.println("amount can't Less than delAmount !!!!");
return false;
}
//Redis 通过 MULTI 开始一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务
Transaction transaction = jedis.multi();
transaction.decrBy(balance, delAmount);
transaction.incrBy(debt, delAmount);
//在这里模拟网络延迟时,我们通过redis命令窗口手动去修改balance值。
List<Object> exec = transaction.exec();
//执行exec操作时发现balance值被修改,因此终止操作。
if (exec.size() <= 0) {
System.out.println("balance is upfated by other person,debt is fail!!!");
return false;
}
System.out.println("After updated balance== " + Long.parseLong(jedis.get(balance)));
System.out.println("After updated debt== " + Long.parseLong(jedis.get(debt)));
return true;
}
关注公众号免费领取学习资料~