C++操作redis(实现连接池、分布式锁)



Redis连接池

封装hiredis的一些基本操作,redishelper类提供包含连接,放回,存取键,push,pop,执行redis语句和执行lua脚本的函数,连接池是类模板,传入相应helper类即可实现多种连接池,后续实现mysql连接池。

编译项目

  1. 安装hiredis,解压文件中hiredis.tar(后续对源码进行了改动,文件中是改后的)
tar -xvf hiredis.tar
cd hiredis/
make
make install	# 权限不足加上sudo
  1. 删除之前的CMakeCache.txt
cd ConnPool/
rm CMakeCache.txt
  1. 重新cmake和make
cmake .
make -j4
  1. 后续引入头文件SimConnPool.h即可使用,简单应用见tests文件

整体架构

  1. RedisHelper:redis相关操作,创建一个连接时构造函数不做任何事,在connection函数时才真正建立一条连接,每条连接都包含一个指向连接池的指针,为了实现放回操作,基础实现依赖hiredis库中的redisCommand函数
  2. ConnPool :连接池模板,初始化时构造函数不做任何操作,在连接池connection时才执行init函数完成每一个连接的初始化,并且依次调用每条连接的connection函数
  3. Singleton:单例模板,包装ConnPool类,变成单例对象

使用

static SimpleConnPool::redisPool* pool = SimpleConnPool::redisConnPoolMgr::GetInstance();   // 获取一个连接池单例对象,默认5个连接
pool->connection("127.0.0.1", 6000);      // 连接池内连接上redis,入参:ip、端口号、密码默认为空
SimpleConnPool::redisPool::connPtr conn = pool->get();      // 取出一条连接
// TODO
conn->connBack();     // 放回连接

分布式锁

当多线程并发访问数据库中某个资源时,可能同一时间返回都取到了旧值,并且对旧值进行变更,这样会造成逻辑上的混淆。比如在电商场景,库存这个变量,当多个用户进行抢购的时候,首先判断库存然后库存减一,若不加锁,库存为1的时候,可能多个用户获取库存判断时都拿到了1,最后都进行库存减一,可能就出现负数了。

在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行

高可用的获取锁与释放锁

具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)

具备锁失效机制,即自动解锁,防止死锁

加锁:

实际是,对一个键进行赋值,借助set key val NX PX timeout 语句实现,NX表示若键存在返回nil,不存在则赋值,当多线程运行到lock时,只有第一个执行语句的线程可以跳出,继续执行,其余线程都需要阻塞在lock内部。

设置超时时间为了保证不会因为加锁线程挂掉而导致死锁的情况。

/* 
        加锁逻辑:
        在不存在键时设置键 并同时设置超时时间  保证原子性
        set key value NX PX timeout
        默认10秒
        返回该线程拿到锁生成的值,后续解锁比较,不能因为挂机或网络原因解锁别的线程锁
        分布式锁的键为:redis.lock      值为:当前时间戳 nsec
    */
std::string lock(uint64_t timeout = 30000) {
    while(1) {
        struct timespec time = {0, 0};
        clock_gettime(CLOCK_REALTIME, &time);
        std::string uuid = std::to_string(time.tv_nsec);
        std::string cmd = "set redis.lock " + uuid + " NX PX " + std::to_string(timeout);
        std::string flag;
        RedisState sta;
        {
            MutexType::Lock lock(m_mutex);
            sta = execute(cmd, &flag);
        }
        if(sta == RedisState::M_REDIS_OK) {
            std::cout << "get redis.lock!" << std::endl;
            return uuid;
        }
    }
}

解锁:

需要取值、判断、删除三个操作,因此使用lua脚本来保证原子性,判断是为了保证不会删除其余线程所创建的锁。

/*
        解锁逻辑:
        获取锁,判断锁,删除锁
        为了保证原子性,使用lua脚本来完成三个操作
        redis使用eval "xxxx" keys数量 key1 key2 num1 num2 
    */
