乐观锁实现
核心思路就是每次要改变数据时先请求一个version号,如果改变的时候其他人改变成功了version会加,这时会无法改变该条数据,思路是用@version注解,也可以参考下面链接自己写sql语句
乐观锁实现参考
StackOverflow回答
存在的问题:由于用了mybatis自动生成代码,在加入自定义乐观锁实现后,每次更新数据表重新运行mybatis时会覆盖掉自添加的代码,mapper层和xml的代码都会覆盖,除非这个表不会再变直接从自动生成的配置文件里去除该表
另外一种思路是做行锁,加上for update的操作:
这里值得注意的是:InnoDB 默认是行级别的锁 , 当有明确指定的主键时候 , 是行级锁 , 否则是表级别,具体看一下以下的链接
ForUpdate示例
START TRANSACTION ;
SELECT * FROM table_name WHERE id=1 FOR UPDATE ;
UPDATE table_name set count = count-1 WHERE id=1;
<!--
乐观锁自定义实现
-->
<update id="updateCreditsCAS" parameterType="org.linlinjava.litemall.db.domain.LitemallUserCredits">
<!--
WARNING - @mbg.generated
This element is automatically generated by MyBatis Generator, do not modify.
-->
update litemall_user_credits
set user_id = #{userId,jdbcType=INTEGER},
type_id = #{typeId,jdbcType=INTEGER},
credit_name = #{creditName,jdbcType=VARCHAR},
credits_count = #{creditsCount,jdbcType=INTEGER},
expired_time = #{expiredTime,jdbcType=TIMESTAMP},
create_time = #{createTime,jdbcType=TIMESTAMP},
update_time = #{updateTime,jdbcType=TIMESTAMP},
version = version+1
where id = #{id,jdbcType=INTEGER} and version = #{version, jdbcType=INTEGER}
</update>
获取请求body中的参数
1、 用@RequestBody JSONObject jsonObject
public Object increaseStoreCredits(@RequestBody JSONObject jsonObject) {
double amount = jsonObject.getDouble("amount");
long orderId = jsonObject.getLong("orderId");
}
2、写个JacksonUtil类来转换
mport com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;
import java.util.List;
import java.util.Map;
public class JacksonUtil {
private static final Log logger = LogFactory.getLog(JacksonUtil.class);
public static String parseString(String body, String field) {
ObjectMapper mapper = new ObjectMapper();
JsonNode node;
try {
node = mapper.readTree(body);
JsonNode leaf = node.get(field);
if (leaf != null)
return leaf.asText();
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
return null;
}
public static Integer parseInteger(String body, String field) {
ObjectMapper mapper = new ObjectMapper();
JsonNode node;
try {
node = mapper.readTree(body);
JsonNode leaf = node.get(field);
if (leaf != null)
return leaf.asInt();
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
return null;
}
在Controller层调用
public Object register(@RequestBody String body, HttpServletRequest request) {
String username = JacksonUtil.parseString(body, "username");
}
获取url路径参数
有两种路径参数:
- /api/get?id=2
- /api/get/id/2
先说第一种/api/get?id=2
public Object querydeliveryFee(@RequestParam String id)
注意此时url里必须有id,跟代码里的id名要一样,如果你传了一个叫otherid,代码无法识别
再看第二种:/api/get/id/2
@RequestMapping(value="/api/{id}/{age}",method=RequestMethod.GET)
public String getName(ModelMap map,@PathVariable("id") String name,@PathVariable("age") int age)
自定义注解
这里记录一个@VisitorAccessible
的注解,打上该注解的接口不需要登录,其他的需要登录。
VisitorAccessible 类:
@Inherited
@Retention(RUNTIME)
@Target({METHOD})
public @interface VisitorAccessible {
}
InitInterceptor类:实现该注解的拦截和操作
@Component
public class InitInterceptor implements HandlerInterceptor {
private static final Log LOGGER = LogFactory.getLog(InitInterceptor.class);
@Autowired
private LitemallUserService userService;
private NamedThreadLocal<Long> startTimeThreadLocal =
new NamedThreadLocal<Long>("StopWatch-StartTime");
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "Content-Type,Accept,X-Requested-With,token");
long beginTime = System.currentTimeMillis();//1、开始时间
startTimeThreadLocal.set(beginTime);
if (request.getMethod().toUpperCase().equals("OPTIONS") || request.getServletPath().contains("ok") || request.getServletPath().contains("api/user/login")) {
return true;
}
// 处理用户
String token = request.getHeader("token");
if (StringUtils.isNotEmpty(token)) {
String userCode = "";
try {
userCode = (String) JWTVerifierUtil.verify(token).get(2);
if (StringUtils.isEmpty(userCode)) {
throw new ApplicationException(CommonCodeEnum.NO_ACCESS_RIGHT);
}
} catch (Exception e) {
throw new ApplicationException(CommonCodeEnum.NO_ACCESS_RIGHT);
}
LitemallUser userInfoEntity = userService.findById(Integer.valueOf(userCode));
if (userInfoEntity == null) {
throw new ApplicationException(CommonCodeEnum.NO_ACCESS_RIGHT);
}
UserVo userInfo = new UserVo();
BeanUtils.copyProperties(userInfoEntity, userInfo);
GlobalHolder.setCurrentLoginUser(userInfo);
} else {
HandlerMethod handlerMethod = (HandlerMethod) handler;
VisitorAccessible annotation = handlerMethod.getMethodAnnotation(VisitorAccessible.class);
if (annotation == null) {
throw new ApplicationException(CommonCodeEnum.NO_ACCESS_RIGHT);
}
}
return true;
}
/**
* 在业务处理器处理请求执行完成后,生成视图之前执行的动作 可在modelAndView中加入数据,比如当前时间
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
/**
* 在DispatcherServlet完全处理完请求后被调用,可用于清理资源等
* 当有拦截器抛出异常时,会从当前拦截器往回执行所有的拦截器的afterCompletion() 处理写session工作
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
try {
long endTime = System.currentTimeMillis();//2、结束时间
long beginTime = startTimeThreadLocal.get();//得到线程绑定的局部变量(开始时间)
long consumeTime = endTime - beginTime;//3、消耗的时间
if (consumeTime > 500) {//此处认为处理时间超过500毫秒的请求为慢请求
String ip = IpUtil.getIpAddr(request);
LOGGER.info("ip:"+ ip +"request:"+ String.format("%s consume %d millis", request.getRequestURI(), consumeTime));
}
} catch (Exception e) {
e.printStackTrace();
}
GlobalHolder.removeCurrentLoginUser();
}
}
GlobalHolder类:增删查当前用户
public class GlobalHolder {
private static ThreadLocal<UserVo> currentLoginUser = new ThreadLocal<UserVo>();
public static UserVo getCurrentLoginUser() {
return currentLoginUser.get();
}
public static void setCurrentLoginUser(UserVo user) {
currentLoginUser.set(user);
}
public static void removeCurrentLoginUser() {
currentLoginUser.remove();
}
}
WxWebMvcConfiguration :在配置类里配置需要拦截的访问路径为api开头
@Configuration
public class WxWebMvcConfiguration implements WebMvcConfigurer {
@Autowired
private InitInterceptor initInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(initInterceptor).addPathPatterns("/api/**");
}
}
@LoginUser获取用户token注解实现
这里核心是使用了HandlerMethodArgumentResolver的接口,先定义LoginUser的注解
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}
public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
public static final String LOGIN_TOKEN_KEY = "X-Litemall-Token";
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().isAssignableFrom(Integer.class) && parameter.hasParameterAnnotation(LoginUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
NativeWebRequest request, WebDataBinderFactory factory) throws Exception {
// return new Integer(1);
String token = request.getHeader(LOGIN_TOKEN_KEY);
if (token == null || token.isEmpty()) {
return null;
}
return UserTokenManager.getUserId(token);
}
}
HandlerMethodArgumentResolver实现教程
自定义注解拦截url
实现短信发送
ClickSend短信发送平台
在pom文件引入
<dependency>
<groupId>com.github.clicksend</groupId>
<artifactId>clicksend-java-client</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
发送短信代码:
public String sendSMS(String sendTo, String content) {
ApiClient defaultClient = new ApiClient();
defaultClient.setUsername("yourusername");
defaultClient.setPassword("YourKey");
SmsApi apiInstance = new SmsApi(defaultClient);
SmsMessage smsMessage = new SmsMessage();
smsMessage.body(content);
smsMessage.to(sendTo);
smsMessage.source("sdk");
List<SmsMessage> smsMessageList = Arrays.asList(smsMessage);
// SmsMessageCollection | SmsMessageCollection model
SmsMessageCollection smsMessages = new SmsMessageCollection();
smsMessages.messages(smsMessageList);
try {
String result = apiInstance.smsSendPost(smsMessages);
System.out.println(result);
return result;
} catch (ApiException e) {
System.err.println("Exception when calling SmsApi#smsSendPost");
e.printStackTrace();
}
return "Something wrong";
}
jar启动
代码里如果配置了日志目录,目录文件在当前执行java -jar的目录,2>&1代表把nohup的错误和info都输出在一个地方
nohup java -jar litemall-wx-api.jar >nohup.out 2>&1 &
数据库备份
#!/bin/bash
#备份路径
backup_path=/backup/mysql
#日期
current_date=`date "+%Y_%m_%d"`
#备份工具,如果找不到用whereis mysqldump
dump=/usr/local/bin/mysqldump
#连接数据库用户名
user=root
#连接数据库密码
password=yourpassword
#获取所有数据库
databases=$(/usr/local/bin/mysql -h192.168.213.135 -P31313 -uroot -pyourpassword -e "show databases" | grep -Ev "Database|sys|information_schema|performance_schema|mysql")
#循环备份
for db in $databases
do echo
echo ----------------$backup_path/${db}_$current_date.sql.gz BEGIN ----------------------
/usr/local/bin/mysqldump -h192.168.213.135 -P31313 -uroot -pyourpassword --default-character-set=utf8 -q --lock-all-tables --flush-logs -E -R --triggers -B ${db} | gzip > $backup_path/${db}_$current_date.sql.gz
echo -------------------$backup_path/${db}_$current_date.sql.gz COMPLETE -----
echo
done
echo "done"
#删除三天前备份文件
echo "-----------delete 3days before files start -----------"
find /backup/mysql/ -type f -mtime +3 -exec rm {} \;
打包命令
mvn clean package -pl litemall-admin-api -am -Dmaven.test.skip=true
mvn clean package -pl litemall-wx-api -am -Dmaven.test.skip=true
git查看某个文件历史变化
最简单直观的方式是借助IDEA:在project源码中,找到目标文件–>右键Git–>show histroy,在每个commit里可以右击选择differ查看内容
-
git log filename
注意这里的filename属于文件的路径,比如:F:/workspace/shop-service/litemall-wx-api/src/main/java/org/linlinjava/litemall/wx/web/WxAuthController.java
可以看到fileName相关的commit记录 -
git log -p filename
可以显示每次提交的diff
- 只看某次提交中的某个文件变化,可以直接加上fileName
git show c5e69804bbd9725b5dece57f8cbece4a96b9f80b filename
省市区数据维护
澳洲行政区邮编数据,这里的数据还不够细化,一个悉尼的城区会对应多个邮编,需要细化到城区里的具体哪个区
官方统计数据
Mysql排除某个查询结果
使用场景:查询用户信息,但是信息表里包含了密码的敏感信息,需要忽略他,比较笨的方式是一个个select出来,但是如果字段很多就显得冗余。
有三种思路:
1、结合Mybatis plus插件来做,还没实验过
2、用@JsonIgnore注解:import com.fasterxml.jackson.annotation.JsonIgnore;
这里只是针对返回为JSON数据时才会忽略,如果只是单纯的返回对象,他还是会显示出查询的结果来
3、写一个返回类,不包含其你要忽略的字段
idea去除mapper.xml的黄色显示背景
删除/**类的注释
删除 java 注释 /* */:/\*{1,2}[\s\S]*?\*/
删除 java 注释 //://[\s\S]*?\n
删除xml注释:<!-[\s\S]*?-->
删除空白行:^\s*\n
idea一直卡在Scanning files to index
打开项目后idea一直卡在这个页面,且卡死,此时只要找到前端代码的node_modules文件夹,右击将其exclude即可
MySQL设置了默认值但是插入后显示为null
看下网上的解答,查看一下代码果然是,不用selective的话mybatis会把没写的填充成null传过去,此时数据库又认为你传值过来了,null就狸猫换太子
你是不是用了持久化工具如mybatis这样的;
然后按照实体来进行插入?
1.mysql字段默认的含义:在插入时不指定该字段的值;
2.以mybatis举例,如果是插入实体,那么为空的字段就会插入空;
3.如果不想mybatis等持久化工具插入空,可以尝试insertSelective方式进行,这样为空字段会被剔除
idea设置jetbrain mono字体
发现了一个非常适合代码的字体,安装摸我
xtrabackup安装与使用
##install
wget https://www.percona.com/downloads/XtraBackup/Percona-XtraBackup-2.4.4/\
binary/redhat/7/x86_64/percona-xtrabackup-24-2.4.4-1.el7.x86_64.rpm
yum localinstall percona-xtrabackup-24-2.4.4-1.el7.x86_64.rpm
## uninstall
yum remove percona-xtrabackup
备份命令只能在数据库服务器上执行。
备份到远程服务器
专有云oss配置(一般不需要该配置)
修改配置文件
您需要运行以下命令,修改 /etc/sysctl.conf 配置文件。
sudo vi /etc/sysctl.conf
对比和添加参数
对照以下参数编辑配置文件,添加缺少的参数。参数的 key 和 value 都需要与以下内容保持一致
vm.max_map_count = 8388608
net.ipv4.tcp_rmem = 4096 87380 4194304
net.ipv4.tcp_wmem = 4096 16384 4194304
net.core.wmem_default = 8388608
net.core.rmem_default = 8388608
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.netdev_max_backlog = 204800
net.core.somaxconn = 204800
net.ipv4.tcp_max_orphans = 3276800
net.ipv4.tcp_max_syn_backlog = 204800
net.ipv4.tcp_tw_recycle = 0
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.ip_local_port_range = 1024 65000
net.ipv4.tcp_syncookies = 0
重新加载内存参数
修改完毕后,运行以下命令重新加载内存参数。
sudo /sbin/sysctl -p
专有云配置阿里OSS
nginx访问文件提示403
主要问题在于把静态文件放到了/root/www下,而此时的nginx启动是没有以root用户启动,要么把文件移动到非root目录,要么在nginx.conf里加入user root
,再重新启动即可
nginx开启访问日志及错误日志
在nginx.conf主配置文件http模块里要设置日志格式,
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
再到具体的配置文件里开启日志:
access_log /service/access_log/admin_front.log main;
error_log /service/access_log/admin_front_error.log error;
遗留问题
1、Optional类针对空指针的使用
2、Goods分页查询的使用
3、Git解决冲突,防止代码丢失
4、mybatis自定义sql语句
5、mysql高可用方案:xtraback远程备份,目前只能本地
6、服务器安全防护:nginx,限制访问静态资源,跨域等安全问题
7、后台开放设置用户注销,禁用,启用状态
8、前端根据用户状态认证登录
9、数据库备份过慢,不知是否为网速问题
远程备份只能是从数据库本地备份再scp到远程存储服务器里。
远程备份xtrabackup
optional官方教程最好先看lambda
lambda part1官方教程
lambda part2