redis缓存数据库详解(五):分布式锁的实现

作为一个稀有的Java妹子,所写的所有博客都只是当作自己的笔记,留下证据自己之前是有用心学习的~哈哈哈哈(如果有不对的地方,也请大家指出,不要悄悄咪咪的不告诉我)

1.锁

锁是为了控制高并发而产生的,多个线程访问同一个资源、用户多次提交等等都是高并发的情况,带来的影响就是数据不正确。比如提交订单,用户点击很多次,如果代码里不加控制,就会产生多条订单数据。
线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量
分布式锁:随着分布式系统的流行,应用被拆分为许多子系统,每个子系统都可能被部署在不同的服务器上,jvm就不止一个,synchronized就不适用于分布式应用了。所以就会考虑在应用服务器之外有一个存储服务器,存储锁信息,是不是就马上想到了redis。还有其他的分布式锁的解决方案,比如zookeeper等。

2.分布式锁的特征

1.锁应当有过期时间,避免加锁服务挂掉后没有释放锁造成死锁。
2.同一时刻只能有一个线程获取到锁。
3.线程只能解自己加的锁

3.redis实现分布式锁

之前的文章中有介绍redis的一个命令,setnx:set if not exists。当key不存在时保存,存在则什么也不做,利用这个命令就可以实现分布式锁。加锁和解锁都是用luna表达式,因为luna表达式可以原子性的执行命令,因为setnx是先set再设置过期时间,如果出现set完后,服务器宕机,过期时间还没设置,就产生了死锁。
加锁的luna语句为:执行成功返回1,表明加锁成功,否则失败,即redis中已存在相同的key了。

 "local key = KEYS[1]; local value = ARGV[1]; if redis.call('set', key, value,
 'NX' ,'PX', "+ millisecond + ") then return 1 else return 0 end";

解锁的luna语句:查询是否存在key,存在则删除,使用luna保持原子性

 "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) 
 else return 0 end";

完整的service:

@Component
public class RedisLockService {
    private static final Long RELEASE_SUCCESS = 1L;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     *  加锁
     * @param key key
     * @param value value
     * @param millisecond millisecond
     * @param tryCount tryCount
     * @return 状态
     */
    public boolean lock(String key, String value, int millisecond, int tryCount) {
        // 重试次数最大5次
        if (tryCount > 5) {
            tryCount = 5;
        }
        try {
            String script =
                    "local key = KEYS[1]; local value = ARGV[1]; if redis.call('set', key, value, 'NX' ,'PX', "
                            + millisecond + ") then return 1 else return 0 end";
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
            Long result = redisTemplate
                    .execute(redisScript, Collections.singletonList(key), Collections.singletonList(value));
            // 判断结果
            if (RELEASE_SUCCESS.equals(result)) {
                return true;
            }
        } catch (Exception ex) {
            // redis命中失败或连接有问题时,重试机制
            // 重试次数
            if (tryCount == 0) {
                return false;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                return false;
            }
            System.out.println(tryCount);
            // 重试机制
            return lock(key, value, millisecond, tryCount - 1);
        }
        return false;
    }
    /**
     * 释放分布式锁
     *
     * @param lockKey 锁
     * @param value   请求标识
     * @return 是否释放成功
     */
    public boolean releaseLock(String lockKey, String value) {

        String script =
                "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        Long execute =
                redisTemplate.execute(redisScript, Collections.singletonList(lockKey), Collections.singletonList(value));
        if (RELEASE_SUCCESS.equals(execute)) {
            return true;
        }
        return false;

    }
}

4.模拟一下并发情况

public void testRedisLockService() throws InterruptedException {
		//加锁,设置过期时间,这里只是为了演示,设置得很大
        boolean lock = redisLockService.lock("testLock", "testLock", 1000000000, 0);
        try{
            if(lock){
            	log.info("拿到锁,正在处理业务!");
            	//模拟业务处理,占用锁资源
                Thread.sleep(1000000000);
            }else {
                System.out.println("锁已被占用");
            }
        }catch (Exception e){
            log.info(e.getMessage());
        }finally {
        	//记得释放锁资源
        	if (lock) {
            	redisLockService.releaseLock("testLock","testLock");
            }
        }
        
    }

启动一个线程调用testRedisLockService方法:

在这里插入图片描述
再启动一个线程调用testRedisLockService方法:
在这里插入图片描述
redis锁起效。

5.注意

在生产环境中,锁的过期时间应该根据业务场景来决定,业务处理完后一定要释放锁资源。

6.缓存穿透、缓存雪崩、缓存击穿

虽然缓存可以提高应用的响应时间,但是还是会有一些潜在的问题,比如数据的一致性,视频的点赞量,缓存里的数据跟数据库的数据肯定不是时时都是一致的,数据库是定时的去同步缓存里的数据,所以是存在数据不一致的。
一般来讲,查询某个数据是先查缓存,如果有则直接返回,没有则查询数据库,数据库中存在则放入缓存,没有则什么也不做。

缓存穿透:指查询时,缓存里没有这个数据,数据库里也没有,那么每次查询都会读库,因为数据库不存在则不会放入缓存,如果攻击者就一直查询这个数据,就会一直访问数据库。比如根据ID查询用户身份,传的是用户ID,用户ID是正整数,如果传一个为-1的就会一直查询数据库,缓存就没有起作用,穿透缓存直接查数据库。

缓存雪崩:某一个时期,缓存里的值集体过期,那么这个时期就会出现数据库压力突然增加的情况,或者是缓存服务器挂机也会出现这种情况。在设置的时候,热点数据过期时间要错开设置过期时间。

缓存击穿:当一个热点key一直被访问,一直承受很大的访问压力,突然这个key不见了,一瞬间所有的压力就落到数据库上,像是在缓存上一个孔被击穿,给数据库造成压力

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值