Redis应用实战及注意事项

Redis的使用

引入pom

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

修改application.properties

使用

@Autowired
RedisTemplate redisTemplate;

Lettuce(默认客户端)

Redis的Java客户端

  • Redisson: 基于java对象和服务,底层基于Netty,不支持springboot集成,只能导入redisson包使用;
  • Lettuce: 支持线程安全同步、异步,支持Cluster、Sentinel,底层基于Netty,支持springboot集成;
  • Jedis: 基于redis指令进行封装,同步的,支持springboot集成;

Redis的请求通讯协议

set n v

*3\r\n$3\r\nset\r\n$1\r\nn\r\n$1\r\nv

*3表示有3个指令

$3表示指令长度为3:set

$1标识指令长度为1:n

$1标识指令长度为1:v

如何优化

  • 通信层面的优化(使用NIO)
  • 是否采用异步通信(多线程)
  • 针对key和value的序列化
  • 使用Java8特性
  • 解决方案

使用过程中的问题

数据库和redis的数据一致性问题(保证最终一致性)

先更新数据库,再删除redis缓存(相当于被动更新缓存)

这种方案,也会造成数据不一致问题,但是如果设置了key过期时间,那么能保证数据的最终一致性;
问题:
如果线程A更新数据库成功,正准备更新redis时,线程B在A更新redis之前获取了redis中的数据,那么这个数据是旧数据,如果删除redis失败也会造成数据不一致问题;
解决方案:
1、mysql和redis设置事务,发生异常时回滚数据;
2、redis设置重试机制,删除redis失败后进入重入模式,保证数据的最终一致性;

先删除redis缓存,再更新数据库

这种方案,也会造成数据不一致问题,但是接近最优。
问题:
如果线程A删除了redis,正准备更新数据库;线程B查询redis没有数据,查询数据库获得旧数据,并且把旧数据写入redis;之后线程A才更新数据库成功,也会出现数据不一致问题;
解决方案:
延迟双删 -- 线程A在删除redis以及更新数据库后,睡眠一段时间,然后再次删除redis中的数据,这个睡眠时间得大于一次查询的时间,也能保证数据的最终一致性;

缓存击穿

假设当10万个请求同时访问某个数据,此时刚好redis key过期,那么这10万个请求会同时并发访问数据库访问数据,这就是访问击穿,如下代码片段:

public class ItemService {
    @Autowired
    private RedisTemplate redisTemplate;

    public List<Item> select(String key) {
        List<Item> items = (List<Item>) redisTemplate.opsForValue().get(key);
        if (items == null) { // 如果redis中为空,则查询数据库
            items = itemManager.select(key);
            if (null != items) {
                redisTemplate.opsForValue().set(key, items, 5, TimeUnit.MINUTES);
            }
        }
    }
}

解决以上问题,对代码进行优化:通过DCL双重检查锁

public class ItemService {
    @Autowired
    private RedisTemplate redisTemplate;

    public List<Item> select(String key) {
        List<Item> items = (List<Item>) redisTemplate.opsForValue().get(key);
        if (items == null) { // 如果redis中为空,则查询数据库
            // 通过DCL双重检查锁解决缓存击穿问题
            synchronized(ItemService.class) {
                items = (List<Item>) redisTemplate.opsForValue().get(key);
                if (items == null) { // 再次判断redis中是否有值
                    items = itemManager.select(key);
                    if (null != items) {
                        redisTemplate.opsForValue().set(key, items, 5, TimeUnit.MINUTES);
                    }
                }
            }
        }
    }
}

缓存雪崩

出现的原因

  • 大量热点数据同时失效
  • 或者redis出现故障
  • 总之由于redis的key过期导致

解决方案:

  • key过期时间错开,或者设置随机过期时间
  • 对于热点数据,没必要设置过期时间
  • redis集群,灾备方案,保证高可用,避免redis故障

缓存穿透

redis和mysql都不存在这样的数据的情况,有可能是恶意攻击,需要通过布隆过滤器解决缓存穿透

布隆过滤器BloomFilter

