https://www.jianshu.com/p/7bf5dc61ca06/
https://blog.csdn.net/qq_34021712/article/details/79606551
https://www.jianshu.com/p/c9f5718e58f0
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
application.properties
简易测试:项目设置pool.max-active=10 ,客户端+redis服务器(1C1G)都是配置很低的PC,无其他业务代码,查询内容在20个字符以内。
用核心线程1000的线程池并发执行3000千次(Hash+String)查询,用时在1s左右。
用核心线程100的线程池并发执行3000千次(Hash+String)查询,用时在1100ms左右。
把设置改为pool.max-active=50,速度没有提升?可能是redis服务器端性能瓶颈?
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=1
# 连接超时时间(毫秒)
spring.redis.timeout=0
#哨兵的配置列表 配置哨兵就不用配置单独host
# redis主节点别名
spring.redis.sentinel.master=mymaster #mymaster是redis-sentinel配置文件中的主机别名
# 哨兵集群 (这里配置的是sentinel节点,不是redis节点)
spring.redis.sentinel.nodes=39.107.119.254:26379,39.107.119.254:26380,39.107.119.254:26381
默认使用jedis作为底层实现。也可以配置成lettuce。lettuce与jedis都是redis的API。但是lettuce类是线程安全的可以通过一个单例对象为多线程提供API服务。jedis不是线程安全的,一个线程对应一个jedis对象。
spring:
redis:
lettuce:
pool:
# 连接池最大连接数(使用负值表示没有限制) 默认 8
max-active: 100
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
max-wait: -1
# 连接池中的最大空闲连接 默认 8
max-idle: 8
# 连接池中的最小空闲连接 默认 0
min-idle: 0
序列化Object
https://blog.csdn.net/u013958151/article/details/80603365
不同序列化方式,存在着解析性能和存储大小的差异,若比较敏感请通过批量读写测试对比。
JDK序列化
默认使用jdk的序列化 RedisTemplate<Object, Object>,其在Redis服务器中是乱码不能解析,可读比较差,但是性能比json格式好。下图是JDK序列化后的KEY-VALUE在Redis服务器中的显示效果。
序列化:Key=String;Value=String (灵活且兼容各种value格式)
RedisTemplate的K-V序列化都设置为StringRedisSerializer。以string格式读/写key-value,此种序列化格式与redis-cil执行指令时序列化格式匹配。在redis控制台中通过指令设置/保存的数据,可以通过此RedisTemplate完成正确的读写。
若value是json格式,可取出后手动再对String转换。
//spring默认的RedisTemplate可以直接使用
@Autowired
StringRedisTemplate stringRedisTemplate;
序列化:Key=String;Value=Json
自定义序列化RedisTemplate<Object, Object>,redisTemplate的Value序列化格式一定要与redis中Value保存的序列化格式一致,才能在读数据时正确的反序列化,否则会抛出jackson序列化异常。
即:只能读取同样序列化配置的RedisTemplate所保存的value,否则会抛出jackson序列化异常。
/**
* redisTemplate 序列化使用的jdkSerializeable, 存储二进制字节码, 所以自定义序列化类
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//下面这行配置不注释,lua返回json会抛异常。Unexpected token (START_OBJECT)
//objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
// value: 将Object转化为Json 保存在Redis中
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); //另外一种JSON格式new GenericJackson2JsonRedisSerializer()
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置HASH序列化
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
下边所有测试都是基于上面Key=String;Value=Json的配置
以上序列化配置在Redis中的存储效果,String类型自动解析为"",自定义类型会记录全类名。
注意设置数字时要这样执行redisTemplate.opsForValue().set("1",1) ;
不要这样执行redisTemplate.opsForValue().set("1","1");这样将设置为字符串
第二种设置的是字符串,不支持任何数字操作api
String-Json value序列化= new Jackson2JsonRedisSerializer(Pojo.class)
//序列化 new Jackson2JsonRedisSerializer(Pojo.class);
@Autowired
private RedisTemplate<String, Pojo> redisTemplate;
SET-JSON
HASH <String,Dhhm> value序列化= new Jackson2JsonRedisSerializer(Dhhm.class)
//序列化 new Jackson2JsonRedisSerializer(Dhhm.class);
@Autowired
private RedisTemplate<String, Dhhm> redisTemplate;
(Dhhm) redisTemplate.opsForHash().get("dhhm", "947607449");
//子key保存到redis时序列化为String,这里也必须是String
以上面配置的RedisTemplate<Object, Object>+ new Jackson2JsonRedisSerializer(Object.class)配置为例子,取出json-value是Object类型(实际底层是LinkedHashMap),不能强转为目标类型,需要通过Jackson转换。
以Zset为例子,其他数据结构同理:与上边例子对比,泛型为<Object,Object>时,redis不会记录class信息。
jackson转换类型:com.fasterxml.jackson.core.type.TypeReference<Set<Pojo>>,支持泛型属性转换
redis中存储格式:
{
"name": "id1",
"map": {
"1": "2",
"2": [1,2,3,4]
},
"list": [
{
"name": "son",
"map": {
},
"list": [
{
"name": "hhhhh",
"map": {
},
"list": []
}
]
}
]
}
@Test
public void testRedisZsetJson() throws IOException {
ZSetOperations<Object, Object> zSetOperations = redisService.getRedisTemplate().opsForZSet();
//写
Pojo pojo = new Pojo();
pojo.setName("id1");
pojo.getMap().put("1","2");
pojo.getMap().put("2", Arrays.asList(1,2,3,4));
Pojo son = new Pojo();
son.setName("son");
//泛型List
pojo.getList().add(son);
son.getList().add( new Pojo("hhhhh"));
zSetOperations.add("jsonZset",pojo,10);
//重复提交pojo,set去重,以最后为准
zSetOperations.add("jsonZset",pojo,11);
//读
ObjectMapper objectMapper = new ObjectMapper();
Set<Object> jsonZset = zSetOperations.range("jsonZset", 0, 200);
//因为使用的RedisTemplate<Object, Object>+json 所以直接转换Pojo类型有java.lang.ClassCastException: java.util.LinkedHashMap
//jackson做类型转换和泛型支持
Set<Pojo> pojos = objectMapper.convertValue(jsonZset, new TypeReference<Set<Pojo>>() {});
for (Pojo o : pojos) {
System.out.println(o);
System.out.println(o.getList().get(0));
System.out.println(o.getList().get(0).getList().get(0));
//删
//zSetOperations.remove("jsonZset",o);
}
}
class Pojo {
private String name;
private HashMap<Object,Object> map=new HashMap();
private List<Pojo> list =new ArrayList<>();
public Pojo() {
}
public Pojo(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public HashMap<Object, Object> getMap() {
return map;
}
public void setMap(HashMap<Object, Object> map) {
this.map = map;
}
public List<Pojo> getList() {
return list;
}
public void setList(List<Pojo> list) {
this.list = list;
}
@Override
public String toString() {
return "Pojo{" +
"name='" + name + '\'' +
", map=" + map +
'}';
}
}
执行结果
Pojo{name='id1', map={1=2, 2=[1, 2, 3, 4]}, list=[Pojo{name='son', map={}, list=[Pojo{name='hhhhh', map={}, list=[]}]}]}
Pojo{name='son', map={}, list=[Pojo{name='hhhhh', map={}, list=[]}]}
Pojo{name='hhhhh', map={}, list=[]}
常用API
以下以RedisTemplate<String, String>为例
Key
redisTemplate.
keys(String pattern):Set<String>
hasKey(String k):Boolean
delete(String k)
expire(String k,long timeout,TimeUnit unit)
expireAt(String k,Date date)
getExpire(String k)
getExpire(String k,TimeUnit unit)
watch(String k)
watch(Collection<String> keys)
multi()
exec():List<Object>
discard()
String
redisTemplate.opsForValue()
set(String k, String v) :void
get(Object k) :String
getAndSet(String k, String v) :String
setIfAbsent(String k, String v) :Boolean //SETNX
increment(String k, long v) :Long //加v 返回运算后结果
increment(String k, double v) :Double //加v 返回运算后结果
List
redisTemplate.opsForList()
size(String k)
range(String k, long s, long e) :List<String> //返回list k的从s到e元素
leftPush(String k, String v) :Long //还有对应的right版
leftPushIfPresent(String k, String v):Long
leftPop(String k) :String //没有会阻塞
leftPop(String key, long time, TimeUnit unit):String
set(String k,long index,String value)
remove(String k,long count,Object value):Long //移除count个,value相等的值,返回移除个数
Hash
redisTemplate.opsForHash()
put(String key, Object hashKey,Object value)
putIfAbsent(String key,Object hashKey,Object value):Boolean
delete(String key,Object... hashKeys):Long
get(String key, Object hashKey):Object
increment(String key,Object hashKey,long delta):Long //还有Double版 返回计算结果
size(String key):Long
hasKey(key, hashKey):Boolean
keys(key):Set<Object>
values(key):List<Object>
entries(key):Map<Object,Object>
RedisTemplate事务
watch、unwatch、multi、exec、discard
可以实现CAS,非原子性事务批量执行,按顺序地串行化整体执行而不会被其它命令插入。multi命令使Redis将这些命令放入queue,而不是执行这些命令。当放入queue失败时(例如:语法错误),EXEC命令可以检测到并且不执行。一旦调用EXEC命令成功,那么Redis就会一次性执行事务中所有命令,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。相反,调用DISCARD命令将会清除事务队列,然后退出事务。EXEC命令的返回值是一个数组,其中的每个元素都分别是事务中的每个命令的返回值,返回值的顺序和命令的发出顺序是相同的。
//CAS 伪代码
//注意MULTI中所有命令 返回NULL,包括GET
WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC
RedisTemplate事务陷阱
https://blog.csdn.net/qq_34021712/article/details/79606551(Spring 事务陷阱,永不关闭连接)
https://www.jianshu.com/p/c9f5718e58f0
陷阱1:不关闭连接问题
如果你的redisTemplate已经开启了事务支持 redisTemplate.setEnableTransactionSupport(true),
且在未标明@Transactional的方法内使用时,需要手动释放链接,否则链接一直不解绑关闭,造成无可用连接问题,或内存爆满
redisTemplate.setEnableTransactionSupport(true);
public Object get(String key){
Object o = redisTemplate.opsForValue().get(key);
//解绑 释放链接
TransactionSynchronizationManager.unbindResource(redisTemplate.getConnectionFactory());
return o;
}
陷阱2:事务将命令放到Queue中,因为命令并没有执行,所以无法拿到返回值,代码片段如下:
假设当前在multi事务中
String i =redisTemplate.opsForValue().get("6666").toString;
Long increment = redisTemplate.opsForValue().increment("6666", 1);
System.out.println(i);//null
System.out.println(increment);//null
是无法拿到返回值的,是因为redis在MULTI/EXEC代码块中,命令都会被delay,放入Queue中,而不会直接返回对应的值。即例子中的increment自增,自增命令执行完成,默认是会返回自增之后的值,但是却是返回了null.
解决思路
前提必须设置:redisTemplate.setEnableTransactionSupport(true)
方法1: @Transactional 来声明 Redis 事务的范围,中间发生异常全部不执行(因为Redis事务是放入队列,Exec时一起执行)
redisTemplate.setEnableTransactionSupport(true);
@Transactional(rollbackFor = Exception.class)
public String put() {
int i = (int)(Math.random() * 100);
template.opsForValue().set("key"+i, "value"+i, 300, TimeUnit.SECONDS);
return "success "+"key"+i;
}
//方法中可以同时有Redis 和 SQL
我们可以先在 Spring 语境里配置一个 PlatformTransactionManager(例如 DataSourceTransactionManager),然后再用 @Transactional 注释来声明 Redis 事务的范围,让 Spring 自动关闭 Redis 连接。
另外,我们还发现了 Redis 事务和关系数据库事务(在本例中,即 JDBC)相结合的不利之处。混合型事务的表现和预想的不太一样。
方法2: 无需@Transactional,手动执行事务(比较灵活),中间发生异常全部不执行(因为Redis事务是放入队列,Exec时一起执行)
redisTemplate.setEnableTransactionSupport(true);
无@Transactional ,手动配置执行事务,txResults 顺序的各行命令的执行结果
List<Object> txResults = redisTemplate.execute(new SessionCallback<List<Object>>() {
public List<Object> execute(RedisOperations operations) {
operations.multi();
operations.opsForSet().add("key", "value1");
return operations.exec();
}
});
事务建议
- 升级到springboot 2.0以上版本,如果因为项目原因无法升级看下面的建议
- 如果使用Redis事务的场景不多,完全可以自己管理(方法2),不需要使用spring的注解式事务。
- 如果一定要使用spring提供的注解式事务,建议初始化两个
RedisTemplate
Bean,分别设置enableTransactionSupport
属性为true和false。针对需要事务和不需要事务的操作使用不同的template。 - 从个人角度,我不建议使用redis事务,因为redis对于事务的支持并不是关系型数据库那样满足ACID。Redis事务只能保证ACID中的隔离性和一致性,无法保证原子性和持久性。而我们使用事务最重要的一个理由就是原子性,这一点无法保证,事务的意义就去掉一大半了。所以事务的场景可以尝试通过业务代码来实现。
陷阱
事务中的陷阱已经在上面说明,下面记录一些常用操作陷阱。
1 不释放链接问题
https://segmentfault.com/a/1190000021204378?utm_source=tag-newest
stringRedisTemplate 对redis常规操作做了一些封装,但还不支持像 Scan SetNx等命令,这时需要拿到jedis Connection进行一些特殊的Commands。使用 stringRedisTemplate.getConnectionFactory().getConnection() 是不被推荐的,因为这需要手动释放链接。如果没有代码中手动释放则会触发连接池中无可用连接,大量请求阻塞或失败。
推荐使用下面这种方式拿到RedisConnection。RedisTemplate源码中也是使用execute(RedisCallback)执行。
Set<Object> execute = redisTemplate.execute(new RedisCallback<Set<Object>>() {
@Override
public Set<Object> doInRedis(RedisConnection connection) throws DataAccessException {
Set<Object> binaryKeys = new HashSet<>();
//执行scan
Cursor<byte[]> cursor = connection.scan( new ScanOptions.ScanOptionsBuilder().match("test*").count(1000).build());
while (cursor.hasNext()) {
binaryKeys.add(new String(cursor.next()));
}
//不需要手动关闭链接
return binaryKeys;
}
});
pipeline管道化
使用Pipeline合并请求减少TCP链接次数。客户端允许将多个请求一次发给服务器,过程中而不需要等待请求的回复,在最后再一并读取结果即可。
- pipeline机制可以优化吞吐量,但无法提供原子性/事务保障,而这个可以通过Redis-Multi等命令实现。
- 部分读写操作存在相关依赖,无法使用pipeline实现,可利用Script机制,但需要在可维护性方面做好取舍
https://www.cnblogs.com/littleatp/p/8419796.html
jedis操作
Pipeline pipeline = jedis.pipelined();
int j;
for (j = 0; j < batchSize; j++) {
if (i + j < cmdCount) {
pipeline.set(key(i + j), UUID.randomUUID().toString());
} else {
break;
}
}
pipeline.sync(); //等待
List<Long> List = redisTemplate.executePipelined(new RedisCallback<Long>() {
@Nullable
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
connection.openPipeline();
for (int i = 0; i < 1000000; i++) {
String key = "123" + i;
connection.zCount(key.getBytes(), 0,Integer.MAX_VALUE);
}
return null;
}
});
令牌桶LUA实现
原文:https://my.oschina.net/lyyjason/blog/1608213
将添加令牌改成触发式的方式,取令牌同时做添加令牌的动作(惰性处理,类似于TTL),也可以定时任务处理。
- curr_mill_second = 当前毫秒数
- last_mill_second = 上一次添加令牌的毫秒数
- r = 添加令牌的速率
- reserve_permits = (curr_mill_second-last_mill_second)/1000 * r //通过时间差和添加速率,算出本次要添加数量
--- 获取令牌
--- 返回值
--- 0 没有令牌桶配置
--- -1 表示取令牌失败,也就是桶里没有令牌
--- 1 表示取令牌成功
--- 入参
--- @param key 令牌(资源)的唯一标识
--- @param permits 请求令牌数量
--- @param curr_mill_second 当前毫秒数
--- @param context 使用令牌的应用标识
local function acquire(key, permits, curr_mill_second, context)
local rate_limit_info = redis.pcall("HMGET", key, "last_mill_second", "curr_permits", "max_permits", "rate", "apps")
local last_mill_second = rate_limit_info[1]
local curr_permits = tonumber(rate_limit_info[2])
local max_permits = tonumber(rate_limit_info[3])
local rate = rate_limit_info[4]
local apps = rate_limit_info[5]
--- 标识没有配置令牌桶
if type(apps) == 'boolean' or apps == nil or not contains(apps, context) then
return 0
end
local local_curr_permits = max_permits;
--- 令牌桶刚刚创建,上一次获取令牌的毫秒数为空
--- 根据和上一次向桶里添加令牌的时间和当前时间差,触发式往桶里添加令牌,不超过桶的最大值
--- 并且更新上一次向桶里添加令牌的时间
--- 如果向桶里添加的令牌数不足一个,则不更新上一次向桶里添加令牌的时间
if (type(last_mill_second) ~= 'boolean' and last_mill_second ~= false and last_mill_second ~= nil) then
local reverse_permits = math.floor(((curr_mill_second - last_mill_second) / 1000) * rate)
local expect_curr_permits = reverse_permits + curr_permits;
local_curr_permits = math.min(expect_curr_permits, max_permits);
--- 大于0表示不是第一次获取令牌,也没有向桶里添加令牌
if (reverse_permits > 0) then
redis.pcall("HSET", key, "last_mill_second", curr_mill_second)
end
else
redis.pcall("HSET", key, "last_mill_second", curr_mill_second)
end
local result = -1
if (local_curr_permits - permits >= 0) then
result = 1
redis.pcall("HSET", key, "curr_permits", local_curr_permits - permits)
else
redis.pcall("HSET", key, "curr_permits", local_curr_permits)
end
return result
end
执行LUA
https://blog.csdn.net/u014495560/article/details/82531046
串行化执行保证:原子性、有序性、可见性、减少通讯次数、无回滚
Lua 脚本项目的 resources 目录下,起名 limit.lua 即可
注意:KEYS[1],ARGV[1]必须大写,数组从1开始。
local key = KEYS[1]; //必须大写
local dzyid=ARGV[1];
--转数字(对应key的值必须是redis数字才行,否则nil)
local current =tonumber( redis.call('get', key) );
--tonumber功能并不是将字符串转为数字,而是将redis数字转换LUA数字,为了在LUA中进行计算
if (current==1) then --数字比较
return current+1;
end
源码中写到ResultType支持:Long, Boolean, List, or deserialized value type 。List代表lua返回数组。
/**
* 适用于lua返回数字。
* @return
*/
@Bean
public DefaultRedisScript<Number> redisluaScript() {
DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limit.lua")));
redisScript.setResultType(Number.class); //返回类型
return redisScript;
}
@Bean("simlockredisluaScriptLong")
public DefaultRedisScript<Long> simlockredisluaScriptLong() {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lockdhhm.lua")));
//源码中写到ResultType支持:Long, Boolean, List, or deserialized value type
redisScript.setResultType(Long.class);//返回值类型 不要用Integer
return redisScript;
}
/**
* 适用于lua返回string、json字符串。
* @return String
*/
@Bean
public DefaultRedisScript<String> redisluaScriptStrig() {
DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limit.lua")));
redisScript.setResultType(String.class);//返回类型
return redisScript;
}
/**
* 适用于lua返回List、Set、lua-table。
* 使用RedisTemplate<String, String>执行脚本,返回List<String>类型
* @return List
*/
@Bean("redisluaScriptList")
public DefaultRedisScript<List> redisluaScriptList() {
DefaultRedisScript<List> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/getset.lua")));
redisScript.setResultType(List.class);
return redisScript;
}
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
private DefaultRedisScript<Number> redisluaScript;
List<String> keys= Arrays.asList("key1");
//执行LUA <T> T execute(RedisScript<T> var1, List<K> var2, Object... var3);
//args 传字符串类型,否则会报错。在lua中转数字tonumber()。
Number number = redisTemplate.execute(redisluaScript,keys, arg1, arg2);
注意: 序列化格式与存储格式、脚本返回类型匹配、Value类型与LUA数字计算、入参类型,一定要与redis中实际存储的数据类型匹配,否则会有各种异常。
1 序列化:redisTemplate的序列化(String、Json、JDK序列化...)一定要与redis中Value保存的序列化格式一致,才能在get时正确的反序列化,否则会抛出异常。
2 返回值类型:T redisTemplate.execute返回值类型,取决于DefaultRedisScript设置的返回值类型。
3 lua的返回值类型: lua的返回值类型要与DefaultRedisScript设置的返回值类型完全匹配。比如value=1,返回值类型必须是Long,DefaultRedisScript设置为返回值String也会报错。所以可以设置返回值类型为Object,然后在通过instanceof判断类型。
4 redis存储格式与LUA:注意设置数字时要这样执行redisTemplate.opsForValue().set("1",2) 。
不要这样执行redisTemplate.opsForValue().set("1","2");
第二种设置的是字符串 ,LUA不支持对其tonumber()和运算,tonumber并不是将字符串转为数字。
5 若LUA返回值为数字,要用Long/Number做JAVA的返回类型,不要用Integer;(会出现IllegalStateException)
6 参数类型一定和redis中匹配。args 最好传字符串类型,否则可能会报错。在lua中转数字tonumber(AVGE[1])。
常见异常报错
例1
Caused by: io.lettuce.core.RedisCommandExecutionException: ERR Error running script (call to f_47e4c946b9a30705a03eded83a2e086b54791c44): @user_script:3: user_script:3: attempt to perform arithmetic on local 'current' (a nil value)
解释 在lua脚本的第3行出错,local变量current是nil
例2
SerializationException: Could not read JSON
序列化异常,要保证返回值格式符合redisTemplate解析格式。设置redisTemplate解析value为JSON,若LUA直接返回字符串需要加“”,否则无法解析JSON。
if (haskey == 0) then
return '"失败"';
else
return '"成功"';
end
例3
java.lang.IllegalStateException
LUA返回值为整数时出现此异常,确定请在JAVA配置的返回值类型为Long而不是Integer。
例4
集群环境:EvalSha is not supported in cluster environment
需要切换使用LettuceConnectionFactory
@Bean
LettuceConnectionFactory redisConnectionFactory(RedisClusterConfiguration configuration) {
return new LettuceConnectionFactory(configuration);
}
例5
集群环境:CROSSSLOT Keys in request don't hash to the same slot
--keys不能落在同一个节点
keys.add("{test}key1");
keys.add("{test}key2");
LUA预加载
启动程序时可以将lua脚本预加载到redis中返回sha1值,执行simlockredisluaScriptLong脚本时只需要发送sha1+参数到redis。
减少了网络开销。
@PostConstruct
public void loadScript() {
//RedisCallback自动释放connection
String execute = redisTemplate.execute((RedisCallback<String>) connection -> {
return connection.scriptLoad(simlockredisluaScriptLong.getScriptAsString().getBytes());
});
}
LUA语法
注意:KEYS[1],ARGV[1]大写,且数组从1开始计数
1 数字相关的比较/计算必须要用tonumber()转化
if (tonumber(var)==tonumber(var1)) then
end
2 解析JSON cjson.decode。
例子:https://blog.csdn.net/fsw4848438/article/details/81540495
redis中的lua已经安装了 lua-cjson 库。不需要手动安装。
注意:在配置redisTemplate的jackson序列化时不要设置objectMapper.enableDefaultTyping();
这个配置使Jackson把数组的json字符串反序列化为List时候报Unexpected token (START_OBJECT)
--json字符串
local sampleJson = [[{"age":"23","testArray":{"array":[8,9,11,14,25]},"Himi":"himigame.com"}]];
--解析字符串,转化为json对象
local data = cjson.decode(sampleJson);
--打印json字符串中的age字段
print(data["age"]);
--打印数组中的第一个值(lua默认是从0开始计数)
print(data["testArray"]["array"][1]);
3 创建JSON cjson.encode
lua返回json时,实际返回的是list<map>,map为json本体。
-- lua table
local result ={}
--可以不传类型,以标识/业务约定。由java代码自主判断json的解析类型
--result["@class"] = "com.common.Result"
result["success"] = true
result["data"] = redis.call("get",key1)
--转化为json字符串
return cjson.encode(result)
4 字符串拼接..
'str1'..'str2'
'str1','str2'
5 判断redis.call('get',limitkey )返回null
redis没有get到key返回null,此时LUA中返回的结果不是 nil
而是 userdata
类型的 ngx.null。
当使用lua脚本执行逻辑时,如果要判断这个值,很容易让人迷惑以为它是nil,从而导致判断不成立,实际它是一个boolean的值
在lua中,除了nil和false,其他的值都为真,包括0,可以通过nil为false这一点来判断是否为空
local current = redis.call('get', key);
--在lua中,除了nil和false,其他的值都为真,包括0,可以通过nil为false这一点来判断是否为空
if current then
return '"存在"';
else
return '"不存在"';
end
6 类型判断
type(apps) == 'boolean' //类型是布尔
7 字符串包含
if not contains(apps, context) then //apps中包含context
一个简单LUA案例
模拟多人并发抢号的场景,抢到号码后锁定10分钟。10分钟后不操作释放号码。
redis中预热保存了50个号码测试信息,key为号码,value为号码信息。
--加载lua脚本
@Bean("simlockredisluaScriptLong")
public DefaultRedisScript<Long> simlockredisluaScriptLong() {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lockdhhm.lua")));
redisScript.setResultType(Long.class);//返回值类型 不要用Integer
return redisScript;
}
@Autowired
@Qualifier("simlockredisluaScriptLong")
DefaultRedisScript<Long> simlockredisluaScriptLong;
int dhhm=111125730; //目标号码
String code="人员ID1";
--执行 注意参数的类型与redis匹配
Long result = redisTemplate.execute(simlockredisluaScriptLong, Arrays.asList(), dhhm, code);
LUA脚本代码 lockdhhm.lua
验证号码合法,验证个人锁定次数防刷,尝试加锁10分钟
--返回负值=失败
local key = 'sim:dhhm'; --号码列表
local dhhm = ARGV[1]; --目标号码
local dzyid = ARGV[2]; --员工ID
--local fdbs=ARGV[3]; --分店
local lockkey = 'sim:lock:' .. dhhm;
--检验号码已被锁(加速返回,不做后边的操作)
local belock = redis.call('get', lockkey);
if belock then
--在lua中,除了nil和false,其他的值都为真,包括0,可以通过nil为false这一点来判断是否为空
--已存在
return -3;
end
--检验号码存在
if (redis.call('hexists', key, dhhm) == 0) then
return -1;
end
--防刷 每个员工ID每个号码一小时只能锁3次
local limitkey = 'sim:limit:' .. dhhm .. ':' .. dzyid;
local limitnum = redis.call('get', limitkey);
if limitnum then
if (tonumber(limitnum) > 2) then
return -2;
end
end
--尝试号码加锁 10分钟
local snkey = 'sim:sn:' .. dhhm;
if (redis.call('setnx', lockkey, dzyid) > 0) then
redis.call('expire', lockkey, 600);
--防刷 记录员工ID锁定此号次数
if (redis.call('setnx', limitkey, 1) > 0) then
--首次
redis.call('expire', limitkey, 3600);
else
--累计
redis.call('incr', limitkey);
end
--此序号用于携带给消费者,消费者核对当前消费是不是此号码最新加锁序号
return redis.call('incr', snkey);
else
return -3;
end
测试模拟多人并发抢同一号码
--模拟多人并发抢同一号码
@Autowired
DefaultRedisScript<Long> simlockredisluaScriptLong;
@Test
public void simlock() throws InterruptedException {
int num=100;
int dhhm=111125730; //抢夺目标号码
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(50, 50,
3, TimeUnit.MINUTES, new ArrayBlockingQueue<>(num));
CountDownLatch begin = new CountDownLatch(1);
CountDownLatch end = new CountDownLatch(num);
for (int i = 0; i < num; i++) {
threadPoolExecutor.execute(() -> {
try {
//测试 随机生成人员编号
String dzyid=String.valueOf(new Random().nextInt(100));
System.out.println(dzyid+"准备就绪");
begin.await();
Long result = redisTemplate.execute(simlockredisluaScriptLong, Arrays.asList(), dhhm, dzyid);
if (result >=0){
System.out.println(dzyid+"成功锁定"+dhhm);
}
end.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
System.out.println("begin:"+System.currentTimeMillis());
begin.countDown();
end.await();
System.out.println("end:"+System.currentTimeMillis());
}
运行后显示 "84成功锁定111125730",检查Redis数据库正确生成对应的锁
Spring boot实现监听Redis key失效事件实现
原文:https://blog.csdn.net/zwrlj527/article/details/88876315
修改: redis.conf 开启相关时间推送。