拿来就能用:Redisson 分布式读写锁组件,搭配WebFilter释放你的双手

哒哒队长

场景

前情提要:https://segmentfault.com/a/11...

为了解决max(id) 引发的事务可见性问题,最初是通过间隙锁完成,但是间隙锁锁的是表,对并发影响太大,因此考虑使用粒度更精细的分布式读写锁。

读写锁

读写锁原理,读读并发,读写、写写不行

根据我们业务规则,可以并发写,但是写的时候不能读,读的时候不能写

因此写操作的时候获取的是读锁,读操作的时候获取的是写锁

引入Redisson

pom.xml

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.12.0</version>
</dependency>

装配

@Bean(destroyMethod = "shutdown")
public RedissonClient getRedissonClient() {

    //创建配置
    Config config = new Config();

    //指定编码,默认编码为org.redisson.codec.JsonJacksonCodec
    //之前使用的spring-data-redis,用的客户端jedis,编码为org.springframework.data.redis.serializer.StringRedisSerializer
    //改用redisson后为了之间数据能兼容,这里修改编码为org.redisson.client.codec.StringCodec
    config.setCodec(new org.redisson.client.codec.StringCodec());

    //指定使用单节点部署方式
    config.useSingleServer()
            .setAddress(String.format("redis://%s:%s", redisConfiguration.getHost(), redisConfiguration.getPort()))
            .setPassword(redisConfiguration.getPassword())
            .setConnectionPoolSize(redisConfiguration.getPool().getMaxActive())
            .setConnectionMinimumIdleSize(redisConfiguration.getPool().getMinIdle())
            .setIdleConnectionTimeout(redisConfiguration.getPool().getIdleTimeout())
            .setConnectTimeout(redisConfiguration.getConnectTimeout())
            .setTimeout(redisConfiguration.getReadTimeout());


    return Redisson.create(config);
}

Lock 服务

代码逻辑比较简单,不明白评论区留言,及时回复。

@Service
@Slf4j
public class LockService {


    @Resource
    private RedissonClient redissonClient;

    @Value("${LOCK_RETRY_TIME:3}")
    private Integer lockReTryTime;

    /**
     * 获取读锁用于存数据
     *
     * @param computeUuids
     */
    public void getReadLock(List<String> computeUuids) {
        getLock(computeUuids, true);
    }

    /**
     * 释放读锁
     *
     * @param computeUuids
     */
    public void unReadLock(List<String> computeUuids) {
        unLock(computeUuids, true);
    }

    /**
     * 获取读锁用于存数据
     *
     * @param computeUuids
     */
    public void getWriteLock(List<String> computeUuids) {
        getLock(computeUuids, false);
    }

    /**
     * 释放读锁
     *
     * @param computeUuids
     */
    public void unWriteLock(List<String> computeUuids) {
        unLock(computeUuids, false);
    }

