package com.starcharge.base.redis; import org.apache.commons.lang3.RandomUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.core.ListOperations; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.types.Expiration; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * Redis工具包 * 依赖Spring自带的RedisTemplate,可根据配置更换Redis客户端Lettuce/Jedis * * @author kevin */ @Component public class RedisClient { @Resource private RedisTemplate<String, Object> redisTemplate; private static final Logger logger = LoggerFactory.getLogger(RedisClient.class); private static final String REDIS_LOCK_PREFIX = "tmp:lock:"; private static final String LOCK_LUA = "if redis.call('get',KEYS[1]) then return 0 else " + "if redis.call('set',KEYS[1],ARGV[1]) then if redis.call('expire',KEYS[1],ARGV[2]) " + "then return 1 else return 0 end else return 0 end end"; private static final String RELEASE_LOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; @SuppressWarnings("unchecked") public <T> RedisTemplate<String, T> getRedisTemplate(Class<T> clz) { return (RedisTemplate<String, T>) redisTemplate; } /*********************************key-value*************************************/ public void put(final String key, final Object value) { put(key, value, null); } /** * 移除key后重新放值 * * @param key * @param value */ public void removePut(final String key, final Object value) { remove(key); put(key, value, null); } /** * 塞入string * * @param key * @param value * @param expire */ public void put(final String key, final Object value, final Integer expire) { redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS); } public <T> T get(final String key, Class<T> clz) { return get(key, clz, null); } public String get(final String key) { return get(key, String.class, null); } @SuppressWarnings("unchecked") public <T> T get(final String key, Class<T> clz, final Integer expire) { if (expire != null) { redisTemplate.expire(key, expire, TimeUnit.SECONDS); } return (T) redisTemplate.opsForValue().get(key); } @SuppressWarnings("unchecked") public <T> List<T> multiGet(final List<String> keys, Class<T> clz) { return (List<T>)redisTemplate.opsForValue().multiGet(keys); } /*********************************key-hash*************************************/ public void putHash(final String key, final Object hashKey, final Object value) { putHash(key, hashKey, value, null); } public void putHash(final String key, final Object hashKey, final Object value, final Integer expire) { redisTemplate.opsForHash().put(key, hashKey, value); if (expire != null) { redisTemplate.expire(key, expire, TimeUnit.SECONDS); } } public void putHash(final String key, Map<String, Object> mapValue) { putHash(key, mapValue, null); } public void putHash(final String key, Map<String, Object> mapValue, final Integer expire) { redisTemplate.opsForHash().putAll(key, mapValue); if (expire != null) { redisTemplate.expire(key, expire, TimeUnit.SECONDS); } } public void deleteHashKey(final String key, Object... hashKeys) { redisTemplate.opsForHash().delete(key, hashKeys); } /*********************************key-set (无序,不重复)*************************************/ public void putSet(final String key, final Object value) { if(value instanceof Set) { putSet(key, ((Set)value).toArray()); }else { putSet(key, new Object[] {value}); } } public void putSet(final String key, final Object... values) { redisTemplate.opsForSet().add(key, values); } @SuppressWarnings("unchecked") public <T> T getSet(final String key, Class<T> clz) { return (T)redisTemplate.opsForSet().pop(key); } @SuppressWarnings("unchecked") public <T> List<T> members(final String key, Class<T> clz, final Integer expire) { if (expire != null) { redisTemplate.expire(key, expire, TimeUnit.SECONDS); } return (List<T>)redisTemplate.opsForSet().members(key); } /*********************************key-list (有序,可重复)*************************************/ public void rightPush(final String key, final Object value) { redisTemplate.opsForList().rightPush(key, value); } public void rightPushAll(final String key, final List values) { redisTemplate.opsForList().rightPushAll(key, values); } public void rightPushAll(final String key, final Object... values) { redisTemplate.opsForList().rightPushAll(key, values); } public void leftPush(final String key, final Object value) { redisTemplate.opsForList().leftPush(key, value); } @SuppressWarnings("unchecked") public <T> T leftPop(final String key, Class<T> clz) { return (T)redisTemplate.opsForList().leftPop(key); } @SuppressWarnings("unchecked") public <T> T rightPop(final String key, Class<T> clz) { return (T)redisTemplate.opsForList().rightPop(key); } /** * 得到key数据结构未list的数据,带分页支持 */ public <T> List<T> findList(String key, Class<T> clz, Integer startRow, Long endRow) { ListOperations<String, T> operations = getRedisTemplate(clz).opsForList(); return operations.range(key, startRow - 1, endRow - 1); } public Long getListTotal(String key) { return getRedisTemplate(Long.class).opsForList().size(key); } public void expire(String key, long expire) { redisTemplate.expire(key, expire, TimeUnit.SECONDS); } /** * 删除某个key * * @param key */ public void remove(String key) { redisTemplate.delete(key); } public void del(String key) { remove(key); } /** * 模糊删除key * * @param key */ public void clear(String key) { Set<String> keys = redisTemplate.keys(key); if (!keys.isEmpty()) { redisTemplate.delete(keys); } } public Long getAtomicLong(String key) { return getAtomicLong(key, 1); } public Long getAtomicLong(String key, long delta) { return getRedisTemplate(Long.class).boundValueOps(key).increment(delta); } public Long getAtomicLong(String key, String hashKey) { return getAtomicLong(key, hashKey, 1); } public Long getAtomicLong(String key, String hashKey, long delta) { return getRedisTemplate(Long.class).boundHashOps(key).increment(hashKey, delta); } /** * 获取redis锁,默认重试1s * @param lockId * @param requestId * @param expire * @return */ public Boolean getLockRetry(String lockId, String requestId, long expire) { return getLockRetry(lockId, requestId, expire, 1000); } /** * 获取redis锁,获取失败则等待200~300毫秒进行重试,可配置超时时间 * @param lockId * @param requestId * @param expire * @param retryTimeout 重试超时时间,根据资源的消耗进行配置 * @return */ public Boolean getLockRetry(String lockId, String requestId, long expire, long retryTimeout) { return getLockRetry(lockId, requestId, expire, retryTimeout, 200+ RandomUtils.nextInt(0, 100)); } /** * 重试获取锁 * @param lockId * @param requestId * @param expire * @param retryTimeout 重试时长 * @param retryInterval 重试时间间隔 * @return */ public Boolean getLockRetry(String lockId, String requestId, long expire, long retryTimeout, long retryInterval) { //请求时间 long requestTime=System.currentTimeMillis(); while(System.currentTimeMillis()-requestTime < retryTimeout){ if(lock(lockId, requestId, expire)) { return Boolean.TRUE; } try { TimeUnit.MILLISECONDS.sleep(retryInterval); } catch (InterruptedException e) { e.printStackTrace(); logger.error("线程等待出现异常"+e.getMessage(), e); } } return Boolean.FALSE; } /** * 利用redis进行锁 * boolean result = redisTemplate.boundValueOps(key).setIfAbsent(0); * if (result) { * redisTemplate.expire(key, expire, TimeUnit.SECONDS); * } * 上面的方式不具有原子性,系统异常时会导致加锁了但是未设置失效时间,导致死锁 * * 在没有锁值传入的情况下,最好返回定义的锁值,便于后期释放锁 * 推荐{@link RedisClient#lock(String, String, long)} 或者 {@link RedisClient#lock(String, long)} */ public boolean getLock(String lockId, long expire) { return redisTemplate.boundValueOps(getLockKey(lockId)).setIfAbsent(0, expire, TimeUnit.SECONDS); } /** * 当前方法仅适用自动释放锁的情况 * @param lockId * @param expire * @return */ public boolean getLockAutoRelease(String lockId, long expire) { return StringUtils.isNotBlank(lock(lockId, expire)); } public String getLockKey(String lockId) { return REDIS_LOCK_PREFIX + lockId; } /** * 获取锁,返回锁值 * @param lockId * @param expire * @return */ public String lock(String lockId, final long expire) { String value=UUID.randomUUID().toString(); if(lock(lockId, value, expire)){ return value; } return null; } /** * 获取锁 * @param lockId 锁的key * @param requestId UUID,用于释放当前线程的锁 * @param expire * @return */ public Boolean lock(String lockId, final String requestId, final long expire) { final String key = getLockKey(lockId); return redisTemplate.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { try { return connection.set(key.getBytes("UTF-8"), requestId.getBytes("UTF-8"), Expiration.seconds(expire), RedisStringCommands.SetOption.SET_IF_ABSENT); }catch (Exception e) { logger.error("获取redis锁出现异常", e); } return false; } }); } /** * 如果当前线程的锁已经释放/失效,拿到的锁值可能不是当前线程存储的值 * @param lockId * @return */ public String getLockValue(String lockId) { final String key = getLockKey(lockId); return redisTemplate.execute(new RedisCallback<String>() { @Override public String doInRedis(RedisConnection connection) throws DataAccessException { try { return new String(connection.get(key.getBytes("UTF-8")),"UTF-8"); }catch (Exception e) { logger.error("获取redis锁requestId出现异常", e); } return null; } }); } /** * 释放锁,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除 * 需要判断加锁的requestId与当前锁的requestId是否一致,一致则释放 * @param lockId * @param requestId 获取锁成功后调用getLockRequestId获取requestId,释放锁时传入requestId * @return */ public boolean releaseLock(String lockId, String requestId) { if (StringUtils.isBlank(lockId) || StringUtils.isBlank(requestId)) { return false; } final String key = getLockKey(lockId); try { // 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁 // spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本 RedisCallback<Boolean> callback = (connection) -> { try { return connection.eval(RELEASE_LOCK_LUA.getBytes("UTF-8"), ReturnType.BOOLEAN, 1, key.getBytes("UTF-8"), requestId.getBytes("UTF-8")); }catch (Exception e) { logger.error("执行释放锁出现异常", e); return Boolean.FALSE; } }; return redisTemplate.execute(callback); } catch (Exception e) { logger.error("release lock occured an exception", e); } return false; } public boolean isLock(String lockId) { final String key = getLockKey(lockId); return redisTemplate.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { try { return connection.get(key.getBytes("UTF-8"))==null; }catch (Exception e) { } return false; } }); } }