void unlock(const std::string& uuid) {
    std::string cmd = "eval \"if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end\" 1 redis.lock " + uuid;
    int flag;
    RedisState sta = executeLua(cmd.c_str(), &flag);
    if(sta == RedisState::M_REDIS_OK && flag > 0) {
        std::cout << "获取到锁并解锁" << std::endl;
    }
    else {
        std::cout << "未获取到锁 errstr: " << m_error_msg << std::endl;
    }
    return ;
}

总结

基于hiredis,只需要引入头文件即可使用,简单方便,复杂语句使用execute函数直接执行,但是需要根据redis客户端返回值,传入该函数第二个参数(set,get返回ok就传string,del返回1或0就传int)

为了可以执行lua脚本,对hiredis中源码进行了改动,原先是以空格来分割命令的,但是lua脚本需要return时肯定包含空格,需要更改分割规则,增加以双引号分割,详见(hiredis中lua脚本调用_hiredis lua_suhiymof的博客-CSDN博客

std::string cmd = "EVAL \"redis.call('SET', KEYS[1], ARGV[1]) local result = redis.call('get',KEYS[1]) return result\" 1 foo1 bar1";
std::string flag;       // set get为string类型,del为int类型,看redis客户端返回的类型设置
conn->executeLua(cmd.c_str(), &flag);
std::cout << "lua执行结果:" << flag << std::endl;

帮助大家熟悉C++和redis的基本操作,使用lua脚本实现了简单的分布式锁,可以多线程的访问数据库中某个变量

还缺少健康检测,自动扩缩容等实现,大家有需要可以继续开发
整体项目地址 供大家学习使用,如果有帮助请多点点star

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
以下是使用Spring Redis实现Redis分布式锁的例子: 首先,在pom.xml文件中添加以下依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> ``` 然后,创建一个Redis分布式锁的接口: ```java public interface RedisLock { /** * 获取锁 * * @param key 锁的key * @param expireTime 锁的过期时间 * @return 是否获取成功 */ boolean lock(String key, long expireTime); /** * 释放锁 * * @param key 锁的key */ void unlock(String key); } ``` 接着,实现Redis分布式锁的接口: ```java @Component public class RedisLockImpl implements RedisLock { private final RedisTemplate<String, String> redisTemplate; private final ThreadLocal<Map<String, Integer>> lockers = new ThreadLocal<>(); @Autowired public RedisLockImpl(RedisTemplate<String, String> redisTemplate) { this.redisTemplate = redisTemplate; } @Override public boolean lock(String key, long expireTime) { Map<String, Integer> localLockers = getLocalLockers(); Integer count = localLockers.get(key); if (count != null) { localLockers.put(key, count + 1); return true; } Boolean result = redisTemplate.opsForValue().setIfAbsent(key, ""); if (!result) { return false; } redisTemplate.expire(key, expireTime, TimeUnit.MILLISECONDS); localLockers.put(key, 1); return true; } @Override public void unlock(String key) { Map<String, Integer> localLockers = getLocalLockers(); Integer count = localLockers.get(key); if (count == null) { return; } if (count > 1) { localLockers.put(key, count - 1); } else { localLockers.remove(key); redisTemplate.delete(key); } } private Map<String, Integer> getLocalLockers() { Map<String, Integer> localLockers = lockers.get(); if (localLockers != null) { return localLockers; } lockers.set(new HashMap<>()); return lockers.get(); } } ``` 最后,在需要使用分布式锁的地方,注入RedisLock接口,使用lock()方法获取锁,使用unlock()方法释放锁: ```java @Service public class UserService { private final RedisLock redisLock; @Autowired public UserService(RedisLock redisLock) { this.redisLock = redisLock; } public void updateUser(String userId) { String lockKey = "user_" + userId; boolean locked = redisLock.lock(lockKey, 5000); if (!locked) { throw new RuntimeException("获取锁失败"); } try { // 更新用户信息 } finally { redisLock.unlock(lockKey); } } } ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jiaoooooo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值