    /**
     * 获取读锁用于存数据
     *
     * @param computeUuids
     */
    public void getLock(List<String> computeUuids, boolean read) {
        log.info("批量获取{}锁 computeUuid:{}", read ? "读" : "写", JSON.toJSONString(computeUuids));
        List<RLock> lockedList = new ArrayList<>();
        try {
            for (String computeUuid : computeUuids) {
                String lockName = RedisCacheKey.DEVICE_AUTHORITY_MODIFY_SYNC_LOCK.build(computeUuid);
                RLock rLock;
                if (read) {
                    rLock = redissonClient.getReadWriteLock(lockName).readLock();
                } else {
                    rLock = redissonClient.getReadWriteLock(lockName).writeLock();
                }

                boolean isLocked = false;
                int count = 0;
                log.info("lockName:{} 开始获取{}锁", lockName, read ? "读" : "写");
                // 尝试3次获取,获取失败全部释放
                do {
                    try {
                        // 尝试获取锁(等待时间,过期时间,时间单位)
                        isLocked = rLock.tryLock(RedisCacheKey.DEVICE_AUTHORITY_MODIFY_SYNC_LOCK.getWaitTime(), RedisCacheKey.DEVICE_AUTHORITY_MODIFY_SYNC_LOCK.getExpireSecs(), TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                        log.info("获取锁失败:{}", rLock.getName());
                        count++;
                    }
                } while (!isLocked && count < lockReTryTime);

                if (count == 3) {
                    log.info("================={}锁申请失败{},开始释放============================", read ? "读" : "写", lockName);
                    break;
                } else {
                    lockedList.add(rLock);
                    log.info("================={}锁申请成功{}============================", read ? "读" : "写", lockName);
                }
            }
        } finally {
            if (computeUuids.size() != lockedList.size()) {
                log.info("=================存在{}锁申请失败,开始释放============================", read ? "读" : "写");
                lockedList.forEach(lock -> {
                    lock.unlock();
                    log.info("================={}锁释放成功{}============================", read ? "读" : "写", lock.getName());
                });
            }
        }


    }

    /**
     * @param computeUuids
     */
    public void unLock(List<String> computeUuids, boolean read) {
        log.info("批量释放{}锁 computeUuid:{}", read ? "读" : "写", JSON.toJSONString(computeUuids));
        for (String computeUuid : computeUuids) {
            String lockName = RedisCacheKey.DEVICE_AUTHORITY_MODIFY_SYNC_LOCK.build(computeUuid);
            log.info("lockName:{} 开始释放{}锁", lockName, read ? "读" : "写");
            RLock rLock;
            try {
                if (read) {
                    rLock = redissonClient.getReadWriteLock(lockName).readLock();
                } else {
                    rLock = redissonClient.getReadWriteLock(lockName).writeLock();
                }
                rLock.unlock();
            } catch (Exception e) {
                log.info("释放时获取锁失败,继续获取下一把:{}", lockName);
            }
            log.info("lockName:{} 释放{}锁成功", lockName, read ? "读" : "写");
        }
        log.info("================={}锁释放成功============================", read ? "读" : "写");
    }
}

Filter 优化放锁

插入数据可以通过统一入口,在固定位置执行,但是放锁需要在各个业务出口处理

由于出口众多,一个一个写无疑是最容易想到的,但是也是最...emmm....憨憨的

于是想到了 webFilter ,每一个请求的线程都需要经过Filter,于是就想到了利用 Filter + ThreadLocal处理

全局 ThreadLocal

public class ComputeLockThreadLocal {
    private static final ThreadLocal<List<String>> threadLocal = new NamedThreadLocal<>("computeLockList");

    public static void set(List<String> computeLockList) {
        threadLocal.set(computeLockList);
    }

    public static List<String> get() {
        return threadLocal.get();
    }

    public static void remove() {
        threadLocal.remove();
    }

}

定义 WebFilter

@Slf4j
public class PassWebFilter implements Filter {


    @Resource
    private LockService lockService;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        try {
            filterChain.doFilter(servletRequest, servletResponse);
        } finally {
            List<String> computeLockList = ComputeLockThreadLocal.get();
            log.debug("*********************获取的读写锁 computeLockList:{}", JSON.toJSONString(computeLockList));
            if (CollectionUtils.isNotEmpty(computeLockList)) {
                log.info("*********************释放的读写锁 computeLockList:{}", JSON.toJSONString(computeLockList));

                lockService.unReadLock(computeLockList);
                ComputeLockThreadLocal.remove();
            }
        }
    }

    @Override
    public void destroy() {
    }
}

配置 WebFilter

@Configuration
public class WebConfiguration {

    @Value("${web.filter.urlPatterns:/*}")
    private String webPrefix;

    @Bean("passWebFilter")
    public Filter webFilter() {
        return new PassWebFilter();
    }

    @Bean
    public FilterRegistrationBean webFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new DelegatingFilterProxy("passWebFilter"));
        registration.setName("passWebFilter");
        registration.addUrlPatterns(webPrefix);// 配置拦截的url,全部拦截
        registration.setOrder(5);
        return registration;
    }

    @Bean
    public RequestContextListener requestContextListener() {
        return new RequestContextListener();
    }
}

这样就可以通过加锁时放入锁key,然后在会话结束时通过 WebFilter 时统一判断是否要放锁,减少修改量

阅读 1.7k

7 声望
3 粉丝
0 条评论
7 声望
3 粉丝
宣传栏