引用pom,创建BloomFilter,类似于HashTable,将所有需要查询的数据全部存入BloomFilter中,每次查询redis缓存之前先查询key是否存在,如果不存在则直接返回,避免去redis缓存中查询,查不到还需要查询数据库从而导致缓存穿透

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>19.0</version>
</dependency>

所有数据通过key存入BloomFilter中,每个key通过多个hash函数进行计算得到一个值存入;每次查询之前先通过BloomFilter进行检查,通过多个hash函数对key进行计算,如果多个hash结果和存入时的结果完全相同,则代表可能存在,如果有一个hash计算结果不相同,则代表查询的数据肯定不存在,从而避免查询redis缓存也避免查询数据库导致缓存穿透

BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF-8), 10000000, 0.03);

过期key的删除

被动删除

通过get(name)从过期key存储表中拿到name的过期时间,发现过期了,返回失效

主动删除

随机抽取20个key,删除20个key中已经过期的key,如果发现20个key中有25%已经过期,那么再次执行随机抽取

Redis实现分布式锁

分布式锁:跨进程实现共享资源的互斥,分布式锁必须是独立于系统业务服务之外的的第三方提供的,来存储锁

实现分布式锁有很多方式:redis、zookeeper、mysql、etcd

redis为什么能实现分布式锁?

1、命令执行是单线程的
2、exists key可以判断key是否存在
3、根据判断结果,如果不存在,调用set key value命令设置值,并且调用expire key seconds命令设置过期时间避免死锁
4、可以发现,这里用到了3个命令,那么3个命令就无法保证原子性和线程安全了,所以需要LUA脚本保证原子性
5、但是redis实现的锁是不可重入的,针对这个问题 Redisson通过hash数据类型hset方法,把标记存为key,重入次数(加锁次数)存为value

为什么不用事务实现?

multi 开启事务, exec 退出,discard回滚,但是下一个指令无法拿到上一个指令的执行结果,所以无法使用事务来做分布式锁

1、redis使用multi命令开启事务,但是每次执行一个命令,只是把命令放在了queue中,并没有真正执行,只有最后提交的时候才一起执行命令;
2、没有办法在第二个命令执行之前拿到第一个命令的执行结果,所以无法实现分布式锁;

锁的类型

  • 排他锁:不允许多个程序(线程、进程)同时访问某个共享资源
  • 共享锁:用于不更新或不更改操作,只读操作

Redis分布式锁原理

  • 通过Lua脚本执行redis命令实现通信
  • 如果指令还没有执行完成,通过自动续约机制
  • 通过看门狗lockWatchdogTimeout定期检查时间

redis之所以可以实现分布式锁,是因为:(1)redis是一个独立部署的中间件,可以对资源进行存储;(2)多个客户端之间不知道是否可以对一个共享资源进行访问,这个时候需要把共享资源的状态存放在redis中,客户端访问共享资源之前先访问redis获取共享资源状态,查看是否可以访问,如果可以访问再访问共享资源,redis起到分布式协调作用;(3)redis的setnx命令可以实现共享互斥变量;(4)redis的setnx命令满足原子性;

public class RedisLockDemo {
    private static RedissonClient redissonClient;
    static {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.2.101:6379");
        redissonClient = Redisson.create(config);
    }

    public static void main(String[] args) throws InterruptedException {
        RLock rLock = redissonClient.getLock("supportUpdate");
        if (rLock.tryLock()) { //返回true,表示获得锁成功
            System.out.println("获取锁成功");
        } else {
            System.out.println("获取锁失败");
        }
        Thread.sleep(2000);
        rLock.unlock(); //释放锁
    }
}

抢占锁原理

public boolean tryLock() {
    return (Boolean)this.get(this.tryLockAsync());
}

public RFuture<Boolean> tryLockAsync() {
    return this.tryLockAsync(Thread.currentThread().getId());
}

public RFuture<Boolean> tryLockAsync(long threadId) {
    RPromise<Boolean> result = new RedissonPromise();
    RFuture<Long> longRFuture = this.tryAcquireAsync(-1L, (TimeUnit)null, threadId);
    longRFuture.onComplete((res, e) -> {
        if (e != null) {
            result.tryFailure(e);
        }

        result.trySuccess(res == null);
    });
    return result;
}

