赞
踩
网上有太多的解释了,基于项目使用简单说一下自己的理解。
背景:项目采用集群部署,需要解决的问题,防止不同服务器调用相同的接口,导致数据重复更新,导致数据问题。
使用Redisson控制相同接口,处理并发操作,来解决上述问题。
下面开整!!!
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.14.0</version>
</dependency>
正常是这样直接引入pom文件即可,但是在后续使用的时候会报maven冲突
所以还是按照下面的方式引入,剔除23采用21。
<!-- 集成redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.14.0</version>
<exclusions>
<exclusion>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-data-23</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-data-21</artifactId>
<version>3.14.0</version>
</dependency>
在使用Redisson之前,很多项目都是已经配置了redis,如果已经配置了redis,那配置文件就不需要改动,便可以直接使用。
如果没有redis的配置文件,则可引入。
此处从安全角度出发,建议redis设置密码。
如果项目没有redis,SpringBoot项目还需要加一个config配置类。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory factory;
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
}
下面搞一个工具类,方便使用。
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.UUID;
/**
* Redisson 加锁
*/
@Component
public class RedissonUtil {
@Resource
private RedissonClient redissonClient;
public String getKey(){
return UUID.randomUUID().toString();
}
public String getKey(Class<?> tClass, Thread thread){
return tClass.toString() + "_" + thread.getStackTrace()[2].getMethodName();
}
public RLock getClint(String key){
RReadWriteLock lock = redissonClient.getReadWriteLock(key);
return lock.writeLock();
}
public void lock(String key) {
this.getClint(key).lock();
}
public void unLock(String key) {
this.getClint(key).unlock();
}
}
工具类中的getKey(),这个是一个随机的,方便测试使用。
getKey另一个getKey()生成的Key为调用的方法对应的类名拼接方法名
具体使用如下
获取Key
String lockKey = redissonUtil.getKey(this.getClass(), Thread.currentThread());
那么问题来了,怎么使用呢?
直接使用工具类,对于特殊的代码块,手动加锁。如下:
public class TestRedisson {
@Resource
private RedissonUtil redissonUtil;
public void testRedisson(){
//定义Key
String myKey = ")!@#$%^&*(";
redissonUtil.lock(myKey);
{
//执行代码块
}
redissonUtil.unLock(myKey);
}
}
如果不想获取两次客户端
public class TestRedisson {
@Resource
private RedissonUtil redissonUtil;
public void testRedisson(){
//定义Key
String myKey = ")!@#$%^&*(";
RLock clint = redissonUtil.getClint(myKey);
clint.lock();
{
//执行代码块
}
clint.unlock();
}
}
这样的话,是可以实现,只不过如果我很多代码都需要的话,是不是有点麻烦了呢?
那么可以换一种方式来处理。
锁的机制是有头有尾,而拦截器只是有头,对尾不做处理。
似乎好像不太可行。
作用似乎同上,好像也不太行。
听上去好像可行。
用一个注解,然后对项目进程开启监听,只要通过调用注解的方法,就加锁。
但问题似乎还是同上,无法做到收尾。
那么我们可以用接口,配合注解来实现包含头包含尾。
开整!!!
先搞一个方法级注解
注:注解使用时,值填的是固定值
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 业务锁
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BusinessLock {
String value() default "";
}
再搞一个接口
如果只写一个接口,里面定义一个方法,那么实现我的接口,就需要实现我的这个固定的方法,然后搞一下加锁。
如此一来,是不是我一个类,如果有两个方法都需要加锁的话,就没办法实现了呢?
改动一下接口
import com.BusinessLock;
import com.RedissonUtil;
import com.SpringContextUtils;
import org.redisson.api.RLock;
import org.springframework.core.annotation.AnnotationUtils;
import java.lang.reflect.Method;
public interface BaseService {
public default Result doBusiness(String json) {
Object o = SpringContextUtils.getBean(this.getClass());
Method[] methods = o.getClass().getMethods();
for (Method m : methods) {
BusinessLock businessLock = AnnotationUtils.findAnnotation(m, BusinessLock.class);
boolean isLock = false;
RLock rLock = null;
if(null != businessLock){
isLock = true;
RedissonUtil redissonUtil = SpringContextUtils.getBean(RedissonUtil.class);
rLock = redissonUtil.getClint(o.getClass().getName() + "_" + m.getName() + "_" + businessLock.value());
}
try {
if(isLock) rLock.lock();
return (Result) m.invoke(o, json);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if(isLock) rLock.unlock();
}
}
throw new IllegalArgumentException("方法不存在" + o.getClass().getName());
}
}
上述方法,似乎已经实现了功能,通过反射调用,在前后进行加解锁。
好像还不能使用,新的问题又来了,我怎么知道反射调用哪个方法呢?
也就是说接口进来,怎么知道调用实现类的哪个方法呢?
OK,再搞一个注解吧
import org.springframework.stereotype.Component;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Component
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BusinessMethod {
String value() default "";
String[] params() default {};
}
BaseService 就得改造一下了
import com.BusinessLock;
import com.HandleMethod;
import com.RedissonUtil;
import com.SpringContextUtils;
import org.redisson.api.RLock;
import org.springframework.core.annotation.AnnotationUtils;
import java.lang.reflect.Method;
public interface BaseService {
public default Result doBusiness(String json, String method) {
Object o = SpringContextUtils.getBean(this.getClass());
Method[] methods = o.getClass().getMethods();
for (Method m : methods) {
BusinessMethod businessMethod = AnnotationUtils.findAnnotation(m, BusinessMethod.class);
if(null != businessMethod && businessMethod.value().equals(method)){
BusinessLock businessLock = AnnotationUtils.findAnnotation(m, BusinessLock.class);
boolean isLock = false;
RLock rLock = null;
if(null != businessLock){
isLock = true;
RedissonUtil redissonUtil = SpringContextUtils.getBean(RedissonUtil.class);
rLock = redissonUtil.getClint(o.getClass().getName() + "_" + m.getName() + "_" + businessLock.value());
}
try {
if(isLock) rLock.lock();
return (Result) m.invoke(o, json);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if(isLock) rLock.unlock();
}
}
}
throw new IllegalArgumentException("方法不存在" + o.getClass().getName());
}
}
嗯,这样似乎就好一点了,不过还有另一种方式,调用那个method,是接口传的,本身接口拿到的就是报文,是不是可以在json中,加一个默认的参数,就叫interfaceMethod?
这样,是不是就可以直接从json中获取方法,与注解中的值匹配了?
注解,接口都好了,那下面就是使用了
@Controller
public class TestRedisson Controller {
@Resource
private ITestRedisson testRedisson;
@RequestMapping("/testRedisson")
@ResponseBody
public Result doBusiness(@RequestBody String strJson) throws Exception {
return testRedisson.doBusiness(strJson,"testRedisson");
}
}
public interface ITestRedisson extends BaseService {
}
public class TestRedisson implements ITestRedisson {
@BusinessLock("testRedisson")
@BusinessMethod("testRedisson")
public void testRedisson(){
{
//执行代码块
}
}
}
嗯,应该可以了,不过还可以优化优化,将这个method放进json中。
锁的作用域仅仅作用在某个类的某个方法上
package com;
import com.Result;
import com.RedissonUtil;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/test")
@Slf4j
public class B {
@Resource
private RedissonUtil redissonUtil;
@RequestMapping(value = "/api")
@ResponseBody
public Result methodOne(@RequestBody String strJson) throws Exception {
RLock clint = redissonUtil.getClint(redissonUtil.getKey(this.getClass(), Thread.currentThread()));
clint.lock();
Thread.sleep(Integer.valueOf(strJson));
this.methodTwo(strJson);
clint.unlock();
return Result.success(strJson);
}
@RequestMapping(value = "/api1")
@ResponseBody
public Result methodTwo(@RequestBody String strJson) throws Exception {
Thread.sleep(Integer.valueOf(strJson));
return Result.success(strJson);
}
}
如上方法
调用接口 /test/api 时,传参10000,这时方法 methodOne 加锁,方法内等待10秒。
在等待的过程中调用接口 /test/api1 ,传参1,这时接口即可响应,并不受 methodOne 加锁的限制。
由此得出,锁内调用的方法,不受加锁的影响,仍可供其他线程调用。
对于金额等敏感数据的操作,一定需要注意是否会重复叠加。
对表的更新,可以直接更新某个字段为某个值,也可以换种方式,更新某个字段,为字段本身加某个值。
例如:update test set amount = amount + 100 where business_no = ‘test’;
OK,整理到这吧!
如有不正确之处,还望指正!书写不易,觉得有帮助就点个赞吧!☺☺☺
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。