// 返回RFuture,表示这里是异步调用返回结果
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
    if (leaseTime != -1L) { //如果设置了锁超时时间
        return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    } else { //如果没有设置锁超时时间,默认30S
        RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e == null) {
                if (ttlRemaining == null) {
                    // 续约/续期机制
                    this.scheduleExpirationRenewal(threadId);
                }

            }
        });
        return ttlRemainingFuture;
    }
}

// 第四个参数script string是Lua脚本,通过Lua脚本执行Redis命令
// Lua脚本的执行是原子的,所以这一堆script是原子指令
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    this.internalLockLeaseTime = unit.toMillis(leaseTime);
    return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getRawName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
}

protected final <V> V get(RFuture<V> future) {
    return this.commandExecutor.get(future);
}

// get方法是阻塞等待获取
public <V> V get(RFuture<V> future) {
    if (Thread.currentThread().getName().startsWith("redisson-netty")) {
        throw new IllegalStateException("Sync methods can't be invoked from async/rx/reactive listeners");
    } else {
        try {
            future.await();
        } catch (InterruptedException var3) {
            Thread.currentThread().interrupt();
            throw new RedisException(var3);
        }

        if (future.isSuccess()) {
            return future.getNow();
        } else {
            throw this.convertException(future);
        }
    }
}

释放锁原理

public void unlock() {
    try {
        this.get(this.unlockAsync(Thread.currentThread().getId()));
    } catch (RedisException var2) {
        if (var2.getCause() instanceof IllegalMonitorStateException) {
            throw (IllegalMonitorStateException)var2.getCause();
        } else {
            throw var2;
        }
    }
}

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[1]); return 0; else redis.call('del', KEYS[1]); return 1; end; return nil;", Collections.singletonList(this.getRawName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
}

Lua脚本

eval “if KEYS[1]=='1' then return ARGV[1] end return ARGV[2]”1 'key'  'arg1' 'arg2'

类似javascript的脚本语言

可以自定义redis的相关操作指令

原子性,一个Lua脚本是一个原子性指令

复用性:可以在Redis客户端定义一个Lua脚本,并复用调用

public class LuaDemo {
    private static RedissonClient redissonClient;
    static {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.2.101:6379");
        redissonClient = Redisson.create(config);
    }

    private static final String LUA_SCRIPT = "return redis.call('incr', KEYS[1])";

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        RScript rScript = redissonClient.getScript();
        List<String> keys = Arrays.asList("LIMIT:" + 1);
        RFuture<String> rFuture = rScript.evalAsync(RScript.Mode.READ_WRITE, LUA_SCRIPT, RScript.ReturnType.INTEGER, keys);
        System.out.println(rFuture.get());
    }
}

在Lua脚本中调用redis命令

redis.call('set', 'key', 'value');

local value = redis.call('get', 'key');

Lua的原子性

如果有一个程序比如redis正在通过Lua脚本执行某个指令,其他应用程序访问redis时会提示处于繁忙状态,这样做的目的是避免其他操作干扰数据的准确性。通过script kill或者shutdown nosave可以终止这个指令。可以通过lua-time-limit参数设置lua脚本执行的时间。

evalsha指令

通过script load "return redis.call('get', 'name')"可以将后面的lua脚本生成一个hash值

通过evalsha指令可以直接通过hash值执行对应的lua脚本

时间轮机制

kafka、zookeeper、dubbo、netty都用到了时间轮机制

public class HashedWheelTimerDemo {
    public static void main(String[] args) {
        HashedWheelTimer hashedWheelTimer = new HashedWheelTimer(new DefaultThreadFactory(
                "timer-pool"), 100, TimeUnit.MILLISECONDS, 8, false);
        hashedWheelTimer.newTimeout(task -> {
            System.out.println("需要执行的任务,延迟1秒钟执行任务");
        }, 1, TimeUnit.SECONDS);
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值